Skip to content

feat!: transforms and tests are now typesafe for the schema you are u… #149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 27, 2025

Conversation

Oudwins
Copy link
Owner

@Oudwins Oudwins commented Apr 26, 2025

…sing. No more typecasting in Zog!

Summary by CodeRabbit

  • Refactor
    • Unified and modernized schema validation and transformation logic using generics for stronger type safety and consistency across all schema types.
    • Updated method and function signatures to use explicit pointer types, removing deprecated methods and reducing the need for type assertions.
    • Enhanced test and transformation callback signatures for improved clarity and maintainability.
  • Bug Fixes
    • Improved error handling and validation accuracy by aligning error codes and parameter types.
  • Chores
    • Removed deprecated methods and unused imports for a cleaner codebase.

Copy link
Contributor

coderabbitai bot commented Apr 26, 2025

Walkthrough

This update refactors the schema validation and transformation framework to use Go generics with explicit pointer types throughout all schema types and their associated processing, transformation, and test methods. All relevant type aliases, interfaces, and method signatures are updated to accept generic pointer types, replacing previous usage of the empty interface (any) and removing type assertions. Deprecated methods such as PostTransform are removed across all schema types. Test and transform functions in both implementation and test code are updated for strong type safety, and internal abstractions are refactored to use generic interfaces and methods for encapsulation and extensibility.

Changes

File(s) Change Summary
boolean.go, numbers.go, string.go, time.go All schema types updated to use generic pointer types for processors, tests, and transforms; removed deprecated PostTransform methods; updated method signatures for Test, TestFunc, Transform, and Required.
slices.go, struct.go, struct_helpers.go Updated to use generics with any for processors and tests; removed deprecated PostTransform methods; updated helper functions and method signatures for generics.
pointers.go Changed required field and NotNil method to use generic test type *p.Test[any].
custom.go Refactored Custom[T] to use p.Test[*T] and pointer-based test functions.
internals/processors.go, internals/tests.go, internals/types.go Refactored all processor, test, and transform interfaces/types to be generic; introduced TestInterface for abstraction; updated all related methods and functions to use generics and pointer types.
internals/contexts.go Changed Processor field to any; updated IssueFromTest to use TestInterface with getter methods.
utils.go, utilsOptions.go, zogSchema.go Made all type aliases and helper functions generic; removed deprecated aliases and backwards compatibility wrappers; updated option functions to use interface methods.
*_test.go (all test files) Updated all transform and test function signatures to accept explicit pointer types instead of any; replaced .PostTransform() with .Transform() where applicable; removed type assertions; updated error assertions for new types.
reusable_tests_test.go Made the reusable test function generic over pointer types.
zhttp/zhttp_test.go Updated custom test function for swagger field to use *string pointer type.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Schema (generic)
    participant Processor (generic)
    participant Test (generic)
    participant Transform (generic)
    participant Ctx

    User->>Schema: Define schema with Test[*T], Transform[*T]
    User->>Schema: Call Validate/Parse(input)
    Schema->>Processor: Process input using ZProcess(*T, Ctx)
    Processor->>Transform: (if transform) Transform(*T, Ctx)
    Processor->>Test: (if test) Test.Func(*T, Ctx)
    Test->>Ctx: Report issue via TestInterface if validation fails
    Schema->>User: Return result or issues
Loading

Possibly related PRs

Poem

Hopping through code with pointer delight,
Generics now guide us, making types right!
No more old transforms, no more type guess—
Just strong, safe schemas, pointer-based success.
Every test and transform knows just what to do,
With a twitch of my nose, I say, “Thank you!”
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 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.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @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.

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: 4

🔭 Outside diff range comments (4)
zogSchema.go (2)

73-75: ⚠️ Potential issue

Pass the pointer (destPtr), not the dereferenced value, to IssueFromTest

required is declared as *p.Test[*T], i.e. the test expects a pointer to T.
By dereferencing destPtr you are handing a value (T) to a function that is instantiated for *T, which results in a compile-time type mismatch:

ctx.AddIssue(ctx.IssueFromTest(required, *destPtr)) // ❌ does not compile

Fix:

- ctx.AddIssue(ctx.IssueFromTest(required, *destPtr))
+ ctx.AddIssue(ctx.IssueFromTest(required, destPtr))

The same pattern occurs in primitiveValidation (see comment below).
Without this change the project will not build.


125-126: ⚠️ Potential issue

Same pointer / value mix-up in primitiveValidation

Analogous to the previous comment, use valPtr instead of *valPtr:

- ctx.AddIssue(ctx.IssueFromTest(required, *valPtr))
+ ctx.AddIssue(ctx.IssueFromTest(required, valPtr))

This removes the generic type conflict and allows the file to compile.

internals/tests.go (1)

123-164: ⚠️ Potential issue

Nil-pointer dereference in length helpers

All LenMin/LenMax/Len helpers dereference *val without verifying that val is non-nil.
A nil *slice or nil *string will panic at runtime during validation.

-fn := func(val *T, ctx Ctx) bool {
-    return len(*val) >= n
-}
+fn := func(val *T, ctx Ctx) bool {
+    if val == nil {
+        return false // treat nil as failing the test
+    }
+    return len(*val) >= n
+}

Apply the same guard to LenMax and Len.

string.go (1)

345-360: ⚠️ Potential issue

Order of option application

Options are applied after the test function is wrapped.
If an option overwrites IssueCode, it will not be reflected in the already-wrapped Func.
Swap the order to avoid subtle bugs:

-if v.isNot {
+for _, opt := range options {
+    opt(&t)
+}
+
+if v.isNot {
     …
 }
-
-for _, opt := range options {
-    opt(&t)
-}
🧹 Nitpick comments (29)
internals/types.go (1)

10-10: Consider updating PostTransform to be generic for complete consistency.

While Transform has been made generic with type parameter T, the PostTransform type on line 10 is still using the untyped any parameter. For complete consistency in the type system, consider updating PostTransform to also use a generic type parameter, or document why it remains non-generic if there's a specific reason.

-type PostTransform = func(dataPtr any, ctx Ctx) error
+type PostTransform[T any] = func(dataPtr T, ctx Ctx) error
struct_test.go (1)

209-212: Consider updating this test function in a future PR

While this PR focuses on making transforms and tests typesafe, there's still a type assertion in this test function (val.(*CustomStruct)). Consider updating this in a future PR to also use a typed pointer parameter for complete type safety.

struct_helper_test.go (1)

173-177: Consider updating transform functions in a future PR

While this PR replaces deprecated methods, the transform functions still use any types with type assertions. Consider updating these in a future PR to use typed pointer parameters for complete type safety, similar to how the test functions were updated.

Also applies to: 203-207

time_validate_test.go (1)

53-58: Type-safe transform function with simplified code

The transform function now uses a typed pointer parameter (*time.Time) instead of any, improving type safety. However, the code could be simplified by removing the temporary variable.

validator := Time().Transform(func(dataPtr *time.Time, ctx Ctx) error {
    // Set the time to noon
-   t := dataPtr
-   *t = time.Date(t.Year(), t.Month(), t.Day(), 12, 0, 0, 0, t.Location())
+   *dataPtr = time.Date(dataPtr.Year(), dataPtr.Month(), dataPtr.Day(), 12, 0, 0, 0, dataPtr.Location())
    return nil
})
numbers.go (3)

168-170: Guard against a nil transform being registered

TransformProcessor.ZProcess will panic if the supplied Transform is nil; add an early check so misuse is caught at the call-site and the stack-trace is easier to read.

 func (v *NumberSchema[T]) Transform(transform p.Transform[*T]) *NumberSchema[T] {
-	v.processors = append(v.processors, &p.TransformProcessor[*T]{Transform: transform})
+	if transform == nil {
+		panic("zog: NumberSchema.Transform received a nil function")
+	}
+	v.processors = append(v.processors, &p.TransformProcessor[*T]{Transform: transform})
 	return v
 }

176-183: Allocate the ‘required’ test directly to avoid pointer-to-stack subtlety

Escaping works fine, but using new makes the intent explicit and avoids an extra copy when options mutate the struct.

-	r := p.Required[*T]()
-	for _, opt := range options {
-		opt(&r)
-	}
-	v.required = &r
+	req := p.Required[*T]()
+	for _, opt := range options {
+		opt(&req)
+	}
+	v.required = &req

204-207: *Prefer accepting a Test to avoid an extra struct copy

The current signature forces callers to pass a value, only for the method to take its address again. Accepting a pointer avoids the copy and lines up with how NewTestFunc returns its value.

-func (v *NumberSchema[T]) Test(t Test[*T]) *NumberSchema[T] {
-	x := p.Test[*T](t)
-	v.processors = append(v.processors, &x)
+func (v *NumberSchema[T]) Test(t *p.Test[*T]) *NumberSchema[T] {
+	v.processors = append(v.processors, t)
 	return v
 }

Updating this requires changing the two call-sites (TestFunc here and the tests elsewhere) to pass the pointer directly.

boolean.go (4)

91-94: Avoid copying the Test struct unnecessarily

Same optimisation as suggested for NumberSchema:

-func (v *BoolSchema[T]) Test(t p.Test[*T]) *BoolSchema[T] {
-	v.processors = append(v.processors, &t)
+func (v *BoolSchema[T]) Test(t *p.Test[*T]) *BoolSchema[T] {
+	v.processors = append(v.processors, t)
 	return v
 }

97-101: Pass pointer returned by NewTestFunc straight through

Once the signature above is changed, this can avoid the dereference / copy.

-	test := p.NewTestFunc("", testFunc, options...)
-	v.Test(*test)
+	v.Test(p.NewTestFunc("", testFunc, options...))

104-107: Nil-transform guard

Replicate the safeguard proposed for NumberSchema.Transform to catch accidental nil:

 func (v *BoolSchema[T]) Transform(transform p.Transform[*T]) *BoolSchema[T] {
-	v.processors = append(v.processors, &p.TransformProcessor[*T]{Transform: transform})
+	if transform == nil {
+		panic("zog: BoolSchema.Transform received a nil function")
+	}
+	v.processors = append(v.processors, &p.TransformProcessor[*T]{Transform: transform})
 	return v
 }

112-118: Same pointer-to-stack remark as for NumberSchema.Required

Consider:

-	r := p.Required[*T]()
-	for _, opt := range options {
-		opt(&r)
-	}
-	v.required = &r
+	req := p.Required[*T]()
+	for _, opt := range options {
+		opt(&req)
+	}
+	v.required = &req
struct.go (3)

193-197: Ensure transform is non-nil

Same small guard as in the primitive schemas:

 func (v *StructSchema) Transform(transform p.Transform[any]) *StructSchema {
-	v.processors = append(v.processors, &p.TransformProcessor[any]{Transform: transform})
+	if transform == nil {
+		panic("zog: StructSchema.Transform received a nil function")
+	}
+	v.processors = append(v.processors, &p.TransformProcessor[any]{Transform: transform})
 	return v
 }

228-231: *Accept Test instead of value for consistency & zero copies

-func (v *StructSchema) Test(t Test[any]) *StructSchema {
-	x := p.Test[any](t)
-	v.processors = append(v.processors, &x)
+func (v *StructSchema) Test(t *p.Test[any]) *StructSchema {
+	v.processors = append(v.processors, t)
 	return v
 }

This matches the pattern used inside primitives once adjusted.


235-238: Propagate pointer after the signature tweak

-	test := p.NewTestFunc("", p.BoolTFunc[any](testFunc), options...)
-	v.Test(Test[any](*test))
+	v.Test(p.NewTestFunc("", p.BoolTFunc[any](testFunc), options...))
zogSchema.go (1)

47-49: Consider returning a pointer from TestFunc to avoid unnecessary copying

p.NewTestFunc already returns *p.Test[T]. Dereferencing it to a value, only to
take its address again in every call site (&x, &r, …), causes an avoidable allocation / copy.

-func TestFunc[T any](IssueCode zconst.ZogIssueCode, fn BoolTFunc[T], options ...p.TestOption) Test[T] {
-    return Test[T](*p.NewTestFunc(IssueCode, p.BoolTFunc[T](fn), options...))
+func TestFunc[T any](IssueCode zconst.ZogIssueCode, fn BoolTFunc[T], options ...p.TestOption) *Test[T] {
+    return (*Test[T])(p.NewTestFunc(IssueCode, p.BoolTFunc[T](fn), options...))
 }

This is an optional micro-optimisation; feel free to ignore if the value semantics are intentional.

time.go (2)

115-118: Ignore-on-error policy is unclear in Transform wrapper

Transform functions return an error, but the enclosing TransformProcessor
silently discards it unless the inner implementation calls ctx.AddIssue
itself.
If that’s intended, add a short comment; otherwise consider propagating the
error through ctx.AddIssue(ctx.IssueFromTransform(err)) (or similar) so that
users don’t accidentally lose transform failures.


173-175: Nil-safety for custom time tests

fn := func(v *time.Time, ctx Ctx) bool { return (*v).After(t) }

If the caller supplies nil (which can happen when the field is optional and
unset), this will panic. A defensive check is cheap:

- return (*v).After(t)
+ if v == nil {
+     return false
+ }
+ return v.After(t)

The same applies to Before and EQ.

slices.go (1)

222-226: Pointer escapes on every Test call

x := p.Test[any](t)
v.processors = append(v.processors, &x)

Creating a new x value only to take its address causes an extra heap
allocation. If you apply the earlier suggestion to make TestFunc
return *Test[T], you can store the pointer directly and avoid this
pattern entirely.

internals/tests.go (4)

52-61: Interface getters / setters are verbose

Because every method is simple field access, an embedded struct or struct{ Test[*T] } wrapper could expose the fields directly and drop eight boiler-plate methods.
Not critical, but worth considering for maintainability.


77-79: Receiver pointer vs value

ZProcess takes valPtr T, not *T. This matches the generic instantiation (Test[*T]) but is slightly surprising in isolation. A short comment explaining that T may itself be a pointer (e.g. *string) would help future readers.


114-121: Required() returns a test with a nil Func

Down-stream processors must special-case a nil function. If that behaviour is relied upon, add a short doc-comment to avoid accidental misuse.


166-182: Avoid reflect.DeepEqual in In()

reflect.DeepEqual is slow and blocks comparability with non-comparable elements.
If you constrain T to comparable, a direct == comparison is possible and faster:

-func In[T any](values []T)
+func In[T comparable](values []T)

For non-comparable types you could fall back to hashing or keep the current reflect-based path behind a build tag.

string.go (7)

176-179: Heap escape for local x

x escapes to the heap when you store &x in processors, so there is no dangling-pointer bug.
Still, you could avoid the extra copy by appending tPtr := &t directly.

- x := p.Test[*T](t)
- v.processors = append(v.processors, &x)
+ tPtr := p.Test[*T](t) // copy for safety
+ v.processors = append(v.processors, &tPtr)

183-187: Empty issue code in TestFunc

NewTestFunc("", …) produces a test with an empty IssueCode, which may confuse issue formatters.

Expose IssueCode as an optional parameter or provide a sensible default (e.g. "custom").


214-218: Email regex correctness

The current RFC-5322-inspired regex rejects valid internationalised email addresses (IDNA).
Consider switching to net/mail’s parser or documenting the limitation.


224-230: URL validation accepts spaces

url.Parse happily parses strings with spaces ("http://example.com /foo").
A stricter check like strings.ContainsAny(raw, " \t\r\n") prevents false positives.


270-281: ASCII-only uppercase check

r >= 'A' && r <= 'Z' fails for Unicode uppercase letters such as Æ or Ñ.

-import "strings"
+import (
+    "strings"
+    "unicode"
+)-for _, r := range string(*v) {
-    if r >= 'A' && r <= 'Z' {
+for _, r := range string(*v) {
+    if unicode.IsUpper(r) {
         return true
     }
 }

300-314: Special-character range misses some symbols

Characters outside '!'–'~' (e.g., non-ASCII punctuation) are skipped.
If that is intentional, add a comment; otherwise consider unicode.IsPunct or a predefined class.


318-323: UUID regex

The current pattern permits upper-case A–F, 👍.
Consider anchoring with ^/$ or using ParseUUID from github.com/google/uuid for guaranteed compliance.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 294b73a and 301c820.

📒 Files selected for processing (32)
  • boolean.go (2 hunks)
  • boolean_test.go (2 hunks)
  • boolean_validate_test.go (3 hunks)
  • custom.go (2 hunks)
  • internals/contexts.go (2 hunks)
  • internals/processors.go (1 hunks)
  • internals/tests.go (4 hunks)
  • internals/types.go (1 hunks)
  • numbers.go (3 hunks)
  • numbers_custom_test.go (3 hunks)
  • numbers_int64_test.go (3 hunks)
  • numbers_test.go (3 hunks)
  • numbers_validate_test.go (3 hunks)
  • pointers.go (2 hunks)
  • reusable_tests_test.go (1 hunks)
  • slices.go (8 hunks)
  • string.go (8 hunks)
  • string_custom_test.go (3 hunks)
  • string_test.go (2 hunks)
  • string_validate_test.go (2 hunks)
  • struct.go (3 hunks)
  • struct_helper_test.go (3 hunks)
  • struct_helpers.go (1 hunks)
  • struct_test.go (2 hunks)
  • struct_validate_test.go (2 hunks)
  • time.go (5 hunks)
  • time_test.go (2 hunks)
  • time_validate_test.go (2 hunks)
  • utils.go (1 hunks)
  • utilsOptions.go (3 hunks)
  • zhttp/zhttp_test.go (1 hunks)
  • zogSchema.go (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (15)
struct_helpers.go (1)
internals/processors.go (1)
  • ZProcessor (4-6)
utils.go (1)
internals/tests.go (1)
  • Test (64-75)
reusable_tests_test.go (6)
internals/tests.go (2)
  • TestOption (17-17)
  • Test (64-75)
utilsOptions.go (2)
  • TestOption (10-10)
  • Message (13-19)
utils.go (2)
  • Test (42-42)
  • Ctx (12-12)
zogSchema.go (1)
  • TestFunc (47-49)
zconst/consts.go (1)
  • ZogIssueCode (54-54)
internals/contexts.go (1)
  • Ctx (8-42)
time_validate_test.go (5)
time.go (1)
  • Time (37-45)
internals/types.go (1)
  • Transform (12-12)
zogSchema.go (2)
  • Transform (34-34)
  • TestFunc (47-49)
internals/contexts.go (1)
  • Ctx (8-42)
utils.go (1)
  • Ctx (12-12)
pointers.go (1)
internals/tests.go (1)
  • Test (64-75)
custom.go (4)
internals/tests.go (3)
  • Test (64-75)
  • TestOption (17-17)
  • TestFuncFromBool (19-28)
utils.go (2)
  • Test (42-42)
  • Ctx (12-12)
internals/contexts.go (1)
  • Ctx (8-42)
utilsOptions.go (1)
  • TestOption (10-10)
struct_validate_test.go (3)
internals/contexts.go (1)
  • Ctx (8-42)
internals/types.go (1)
  • Transform (12-12)
zogSchema.go (1)
  • Transform (34-34)
boolean_test.go (5)
internals/types.go (1)
  • Transform (12-12)
zogSchema.go (2)
  • Transform (34-34)
  • TestFunc (47-49)
internals/contexts.go (1)
  • Ctx (8-42)
utils.go (1)
  • Ctx (12-12)
boolean.go (1)
  • Bool (34-42)
boolean.go (6)
internals/processors.go (2)
  • ZProcessor (4-6)
  • TransformProcessor (8-10)
internals/tests.go (5)
  • Test (64-75)
  • BoolTFunc (11-11)
  • TestOption (17-17)
  • NewTestFunc (41-50)
  • Required (114-121)
utils.go (1)
  • Test (42-42)
zogSchema.go (3)
  • TestFunc (47-49)
  • BoolTFunc (44-44)
  • Transform (34-34)
utilsOptions.go (1)
  • TestOption (10-10)
internals/types.go (1)
  • Transform (12-12)
struct.go (6)
internals/processors.go (2)
  • ZProcessor (4-6)
  • TransformProcessor (8-10)
internals/tests.go (4)
  • Test (64-75)
  • BoolTFunc (11-11)
  • TestOption (17-17)
  • NewTestFunc (41-50)
utils.go (1)
  • Test (42-42)
internals/types.go (1)
  • Transform (12-12)
zogSchema.go (3)
  • Transform (34-34)
  • TestFunc (47-49)
  • BoolTFunc (44-44)
utilsOptions.go (1)
  • TestOption (10-10)
numbers.go (6)
internals/processors.go (2)
  • ZProcessor (4-6)
  • TransformProcessor (8-10)
internals/tests.go (5)
  • Test (64-75)
  • Required (114-121)
  • TestOption (17-17)
  • BoolTFunc (11-11)
  • NewTestFunc (41-50)
utils.go (1)
  • Test (42-42)
internals/types.go (1)
  • Transform (12-12)
zogSchema.go (3)
  • Transform (34-34)
  • TestFunc (47-49)
  • BoolTFunc (44-44)
utilsOptions.go (1)
  • TestOption (10-10)
time.go (8)
internals/processors.go (2)
  • ZProcessor (4-6)
  • TransformProcessor (8-10)
internals/tests.go (5)
  • Test (64-75)
  • Required (114-121)
  • TestOption (17-17)
  • BoolTFunc (11-11)
  • NewTestFunc (41-50)
utils.go (2)
  • Test (42-42)
  • Ctx (12-12)
internals/types.go (1)
  • Transform (12-12)
zogSchema.go (3)
  • Transform (34-34)
  • TestFunc (47-49)
  • BoolTFunc (44-44)
utilsOptions.go (3)
  • TestOption (10-10)
  • IssueCode (32-36)
  • Params (57-61)
internals/contexts.go (1)
  • Ctx (8-42)
zconst/consts.go (1)
  • IssueCodeBefore (168-168)
zogSchema.go (9)
internals/types.go (2)
  • Transform (12-12)
  • ZogPrimitive (15-17)
internals/Issues.go (1)
  • IssueFmtFunc (10-10)
internals/tests.go (5)
  • TFunc (14-14)
  • BoolTFunc (11-11)
  • TestOption (17-17)
  • Test (64-75)
  • NewTestFunc (41-50)
utilsOptions.go (2)
  • IssueCode (32-36)
  • TestOption (10-10)
zconst/consts.go (1)
  • ZogIssueCode (54-54)
utils.go (2)
  • Test (42-42)
  • CoercerFunc (37-37)
internals/contexts.go (1)
  • SchemaCtx (132-142)
internals/processors.go (1)
  • ZProcessor (4-6)
internals/zeroValues.go (1)
  • IsZeroValueFunc (7-7)
slices.go (8)
internals/processors.go (2)
  • ZProcessor (4-6)
  • TransformProcessor (8-10)
zogSchema.go (4)
  • ZogSchema (10-15)
  • Transform (34-34)
  • TestFunc (47-49)
  • BoolTFunc (44-44)
internals/tests.go (6)
  • Test (64-75)
  • Required (114-121)
  • BoolTFunc (11-11)
  • TestOption (17-17)
  • NewTestFunc (41-50)
  • Len (153-164)
utils.go (2)
  • Test (42-42)
  • Ctx (12-12)
internals/types.go (1)
  • Transform (12-12)
utilsOptions.go (3)
  • TestOption (10-10)
  • IssueCode (32-36)
  • Params (57-61)
zconst/consts.go (2)
  • IssueCodeMin (89-89)
  • IssueCodeMax (93-93)
internals/contexts.go (1)
  • Ctx (8-42)
string.go (8)
internals/processors.go (2)
  • ZProcessor (4-6)
  • TransformProcessor (8-10)
internals/tests.go (5)
  • Test (64-75)
  • Required (114-121)
  • TestOption (17-17)
  • BoolTFunc (11-11)
  • NewTestFunc (41-50)
utils.go (2)
  • Test (42-42)
  • Ctx (12-12)
internals/types.go (1)
  • Transform (12-12)
zogSchema.go (3)
  • Transform (34-34)
  • TestFunc (47-49)
  • BoolTFunc (44-44)
internals/contexts.go (1)
  • Ctx (8-42)
utilsOptions.go (3)
  • TestOption (10-10)
  • IssueCode (32-36)
  • Params (57-61)
zconst/consts.go (10)
  • IssueCodeEmail (123-123)
  • IssueCodeURL (135-135)
  • IssueCodeHasPrefix (139-139)
  • IssueCodeHasSuffix (143-143)
  • IssueCodeContains (101-101)
  • IssueCodeContainsUpper (147-147)
  • IssueCodeContainsDigit (155-155)
  • IssueCodeContainsSpecial (159-159)
  • IssueCodeUUID (127-127)
  • IssueCodeMatch (131-131)
🔇 Additional comments (78)
utils.go (1)

42-42: Well implemented type safety improvement.

Excellent change replacing the non-generic Test alias with a generic Test[T any] type. This change aligns perfectly with the PR objective of making transforms and tests typesafe for the schema being used, eliminating the need for typecasting. It ensures that test functions will receive properly typed arguments at compile time rather than requiring runtime type assertions.

struct_helpers.go (1)

26-26: Good adoption of generics for type safety.

The change from a non-generic []p.ZProcessor to a generic []p.ZProcessor[any] correctly updates the type of the processors slice to match the generic interface definition. This is consistent with the overall refactoring to use generics throughout the codebase, providing better type safety while maintaining compatibility with existing code by using any as the type parameter.

zhttp/zhttp_test.go (1)

39-41: Improved type safety in test function.

Good refactoring of the test function to use a typed *string parameter instead of any, allowing direct value access via dereferencing (*val) rather than using type assertions. This change enhances code readability and compile-time type safety.

internals/types.go (1)

12-12: Excellent foundational change for type safety.

Converting the Transform type from a non-generic function to a generic function with type parameter T is a key architectural improvement. This change allows transform functions to be specialized for different schema types, enabling compile-time type checking rather than relying on runtime type assertions. This is a core enhancement that supports the PR's goal of making transforms typesafe.

reusable_tests_test.go (1)

10-17: Good refactoring to support type safety!

The function MyTest has been updated to use generics with a typed pointer parameter (*string), eliminating the need for type assertions. This change aligns perfectly with the PR objective of making transforms and tests typesafe for the schema being used.

pointers.go (2)

15-15: Good update to align with generic typing system!

The required field has been correctly updated to use the generic p.Test[any] type, which aligns with the broader refactoring to use generics across the codebase.


126-132: Implementation correctly updated for type safety

The NotNil method has been properly updated to instantiate a p.Test[any] struct instead of the previous non-generic type. This maintains the same functionality while integrating with the new type system.

struct_test.go (2)

174-176: Good refactoring for type safety!

The custom test function now accepts a typed pointer parameter (*int) instead of any, and properly dereferences it instead of using type assertions. This aligns with the PR objective of ensuring type safety and eliminating typecasting.


307-307: Method correctly updated to use the new Transform API

The deprecated PostTransform method has been correctly replaced with the new Transform method, which is part of the consistent API update across the codebase.

struct_helper_test.go (4)

56-60: Method correctly updated to use the new Transform API

The deprecated PostTransform method has been correctly replaced with the new Transform method, which is part of the consistent API update across the codebase.


63-67: Method correctly updated to use the new Transform API

The deprecated PostTransform method has been correctly replaced with the new Transform method, maintaining the same transformation functionality.


173-177: Method correctly updated to use the new Transform API

The deprecated PostTransform method has been correctly replaced with the new Transform method, which is part of the consistent API update across the codebase.


203-207: Method correctly updated to use the new Transform API

The deprecated PostTransform method has been correctly replaced with the new Transform method, maintaining the same transformation functionality.

numbers_test.go (3)

118-123: Type-safe transformations - good job!

The transformation function now uses a typed pointer parameter (*int) instead of any, which eliminates the need for type assertions and improves type safety. The method call has also been updated from the deprecated .PostTransform() to .Transform().


134-140: Type-safe transform chain implemented correctly

The transformation function signature has been updated to use typed pointers, and the method calls have been updated to use the new .Transform() method, maintaining the ability to chain multiple transformations with improved type safety.


258-262: Custom test function with proper type safety

Great job updating the test function to use a typed pointer parameter (*int) instead of any. The code now directly dereferences the pointer with *val for comparison, eliminating the need for type assertions and improving type safety.

struct_validate_test.go (2)

111-113: Type-safe custom test function

The custom test function now uses a strongly typed pointer parameter (*int) and directly dereferences it, eliminating the need for type assertions and improving type safety.


180-180: Updated to use Transform instead of PostTransform

The method call has been correctly updated from the deprecated .PostTransform() to .Transform(), aligning with the broader refactoring in the codebase to eliminate deprecated methods.

time_validate_test.go (1)

105-107: Type-safe custom test function

The test function now uses a typed pointer parameter (*time.Time) instead of any, with proper dereferencing when calling methods on the time object. This eliminates the need for type assertions and improves type safety.

time_test.go (2)

51-55: Type-safe transform implementation

The transform function now uses a typed pointer parameter (*time.Time) and correctly dereferences it to modify the time value. This eliminates the need for type assertions and improves type safety.


103-105: Type-safe custom test function

The test function now uses a typed pointer parameter (*time.Time) with proper dereferencing when calling methods on the time object. This eliminates type assertions and improves type safety.

string_test.go (2)

59-63: Great type safety improvement!

The transformation function now uses a strongly typed *string parameter instead of the previous any type with internal type assertions. This provides better type safety at compile time and eliminates potential runtime errors from incorrect type assertions.


83-85: Enhanced type safety for test functions

The test function now directly accepts a strongly typed *string parameter instead of any, and uses proper dereferencing for the string comparison. This is consistent with the PR's goal of making transforms and tests typesafe for the schema being used.

string_validate_test.go (2)

62-64: Strongly typed transform function improves safety

Good refactoring to use a typed *string parameter instead of any, which provides compile-time type checking and eliminates the need for manual type assertions within the function body.


84-86: Improved type safety in test function

The TestFunc callback now correctly uses a strongly typed *string parameter instead of a generic any type, which improves code clarity and prevents potential runtime type errors.

numbers_validate_test.go (4)

86-92: Strong typing for number transform functions

The transformation function now uses a strongly typed *int parameter instead of any, which removes the need for type assertions and improves type safety.


91-91: Deprecated method replaced with type-safe alternative

Good change from the deprecated .PostTransform method to the new .Transform method that accepts a strongly typed function parameter.


102-108: Consistent type-safe transform implementation

This transformation function follows the same pattern of using strongly typed *int parameters rather than any, providing consistent type safety throughout the codebase.


245-248: Type-safe test functions for numeric schemas

The test function now uses a strongly typed *int parameter rather than any, eliminating runtime type assertions and improving code clarity through direct dereferencing.

numbers_custom_test.go (3)

130-136: Type safety for custom numeric types

The transformation function now correctly uses *MyInt instead of any, providing type safety for custom numeric types as well. The call to .Transform() properly replaces the deprecated .PostTransform() method.


146-152: Consistent type safety in multiple transformations

Good implementation of multiple transform functions with strongly typed *MyInt parameters. This ensures type safety is maintained throughout transformation chains.


269-273: Type-safe custom test functions

The test function now uses strongly typed *MyInt parameters instead of any, which ensures type safety when working with custom numeric types and eliminates the need for type assertions inside the function body.

internals/contexts.go (2)

141-141: Type change aligns with generic architecture

The Processor field type has been changed from a specific interface to any to accommodate the new generic processor types. This allows the schema context to work with different generic processor implementations.


166-180: Good abstraction through interface usage

The method has been updated to work with the new TestInterface type instead of a concrete *Test pointer, which is consistent with the broader refactoring to use interfaces and generics. The code properly uses accessor methods instead of direct field access, improving encapsulation.

internals/processors.go (3)

4-6: Excellent use of generics for type safety

Converting the ZProcessor interface to use a generic type parameter T eliminates the need for type assertions when processing values. This is a key improvement that supports the PR's goal of making the codebase more type-safe.


8-10: Type-safe transform processor implementation

Making TransformProcessor generic with type parameter T ensures type consistency between the processor and its transform function, reducing the risk of runtime type errors.


12-19: Type-safe process method implementation

The ZProcess method now accepts a typed parameter T instead of any, which eliminates the need for type assertions within the method. The error handling logic remains unchanged, maintaining the same behavior while improving type safety.

custom.go (3)

12-12: Generic test field improves type safety

Using the generic pointer type p.Test[*T] for the test field ensures that test functions receive correctly typed pointers, eliminating the need for type assertions in test implementations.


16-19: Type-safe custom function implementation

The function now creates and initializes the test with properly typed functions that directly accept *T pointers instead of untyped any values, which eliminates the need for runtime type assertions within the test functions.


76-76: Explicit type casting maintains safety

The validate method now explicitly casts ctx.ValPtr to *T before passing it to the test function, ensuring type consistency. This targeted type assertion is safer than implicit assertions inside the test functions.

string_custom_test.go (3)

65-68: Type-safe transform implementation

The Transform method now accepts a typed pointer *Env rather than an untyped any, which aligns with the PR's goal of making transforms type-safe. This eliminates the need for type assertions inside the transformation function.


97-97: Added explicit error code verification

The additional assertion to verify that the error code matches the expected constant zconst.IssueCodeContains improves test coverage by ensuring that the correct error type is being raised.


234-234: Consistent parameter type in error maps

The error parameter type for prefix validation now uses a raw string "test_" instead of wrapping it as Env("test_"). This improves consistency with how parameters are handled in the test interface implementation.

numbers_int64_test.go (5)

107-109: Type-safe transform function using pointer parameter

The function signature has been updated to accept a strongly typed pointer (*int64) instead of an interface type (any). This improves type safety by ensuring the transform function operates directly on the typed value without requiring type assertions or conversions.


112-112: Replaced deprecated PostTransform with Transform method

The code correctly uses the new Transform method instead of the deprecated PostTransform method, aligning with the API changes in this PR.


123-126: Type-safe transform function using pointer parameter

Another instance of updating the function signature to accept a strongly typed pointer (*int64) instead of an interface type, improving type safety by eliminating the need for type assertions.


128-128: Replaced deprecated PostTransform with Transform method

Consistent with previous change, using the updated Transform method instead of the deprecated PostTransform method.


246-249: Type-safe test function using pointer parameter

The test function has been updated to use a strongly typed pointer (*int64) instead of an interface type, improving type safety. The assertion is also updated to dereference the pointer correctly.

boolean_validate_test.go (6)

203-205: Updated test function to use typed pointer parameter

The test function now accepts a strongly typed *bool pointer instead of an untyped interface, improving type safety. The function body correctly dereferences the pointer using *val to access the boolean value.


287-287: Updated transform type to use generic pointer type

The transform type declaration now uses the generic p.Transform[*bool] type with explicit pointer type parameter, ensuring type-safe transformations for boolean values.


294-296: Type-safe transform function using pointer parameter

The transform function signature now uses a strongly typed *bool pointer parameter and correctly modifies the value by dereferencing the pointer, improving type safety and eliminating the need for type assertions.


303-305: Type-safe transform function using pointer parameter

Consistent with previous changes, this transform function now uses a strongly typed *bool pointer parameter.


311-313: Type-safe transform function using pointer parameter

Another instance of updating a transform function to use strongly typed pointer parameters, maintaining consistency throughout the codebase.


339-342: Type-safe test function using pointer parameter

The custom test function now accepts a strongly typed *bool pointer and correctly uses it through dereferencing (assert.Equal(t, true, *val)), eliminating the need for type assertions.

boolean_test.go (5)

387-387: Updated transform type to use generic pointer type

The transform type has been updated to use the generic p.Transform[*bool] with an explicit pointer type parameter, ensuring type-safe transformations for boolean values.


394-396: Type-safe transform function using pointer parameter

The transform function now accepts a strongly typed *bool pointer and correctly modifies the value by dereferencing the pointer with *val = !*val, improving type safety by eliminating type assertions.


403-405: Type-safe transform function using pointer parameter

Consistent with other changes, this transform function now uses a strongly typed *bool pointer parameter.


411-413: Type-safe transform function using pointer parameter

Another example of consistent updating to use strongly typed pointer parameters throughout the transform functions.


441-444: Type-safe test function using pointer parameter

The custom test function now accepts a strongly typed *bool pointer and correctly accesses the value through dereferencing (*val == true), improving type safety by eliminating type assertions.

utilsOptions.go (5)

14-18: Improved encapsulation using TestInterface

The implementation now uses the p.TestInterface abstraction instead of directly manipulating concrete test struct fields. This improves encapsulation by using the interface method SetIssueFmtFunc to set the message formatter.


22-25: Improved encapsulation using TestInterface

Updated the MessageFunc option to use the p.TestInterface abstraction, promoting better encapsulation by using interface methods instead of direct field access.


33-35: Improved encapsulation using TestInterface

The IssueCode option now uses the p.TestInterface abstraction with the SetIssueCode method, maintaining consistency with the interface-based approach.


50-52: Improved encapsulation using TestInterface

The IssuePath option now uses the p.TestInterface abstraction with the SetIssuePath method, following the pattern of better encapsulation.


58-60: Improved encapsulation using TestInterface

The Params option now uses the p.TestInterface abstraction with the SetParams method, completing the consistent pattern of using interface methods instead of direct field access.

numbers.go (2)

15-18: Processors & required fields now use pointer-generic types – looks good

Switching to []p.ZProcessor[*T] and *p.Test[*T] is consistent with the new pointer-generic APIs introduced in internals/. ✅


211-214: Empty IssueCode string – verify this is intentional

NewTestFunc expects a zconst.ZogIssueCode; passing "" compiles only because the underlying type is string.
If the formatter relies on a non-empty code, this may surface as confusing messages.

Please double-check whether a dedicated code constant (e.g. zconst.IssueCodeCustom) should be used instead.

boolean.go (1)

12-17: Pointer-generic fields mirror NumberSchema – good consistency

No functional concerns here. 👍

struct.go (1)

16-19: Generic processor/test fields cleanly reflect the new internals

No issues spotted – the change removes several old type assertions.

internals/tests.go (2)

11-15: Generics migration looks solid

The switch from interface{} to [T any] for both BoolTFunc and TFunc immediately removes the need for unsafe type-casting and gives callers compile-time guarantees. Nicely done.


17-18: Good abstraction for options

Accepting a TestInterface instead of *Test keeps the option helpers decoupled from the concrete implementation and prevents accidental reliance on private fields.

string.go (8)

45-48: Processor slice generified successfully

Storing []p.ZProcessor[*T] and a typed required pointer removes every run-time cast previously scattered through the string schema.
Looks clean. 👍


139-142: Transform helper LGTM

Accepting p.Transform[*T] keeps the public API aligned with the internal processor model.


148-153: Reuse of Required helper is neat

The pass-through of TestOptions to the generated test preserves customisation. No issues spotted.


234-240: Prefix test LGTM


244-253: Suffix test LGTM


257-266: Contains test LGTM


285-296: Digit check LGTM – ASCII digits are usually sufficient for validation.


328-335: Match test LGTM

Comment on lines +182 to 185
func (v *SliceSchema) Transform(transform Transform[any]) *SliceSchema {
v.processors = append(v.processors, &p.TransformProcessor[any]{
Transform: p.Transform[any](transform),
})
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make Transform generic over the concrete slice type

Using Transform[any] forces every transform to perform type assertions. You
already know the element type from v.schema; you can expose that via a type
parameter on SliceSchema, e.g.:

type SliceSchema[T any] struct {
    processors []p.ZProcessor[*[]T]
    // ...
}

This propagates type-safety to callers and removes a whole class of
runtime panics. Refactor is optional but strongly recommended if API
stability allows.

Comment on lines +184 to 221
func EQ[T comparable](n T) (Test[*T], BoolTFunc[*T]) {
fn := func(val *T, ctx Ctx) bool {
return *val == n
}

t := Test{
t := Test[*T]{
IssueCode: zconst.IssueCodeEQ,
Params: make(map[string]any, 1),
}
t.Params[zconst.IssueCodeEQ] = n
return t, fn
}

func LTE[T constraints.Ordered](n T) (Test, BoolTFunc) {
fn := func(val any, ctx Ctx) bool {
v, ok := val.(*T)
if !ok {
return false
}
return *v <= n
func LTE[T constraints.Ordered](n T) (Test[*T], BoolTFunc[*T]) {
fn := func(val *T, ctx Ctx) bool {
return *val <= n
}

t := Test{
t := Test[*T]{
IssueCode: zconst.IssueCodeLTE,
Params: make(map[string]any, 1),
}
t.Params[zconst.IssueCodeLTE] = n
return t, fn
}

func GTE[T constraints.Ordered](n T) (Test, BoolTFunc) {
fn := func(val any, ctx Ctx) bool {
v, ok := val.(*T)
if !ok {
return false
}
return *v >= n
func GTE[T constraints.Ordered](n T) (Test[*T], BoolTFunc[*T]) {
fn := func(val *T, ctx Ctx) bool {
return *val >= n
}

t := Test{
t := Test[*T]{
IssueCode: zconst.IssueCodeGTE,
Params: make(map[string]any, 1),
}
t.Params[zconst.IssueCodeGTE] = n
return t, fn
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Same nil-pointer risk in comparison helpers

EQ/LT/LTE/GTE/GT functions also dereference a possibly nil pointer.

Fix pattern identical to the earlier length helpers.

Comment on lines +19 to 38
func TestFuncFromBool[T any](fn BoolTFunc[T], test *Test[T]) {
test.Func = func(val T, ctx Ctx) {
if fn(val, ctx) {
return
}

c := ctx.(*SchemaCtx)
ctx.AddIssue(c.IssueFromTest(c.Processor.(*Test), val))
ctx.AddIssue(c.IssueFromTest(c.Processor.(TestInterface), val))
}
}

func TestNotFuncFromBool(fn BoolTFunc, test *Test) {
test.Func = func(val any, ctx Ctx) {
func TestNotFuncFromBool[T any](fn BoolTFunc[T], test *Test[T]) {
test.Func = func(val T, ctx Ctx) {
if !fn(val, ctx) {
return
}

c := ctx.(*SchemaCtx)
ctx.AddIssue(c.IssueFromTest(c.Processor.(*Test), val))
ctx.AddIssue(c.IssueFromTest(c.Processor.(TestInterface), val))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Panic risk when Ctx is not a *SchemaCtx

ctx.(*SchemaCtx) will panic if a user passes a custom context that merely implements the public Ctx interface.
Consider a type-switch or an early guard to avoid a hard crash.

- c := ctx.(*SchemaCtx)
+ sc, ok := ctx.(*SchemaCtx)
+ if !ok {
+     // fall back or return; do not panic
+     return
+ }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +128 to 133
v.processors = append(v.processors, &p.TransformProcessor[*T]{
Transform: func(val *T, ctx Ctx) error {
*val = T(strings.TrimSpace(string(*val)))
return nil
},
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Nil-pointer safety in Trim() transform

Trim() assumes the value pointer is never nil. That is true during parsing but not necessarily during standalone Validate.

-Transform: func(val *T, ctx Ctx) error {
-    *val = T(strings.TrimSpace(string(*val)))
+Transform: func(val *T, ctx Ctx) error {
+    if val == nil {
+        return nil // nothing to trim
+    }
+    *val = T(strings.TrimSpace(string(*val)))
     return nil
 },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
v.processors = append(v.processors, &p.TransformProcessor[*T]{
Transform: func(val *T, ctx Ctx) error {
*val = T(strings.TrimSpace(string(*val)))
return nil
},
})
v.processors = append(v.processors, &p.TransformProcessor[*T]{
Transform: func(val *T, ctx Ctx) error {
if val == nil {
return nil // nothing to trim
}
*val = T(strings.TrimSpace(string(*val)))
return nil
},
})

@Oudwins Oudwins merged commit 2da0c0c into master Apr 27, 2025
9 checks passed
@Oudwins Oudwins deleted the feat/typesafe-transforms branch April 27, 2025 15:58
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