Skip to content

feat: testFunc method on schemas for easier custom tests#82

Merged
Oudwins merged 3 commits intomasterfrom
feat/testfuncs
Feb 10, 2025
Merged

feat: testFunc method on schemas for easier custom tests#82
Oudwins merged 3 commits intomasterfrom
feat/testfuncs

Conversation

@Oudwins
Copy link
Owner

@Oudwins Oudwins commented Feb 10, 2025

Summary by CodeRabbit

  • New Features
    • Introduced a streamlined way for users to add custom validation tests across various schema types, enabling inline test functions with tailored messages.
  • Tests
    • Extended the test suite to cover the new custom test capabilities, ensuring robust validation across data types.
  • Documentation
    • Updated core validation documentation to reflect the revised API for custom tests and revised error reporting details.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2025

Walkthrough

This pull request introduces a new method, TestFunc, across several schema types (Boolean, Number, Slice, String, Struct, and Time) to enable the registration of custom test functions similar to Zod’s .refine() method. It standardizes the API for adding custom validations by allowing inline function definitions and supports method chaining. Additionally, test files have been updated to use the new API, and documentation has been revised to reflect these changes, including an update to the type of the error code field.

Changes

File(s) Change Summary
boolean.go, numbers.go, slices.go, string.go, struct.go, time.go Added new TestFunc method to each schema type, which accepts a custom test function (p.TestFunc) and optional parameters, constructs a test object, and appends it to the schema’s tests. This enables flexible, inline validation logic and supports method chaining.
boolean_test.go, boolean_validate_test.go, numbers_test.go, numbers_validate_test.go, slices_test.go, slices_validate_test.go, string_test.go, string_validate_test.go, struct_test.go, time_test.go Updated test cases to replace usage of the old Test(TestFunc(...)) pattern with direct TestFunc calls. Several test function names have been renamed (e.g., TestStringUserTests to TestStringCustomTest), and some assertions (particularly error codes) were adjusted or removed to align with the new API.
docs/docs/core-concepts/1-anatomy-of-schema.md Updated documentation for the Test struct by changing the type of the ErrCode field from zconst.ZogErrCode to zconst.ZogIssueCode. Additionally, the custom test example now uses TestFunc instead of the previous Test wrapping, with a warning note about potential future API changes.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Schema
    participant Validator

    User->>Schema: Create schema instance (e.g., BoolSchema, NumberSchema, etc.)
    User->>Schema: Call TestFunc(customTestFunc, options...)
    Schema->>Schema: Construct a Test object with customTestFunc
    Schema->>Schema: Append Test to internal tests list
    User->>Validator: Validate input using the schema
    Validator->>Schema: Invoke registered custom test(s)
    Schema->>Validator: Return test result (pass/fail)
Loading

Possibly related PRs

Poem

I'm a bunny on a code spree, hopping through schemas with glee,
With TestFunc new and bright, my validations leap to new height! 🐇
Inline tests now smooth and neat, making custom checks a treat,
In every file—from string to time,
My carrot-coded flow is simply sublime!
Hooray for changes, swift and keen, a rabbit's joy in every scene!
🥕🐰 Happy coding!


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec629a0 and 8591ccb.

📒 Files selected for processing (1)
  • boolean_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • boolean_test.go

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 10, 2025

Deploying zog with  Cloudflare Pages  Cloudflare Pages

Latest commit: ec629a0
Status: ✅  Deploy successful!
Preview URL: https://65cc5bb8.zog-3a0.pages.dev
Branch Preview URL: https://feat-testfuncs.zog-3a0.pages.dev

View logs

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (9)
boolean.go (1)

90-95: Consider adding validation for nil testFunc.

While the implementation is good, it might be worth adding a nil check for the testFunc parameter to prevent potential panics.

 func (v *BoolSchema) TestFunc(testFunc p.TestFunc, options ...TestOption) *BoolSchema {
+    if testFunc == nil {
+        panic("testFunc cannot be nil")
+    }
     test := TestFunc("", testFunc)
     v.Test(test, options...)
     return v
 }
numbers.go (1)

153-158: Consider adding validation for nil testFunc.

While the implementation is good, it should include a nil check for the testFunc parameter, similar to the suggestion for BoolSchema.

 func (v *NumberSchema[T]) TestFunc(testFunc p.TestFunc, options ...TestOption) *NumberSchema[T] {
+    if testFunc == nil {
+        panic("testFunc cannot be nil")
+    }
     test := TestFunc("", testFunc)
     v.Test(test, options...)
     return v
 }
numbers_test.go (1)

284-289: Enhance test coverage for the custom test function.

The test function only verifies the success case. Consider adding test cases for:

  1. When the custom test function returns false.
  2. Edge cases like zero, negative numbers, etc.

Apply this diff to improve test coverage:

-func TestNumberCustomTest(t *testing.T) {
+func TestNumberCustomTest(t *testing.T) {
+	tests := []struct {
+		name      string
+		input     int
+		expected  bool
+		expectErr bool
+	}{
+		{
+			name:     "Success case",
+			input:    5,
+			expected: true,
+		},
+		{
+			name:      "Failure case",
+			input:     0,
+			expected:  false,
+			expectErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			validator := Int().TestFunc(func(val any, ctx Ctx) bool {
+				num := val.(int)
+				return num > 0
+			}, Message("custom"))
+
+			dest := 0
+			errs := validator.Parse(tt.input, &dest)
+
+			if tt.expectErr {
+				assert.NotEmpty(t, errs)
+			} else {
+				assert.Empty(t, errs)
+			}
+			assert.Equal(t, tt.input, dest)
+		})
+	}
}
boolean_validate_test.go (1)

378-390: Enhance test coverage for the custom test function.

The test function only verifies the success case. Consider adding test cases for:

  1. When the custom test function returns false.
  2. Edge cases like false value.

Apply this diff to improve test coverage:

-func TestBoolValidateCustomTest(t *testing.T) {
+func TestBoolValidateCustomTest(t *testing.T) {
+	tests := []struct {
+		name      string
+		input     bool
+		expected  bool
+		expectErr bool
+	}{
+		{
+			name:     "Success case",
+			input:    true,
+			expected: true,
+		},
+		{
+			name:      "Failure case",
+			input:     false,
+			expected:  false,
+			expectErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			validator := Bool().TestFunc(func(val any, ctx Ctx) bool {
+				b := val.(bool)
+				return b
+			}, Message("custom"))
+
+			dest := tt.input
+			errs := validator.Validate(&dest)
+
+			if tt.expectErr {
+				assert.NotEmpty(t, errs)
+			} else {
+				assert.Empty(t, errs)
+			}
+			assert.Equal(t, tt.expected, dest)
+		})
+	}
}
struct_test.go (2)

141-152: Enhance test coverage for the custom test function.

The test function only verifies the success case. Consider adding test cases for:

  1. When the custom test function returns false.
  2. Edge cases like zero or negative numbers.

Apply this diff to improve test coverage:

-	// Create a custom test function
-	customTest := func(val any, ctx ParseCtx) bool {
-		// Custom test logic here
-		num := val.(int)
-		return num > 0
-	}
-
-	// Create a schema with a custom test
-	schema := Struct(Schema{
-		"str": String().Required(),
-		"num": Int().TestFunc(customTest),
-	})
+	tests := []struct {
+		name      string
+		input     int
+		expected  bool
+		expectErr bool
+	}{
+		{
+			name:     "Success case",
+			input:    10,
+			expected: true,
+		},
+		{
+			name:      "Failure case - zero",
+			input:     0,
+			expected:  false,
+			expectErr: true,
+		},
+		{
+			name:      "Failure case - negative",
+			input:     -1,
+			expected:  false,
+			expectErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			schema := Struct(Schema{
+				"str": String().Required(),
+				"num": Int().TestFunc(func(val any, ctx ParseCtx) bool {
+					num := val.(int)
+					return num > 0
+				}),
+			})
+
+			var obj CustomStruct
+			data := map[string]any{
+				"str": "hello",
+				"num": tt.input,
+			}
+
+			errs := schema.Parse(data, &obj)
+			if tt.expectErr {
+				assert.NotNil(t, errs)
+			} else {
+				assert.Nil(t, errs)
+				assert.Equal(t, "hello", obj.Str)
+				assert.Equal(t, tt.input, obj.Num)
+			}
+		})
+	}

171-186: Uncomment the error code assertion.

The error code assertion has been commented out. Since error codes are important for error handling, consider keeping the assertion.

Apply this diff to restore the error code assertion:

-	// assert.Equal(t, "customTest", errs["$root"][0].Code())
+	assert.Equal(t, "customTest", errs["$root"][0].Code())
string.go (1)

165-170: LGTM! Consider adding a custom error code parameter.

The implementation is clean and follows the method chaining pattern. However, consider adding an optional error code parameter to allow users to customize the error message.

Apply this diff to add an optional error code parameter:

-func (v *StringSchema) TestFunc(testFunc p.TestFunc, options ...TestOption) *StringSchema {
-	test := TestFunc("", testFunc)
+func (v *StringSchema) TestFunc(testFunc p.TestFunc, errCode string, options ...TestOption) *StringSchema {
+	test := TestFunc(errCode, testFunc)
docs/docs/core-concepts/1-anatomy-of-schema.md (2)

64-64: Replace hard tab with spaces.

Replace the hard tab with spaces to maintain consistent formatting.

Apply this diff to fix the formatting:

-	ErrCode      zconst.ZogIssueCode // the error code to use if the validation fails. This helps identify the type of error, for example ErrCodeMin identifies the Min() test
+    ErrCode      zconst.ZogIssueCode // the error code to use if the validation fails. This helps identify the type of error, for example ErrCodeMin identifies the Min() test
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

64-64: Hard tabs
Column: 1

(MD010, no-hard-tabs)


78-80: Enhance example to show error code usage.

The example could be improved by showing how to use a custom error code with TestFunc.

Apply this diff to enhance the example:

-z.String().TestFunc(func(data any, ctx z.Ctx) bool {
+z.String().TestFunc(func(data any, ctx z.Ctx) bool {
   return data == "test"
-}))
+}, "custom_test_error"))
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1560f62 and ec629a0.

📒 Files selected for processing (18)
  • boolean.go (1 hunks)
  • boolean_test.go (1 hunks)
  • boolean_validate_test.go (1 hunks)
  • docs/docs/core-concepts/1-anatomy-of-schema.md (2 hunks)
  • numbers.go (1 hunks)
  • numbers_test.go (1 hunks)
  • numbers_validate_test.go (1 hunks)
  • slices.go (1 hunks)
  • slices_test.go (1 hunks)
  • slices_validate_test.go (2 hunks)
  • string.go (1 hunks)
  • string_test.go (1 hunks)
  • string_validate_test.go (1 hunks)
  • struct.go (1 hunks)
  • struct_test.go (3 hunks)
  • time.go (1 hunks)
  • time_test.go (1 hunks)
  • time_validate_test.go (1 hunks)
🧰 Additional context used
🪛 markdownlint-cli2 (0.17.2)
docs/docs/core-concepts/1-anatomy-of-schema.md

64-64: Hard tabs
Column: 1

(MD010, no-hard-tabs)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (13)
time_validate_test.go (1)

118-120: LGTM! Simplified custom test function syntax.

The changes improve readability by using the new TestFunc method, which provides a cleaner way to define custom test functions.

boolean.go (1)

82-88: LGTM! Well-implemented test method.

The Test method correctly applies options and maintains method chaining.

time_test.go (1)

123-125: LGTM! Consistent with the new API.

The changes align with the new TestFunc method pattern, maintaining consistency across the codebase.

slices_test.go (2)

219-223: LGTM! The test function has been simplified.

The change aligns with the PR objective of making custom tests easier to define by removing the need for explicit test function naming.


230-230: Verify if error code assertion should be removed.

The commented-out assertion suggests a change in error code handling. Please verify if this is intentional and aligns with the broader changes in error handling across the codebase.

✅ Verification successful

🏁 Script executed:

#!/bin/bash
# Description: Check for similar error code assertions in other test files.

# Test: Search for error code assertions. Expect: Pattern of commented or removed assertions.
rg -A 1 'assert\.Equal\(t, .+, errs\[.+\]\.Code\(\)\)'

Length of output: 685


Subject: Verified – Removal of error code assertions in slices tests is intentional

The investigation shows that the error code assertions in slices tests (e.g., in slices_test.go and slices_validate_test.go) are commented out, while similar assertions remain active in struct tests with a different expected code format. This indicates that the removal was deliberate to accommodate the updated error handling approach for slices.

time.go (1)

166-171: LGTM! The TestFunc method implementation is clean and consistent.

The implementation follows the same pattern as other schema types and maintains proper method chaining.

Please verify if using an empty string as the error code is intentional:

✅ Verification successful

🌐 Web query:

What is the recommended practice for handling error codes in Go validation libraries like Zod?

💡 Result:

Error handling in Go validation follows different patterns than TypeScript's Zod due to language design differences, but several best practices emerge from Go's ecosystem:

Core Principles for Go Validation Errors

1. Custom Error Types with Context

  • Define validation-specific error types implementing error interface
  • Include contextual details (field path, expected/received values):
type ValidationError struct {
    Field   string
    Message string
    Code    int
}

2. Sentinel Errors for Specific Cases

  • Use package-level variables for common validation failures:
var (
    ErrInvalidEmail = errors.New("invalid email format")
    ErrRequiredField = errors.New("field is required")
)

3. Structured Error Returns

  • Return collections of errors for batch validation:
func ValidateUser(u User) []ValidationError {
    var errs []ValidationError
    if u.Email == "" {
        errs = append(errs, ValidationError{
            Field: "email",
            Message: ErrRequiredField.Error(),
            Code: 1001,
        })
    }
    return errs
}

Recommended Patterns from Search Results

Error Wrapping (Go 1.13+)

  • Use fmt.Errorf with %w verb to preserve error chain[1][9]:
if err := validate(input); err != nil {
    return fmt.Errorf("validation failed: %w", err)
}

Functional Options Validation

  • Implement reusable validation rules as functions[4]:
type Validator func(interface{}) error

func WithMinLength(n int) Validator {
    return func(v interface{}) error {
        s, ok := v.(string)
        if !ok || len(s) < n {
            return fmt.Errorf("minimum length %d required", n)
        }
        return nil
    }
}

HTTP Error Handling

  • Convert validation errors to structured HTTP responses[10]:
func handleError(w http.ResponseWriter, errs []ValidationError) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusBadRequest)
    json.NewEncoder(w).Encode(map[string]interface{}{
        "errors": errs,
    })
}

Comparison with Zod-like Features

Aspect Zod (TypeScript) Go Equivalent
Error Structure ZodError with issues Slice of custom error structs
Error Codes Predefined ZodIssueCode Custom constants/sentinel errors
Path Tracking path array property Field names in error structs
Custom Messages Error maps Error struct methods/String() impl
Batch Validation Automatic aggregation Manual error slice collection

For complex validations, consider using established Go libraries like:

  • go-playground/validator: Struct tag-based validation
  • asaskevich/govalidator: Common validation rules
  • hashicorp/go-cty: Type system with validation

The key differentiator in Go is explicit error handling - rather than throwing exceptions, validation errors should be treated as normal return values that callers must consciously handle[1][7]. This aligns with Go's philosophy of making error conditions explicit rather than hidden through exception propagation.

Citations:


The implementation is consistent with the intent of the library. In similar schema types throughout the codebase (e.g., for string, number, etc.), a call to the internal helper function (named TestFunc) is made with an empty error code string when a code is not explicitly provided. The empty string here acts as a default “no code” signal, aligning with the library’s pattern rather than implying an error. In Go’s ecosystem, while many validation libraries may use structured or numeric error codes, this library’s design choice to use an empty string here is intentional and consistent with its method chaining pattern.

Actionable Note:

  • No changes are needed regarding the empty string for the error code in the TestFunc method in time.go.
slices_validate_test.go (2)

157-162: LGTM! The test function has been simplified.

The change aligns with the PR objective of making custom tests easier to define.


176-176: Verify if error code assertion should be removed.

The commented-out assertion suggests a change in error code handling. Please verify if this is intentional and aligns with the broader changes in error handling across the codebase.

numbers_validate_test.go (1)

274-278: LGTM! The test function has been simplified.

The change aligns with the PR objective of making custom tests easier to define.

struct.go (1)

297-302: LGTM! The implementation is clean and consistent.

The TestFunc method follows the same pattern as other schema types, making it easy to understand and maintain.

string_test.go (1)

101-118: LGTM! Well-structured test function.

The test function is well-designed, covering both success and failure cases for custom validation, with proper error message verification.

string_validate_test.go (1)

106-122: LGTM! Comprehensive test coverage.

The test function effectively validates the custom test functionality using both success and failure cases, with proper error message verification.

slices.go (1)

280-285: LGTM! Clean implementation of TestFunc.

The implementation is consistent with other schema types and properly leverages the existing Test method.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant