Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Contributing

Thank you for your interest!

## Prerequisite

Install the following programs

- `go` - Of course =) [installation instructions](https://go.dev/doc/install)
- `just` - [Just a command runner](https://github.com/casey/just)
- `python3` - latest stable version (tested with 3.13). No dependencies are needed. This is for code generation.
- `golangci-lint` - [linter and formatter for go](https://golangci-lint.run/welcome/install/)

To contribute follow the following steps:

1. Fork this repository.
2. Make your changes.
3. Run `just` command (without any arguments). Fix errors it emits, if any.
4. Push your changes to your fork.
5. Make PR.
6. You rock!

## Adding new validators

> If you have any questions after reading this section feel free to open an issue - I will be happy to answer.

Validators meta information are stored in [validators.toml](./validators.toml).

This is done so that comment strings and documentation are generated from
a single source of truth to avoid typos and manual work.

After changing this file run:

```sh
just generate
```

After that change [validate/impl.go](./validate/impl.go) file to add `Validate` method for your new validator.

Again, if you have any questions - feel free to open an issue.
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,10 @@ See [parse example](./examples/parse/main.go) for more information.

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

## Validators

For a list of available validators see [validators](./validators.md)

## Performance

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

## Validators

Not listed here yet, but can see a full list
of available validators in [validate/validators.go](./validate/validators.go)

## TODO

- [x] Support for manual construction (similar to `.parse(...)` in zod) (using codegen)
Expand Down
6 changes: 3 additions & 3 deletions examples/codegen/User_schema.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ fmt:
# lint source code
lint:
golangci-lint run --tests=false

# Open documentation
doc:
go run golang.org/x/pkgsite/cmd/pkgsite@latest -open
178 changes: 178 additions & 0 deletions optional/custom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Package optional provides types whose values may be either empty (null) or be present and pass validation.
//
// Optional types support the following encoding/decoding formats:
// - json
// - sql
// - text
// - binary
// - gob
package optional

import (
"reflect"

"github.com/metafates/schema/parse"
"github.com/metafates/schema/validate"
)

// Custom optional type.
// When given non-null value it errors if validation fails.
type Custom[T any, V validate.Validator[T]] struct {
value T
hasValue bool
validated bool
}

// TypeValidate implements the [validate.TypeValidateable] interface.
// You should not call this function directly.
func (c *Custom[T, V]) TypeValidate() error {
if !c.hasValue {
return nil
}

if err := (*new(V)).Validate(c.value); err != nil {
return validate.ValidationError{Inner: err}
}

// validate nested types recursively
if err := validate.Validate(&c.value); err != nil {
return err
}

c.validated = true

return nil
}

// HasValue returns the presence of the contained value.
func (c Custom[T, V]) HasValue() bool { return c.hasValue }

// Get returns the contained value and a boolean stating its presence.
// True if value exists, false otherwise.
//
// Panics if value was not validated yet.
// See also [Custom.GetPtr].
func (c Custom[T, V]) Get() (T, bool) {
if c.hasValue && !c.validated {
panic("called Get() on non-empty unvalidated value")
}

return c.value, c.hasValue
}

// Get returns the pointer to the contained value.
// Non-nil if value exists, nil otherwise.
// Pointed value is a shallow copy.
//
// Panics if value was not validated yet.
// See also [Custom.Get].
func (c Custom[T, V]) GetPtr() *T {
if c.hasValue && !c.validated {
panic("called GetPtr() on non-empty unvalidated value")
}

var value *T

if c.hasValue {
valueCopy := c.value
value = &valueCopy
}

return value
}

// Must returns the contained value and panics if it does not have one.
// You can check for its presence using [Custom.HasValue] or use a more safe alternative [Custom.Get].
func (c Custom[T, V]) Must() T {
if !c.hasValue {
panic("called must on empty optional")
}

value, _ := c.Get()

return value
}

// Parse checks if given value is valid.
// If it is, a value is used to initialize this type.
// Value is converted to the target type T, if possible. If not - [parse.UnconvertableTypeError] is returned.
// It is allowed to pass convertable type wrapped in optional type.
//
// Parsed type is validated, therefore it is safe to call [Custom.Get] afterwards.
//
// Passing nil results a valid empty instance.
func (c *Custom[T, V]) Parse(value any) error {
if value == nil {
*c = Custom[T, V]{}

return nil
}

rValue := reflect.ValueOf(value)

if rValue.Kind() == reflect.Pointer && rValue.IsNil() {
*c = Custom[T, V]{}

return nil
}

if _, ok := value.(interface{ isOptional() }); ok {
// NOTE: ensure this method name is in sync with [Custom.Get]
res := rValue.MethodByName("Get").Call(nil)
v, ok := res[0], res[1].Bool()

if !ok {
*c = Custom[T, V]{}

return nil
}

rValue = v
}

v, err := convert[T](rValue)
if err != nil {
return parse.ParseError{Inner: err}
}

aux := Custom[T, V]{
hasValue: true,
value: v,
}

if err := aux.TypeValidate(); err != nil {
return err
}

*c = aux

return nil
}

func (c *Custom[T, V]) MustParse(value any) {
if err := c.Parse(value); err != nil {
panic("MustParse failed")
}
}

func (Custom[T, V]) isOptional() {}

func convert[T any](v reflect.Value) (T, error) {
tType := reflect.TypeFor[T]()

original := v

if v.Kind() == reflect.Pointer {
v = v.Elem()
}

if v.CanConvert(tType) {
//nolint:forcetypeassert // checked already by CanConvert
return v.Convert(tType).Interface().(T), nil
}

return *new(T), parse.UnconvertableTypeError{
Target: tType.String(),
Original: original.Type().String(),
}
}
Loading