Skip to content

Commit ff67d59

Browse files
authored
Generate validators (#1)
Generate validators from a single file
1 parent ee0cf53 commit ff67d59

File tree

15 files changed

+1981
-1125
lines changed

15 files changed

+1981
-1125
lines changed

CONTRIBUTING.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Contributing
2+
3+
Thank you for your interest!
4+
5+
## Prerequisite
6+
7+
Install the following programs
8+
9+
- `go` - Of course =) [installation instructions](https://go.dev/doc/install)
10+
- `just` - [Just a command runner](https://github.com/casey/just)
11+
- `python3` - latest stable version (tested with 3.13). No dependencies are needed. This is for code generation.
12+
- `golangci-lint` - [linter and formatter for go](https://golangci-lint.run/welcome/install/)
13+
14+
To contribute follow the following steps:
15+
16+
1. Fork this repository.
17+
2. Make your changes.
18+
3. Run `just` command (without any arguments). Fix errors it emits, if any.
19+
4. Push your changes to your fork.
20+
5. Make PR.
21+
6. You rock!
22+
23+
## Adding new validators
24+
25+
> If you have any questions after reading this section feel free to open an issue - I will be happy to answer.
26+
27+
Validators meta information are stored in [validators.toml](./validators.toml).
28+
29+
This is done so that comment strings and documentation are generated from
30+
a single source of truth to avoid typos and manual work.
31+
32+
After changing this file run:
33+
34+
```sh
35+
just generate
36+
```
37+
38+
After that change [validate/impl.go](./validate/impl.go) file to add `Validate` method for your new validator.
39+
40+
Again, if you have any questions - feel free to open an issue.

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,10 @@ See [parse example](./examples/parse/main.go) for more information.
346346

347347
Parsing gRPC messages is also supported, see [grpc parse example](./examples/parse-grpc/main.go)
348348

349+
## Validators
350+
351+
For a list of available validators see [validators](./validators.md)
352+
349353
## Performance
350354

351355
**TL;DR:** you can use codegen for max performance (0-1% overhead) or fallback to reflection (35% overhead).
@@ -379,11 +383,6 @@ BenchmarkUnmarshalJSON/codegen/with_validation-12 45936 ns/op
379383
BenchmarkUnmarshalJSON/codegen/without_validation-12 45649 ns/op
380384
```
381385

382-
## Validators
383-
384-
Not listed here yet, but can see a full list
385-
of available validators in [validate/validators.go](./validate/validators.go)
386-
387386
## TODO
388387

389388
- [x] Support for manual construction (similar to `.parse(...)` in zod) (using codegen)

examples/codegen/User_schema.go

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,7 @@ fmt:
4949
# lint source code
5050
lint:
5151
golangci-lint run --tests=false
52+
53+
# Open documentation
54+
doc:
55+
go run golang.org/x/pkgsite/cmd/pkgsite@latest -open

optional/custom.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Package optional provides types whose values may be either empty (null) or be present and pass validation.
2+
//
3+
// Optional types support the following encoding/decoding formats:
4+
// - json
5+
// - sql
6+
// - text
7+
// - binary
8+
// - gob
9+
package optional
10+
11+
import (
12+
"reflect"
13+
14+
"github.com/metafates/schema/parse"
15+
"github.com/metafates/schema/validate"
16+
)
17+
18+
// Custom optional type.
19+
// When given non-null value it errors if validation fails.
20+
type Custom[T any, V validate.Validator[T]] struct {
21+
value T
22+
hasValue bool
23+
validated bool
24+
}
25+
26+
// TypeValidate implements the [validate.TypeValidateable] interface.
27+
// You should not call this function directly.
28+
func (c *Custom[T, V]) TypeValidate() error {
29+
if !c.hasValue {
30+
return nil
31+
}
32+
33+
if err := (*new(V)).Validate(c.value); err != nil {
34+
return validate.ValidationError{Inner: err}
35+
}
36+
37+
// validate nested types recursively
38+
if err := validate.Validate(&c.value); err != nil {
39+
return err
40+
}
41+
42+
c.validated = true
43+
44+
return nil
45+
}
46+
47+
// HasValue returns the presence of the contained value.
48+
func (c Custom[T, V]) HasValue() bool { return c.hasValue }
49+
50+
// Get returns the contained value and a boolean stating its presence.
51+
// True if value exists, false otherwise.
52+
//
53+
// Panics if value was not validated yet.
54+
// See also [Custom.GetPtr].
55+
func (c Custom[T, V]) Get() (T, bool) {
56+
if c.hasValue && !c.validated {
57+
panic("called Get() on non-empty unvalidated value")
58+
}
59+
60+
return c.value, c.hasValue
61+
}
62+
63+
// Get returns the pointer to the contained value.
64+
// Non-nil if value exists, nil otherwise.
65+
// Pointed value is a shallow copy.
66+
//
67+
// Panics if value was not validated yet.
68+
// See also [Custom.Get].
69+
func (c Custom[T, V]) GetPtr() *T {
70+
if c.hasValue && !c.validated {
71+
panic("called GetPtr() on non-empty unvalidated value")
72+
}
73+
74+
var value *T
75+
76+
if c.hasValue {
77+
valueCopy := c.value
78+
value = &valueCopy
79+
}
80+
81+
return value
82+
}
83+
84+
// Must returns the contained value and panics if it does not have one.
85+
// You can check for its presence using [Custom.HasValue] or use a more safe alternative [Custom.Get].
86+
func (c Custom[T, V]) Must() T {
87+
if !c.hasValue {
88+
panic("called must on empty optional")
89+
}
90+
91+
value, _ := c.Get()
92+
93+
return value
94+
}
95+
96+
// Parse checks if given value is valid.
97+
// If it is, a value is used to initialize this type.
98+
// Value is converted to the target type T, if possible. If not - [parse.UnconvertableTypeError] is returned.
99+
// It is allowed to pass convertable type wrapped in optional type.
100+
//
101+
// Parsed type is validated, therefore it is safe to call [Custom.Get] afterwards.
102+
//
103+
// Passing nil results a valid empty instance.
104+
func (c *Custom[T, V]) Parse(value any) error {
105+
if value == nil {
106+
*c = Custom[T, V]{}
107+
108+
return nil
109+
}
110+
111+
rValue := reflect.ValueOf(value)
112+
113+
if rValue.Kind() == reflect.Pointer && rValue.IsNil() {
114+
*c = Custom[T, V]{}
115+
116+
return nil
117+
}
118+
119+
if _, ok := value.(interface{ isOptional() }); ok {
120+
// NOTE: ensure this method name is in sync with [Custom.Get]
121+
res := rValue.MethodByName("Get").Call(nil)
122+
v, ok := res[0], res[1].Bool()
123+
124+
if !ok {
125+
*c = Custom[T, V]{}
126+
127+
return nil
128+
}
129+
130+
rValue = v
131+
}
132+
133+
v, err := convert[T](rValue)
134+
if err != nil {
135+
return parse.ParseError{Inner: err}
136+
}
137+
138+
aux := Custom[T, V]{
139+
hasValue: true,
140+
value: v,
141+
}
142+
143+
if err := aux.TypeValidate(); err != nil {
144+
return err
145+
}
146+
147+
*c = aux
148+
149+
return nil
150+
}
151+
152+
func (c *Custom[T, V]) MustParse(value any) {
153+
if err := c.Parse(value); err != nil {
154+
panic("MustParse failed")
155+
}
156+
}
157+
158+
func (Custom[T, V]) isOptional() {}
159+
160+
func convert[T any](v reflect.Value) (T, error) {
161+
tType := reflect.TypeFor[T]()
162+
163+
original := v
164+
165+
if v.Kind() == reflect.Pointer {
166+
v = v.Elem()
167+
}
168+
169+
if v.CanConvert(tType) {
170+
//nolint:forcetypeassert // checked already by CanConvert
171+
return v.Convert(tType).Interface().(T), nil
172+
}
173+
174+
return *new(T), parse.UnconvertableTypeError{
175+
Target: tType.String(),
176+
Original: original.Type().String(),
177+
}
178+
}

0 commit comments

Comments
 (0)