diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b929608 --- /dev/null +++ b/CONTRIBUTING.md @@ -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. diff --git a/README.md b/README.md index 5cc27b7..0bdbb73 100644 --- a/README.md +++ b/README.md @@ -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). @@ -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) diff --git a/examples/codegen/User_schema.go b/examples/codegen/User_schema.go index b1edfbd..4e43b1c 100644 --- a/examples/codegen/User_schema.go +++ b/examples/codegen/User_schema.go @@ -23,9 +23,9 @@ func _() { } `json:"meta"` Friends []UserFriend `json:"friends"` Addresses []struct { - Tag optional.Custom[string, validate.Charset0[string, charset.Print]] `json:"tag"` - Latitude required.Custom[float64, validate.Latitude[float64]] `json:"latitude"` - Longitude required.Custom[float64, validate.Longitude[float64]] `json:"longitude"` + Tag optional.Custom[string, validate.Charset[string, charset.Print]] `json:"tag"` + Latitude required.Custom[float64, validate.Latitude[float64]] `json:"latitude"` + Longitude required.Custom[float64, validate.Longitude[float64]] `json:"longitude"` } `json:"addresses"` } var v User diff --git a/justfile b/justfile index 43e7aaa..ea4611d 100644 --- a/justfile +++ b/justfile @@ -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 diff --git a/optional/custom.go b/optional/custom.go new file mode 100644 index 0000000..6cb6733 --- /dev/null +++ b/optional/custom.go @@ -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(), + } +} diff --git a/optional/optional.go b/optional/optional.go index 70d19e7..4a8f6ba 100644 --- a/optional/optional.go +++ b/optional/optional.go @@ -1,338 +1,177 @@ -// 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 +// Code generated by validators.py; DO NOT EDIT. package optional import ( - "reflect" - "github.com/metafates/schema/constraint" - "github.com/metafates/schema/parse" "github.com/metafates/schema/validate" "github.com/metafates/schema/validate/charset" ) -type ( - // Custom optional type. - // When given non-null value it errors if validation fails. - Custom[T any, V validate.Validator[T]] struct { - value T - hasValue bool - validated bool - } - - // Any accepts any value of T. - Any[T any] = Custom[T, validate.Any[T]] - - // NonZero accepts all non-zero values. - // - // The zero value is: - // - 0 for numeric types, - // - false for the boolean type, and - // - "" (the empty string) for strings. - NonZero[T comparable] = Custom[T, validate.NonZero[T]] - - // Positive accepts all positive real numbers and zero. - // - // See also [Negative]. - Positive[T constraint.Real] = Custom[T, validate.Positive[T]] - - // Negative accepts all negative real numbers and zero. - // - // See also [Positive]. - Negative[T constraint.Real] = Custom[T, validate.Negative[T]] - - // Positive0 accepts all positive real numbers including zero. - // - // See [Positive] for zero excluding variant. - Positive0[T constraint.Real] = Custom[T, validate.Positive0[T]] - - // Negative0 accepts all negative real numbers including zero. - // - // See [Negative] for zero excluding variant. - Negative0[T constraint.Real] = Custom[T, validate.Negative0[T]] - - // Even accepts real numbers divisible by two. - Even[T constraint.Integer] = Custom[T, validate.Even[T]] - - // Odd accepts real numbers not divisible by two. - Odd[T constraint.Integer] = Custom[T, validate.Odd[T]] - - // Email accepts a single RFC 5322 address, e.g. "Barry Gibbs ". - Email[T constraint.Text] = Custom[T, validate.Email[T]] - - // URL accepts a single url. - // The url may be relative (a path, without a host) or absolute (starting with a scheme). - // - // See also [HTTPURL]. - URL[T constraint.Text] = Custom[T, validate.URL[T]] - - // HTTPURL accepts a single http(s) url. - // - // See also [URL]. - HTTPURL[T constraint.Text] = Custom[T, validate.HTTPURL[T]] - - // IP accepts an IP address. - // The address can be in dotted decimal ("192.0.2.1"), - // IPv6 ("2001:db8::68"), or IPv6 with a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). - IP[T constraint.Text] = Custom[T, validate.IP[T]] - - // IPV4 accepts an IP V4 address (e.g. "192.0.2.1"). - IPV4[T constraint.Text] = Custom[T, validate.IPV4[T]] - - // IPV6 accepts an IP V6 address, including IPv4-mapped IPv6 addresses. - // The address can be regular IPv6 ("2001:db8::68"), or IPv6 with - // a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). - IPV6[T constraint.Text] = Custom[T, validate.IPV6[T]] - - // MAC accepts an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet IP over InfiniBand link-layer address. - MAC[T constraint.Text] = Custom[T, validate.MAC[T]] - - // CIDR accepts CIDR notation IP address and prefix length, - // like "192.0.2.0/24" or "2001:db8::/32", as defined in RFC 4632 and RFC 4291. - CIDR[T constraint.Text] = Custom[T, validate.CIDR[T]] - - // Base64 accepts valid base64 encoded strings. - Base64[T constraint.Text] = Custom[T, validate.Base64[T]] - - // Charset accepts text which contains only runes acceptable by filter. - // - // NOTE: empty strings will also pass. Use [NonZeroCharset] if you need non-empty strings. - Charset[T constraint.Text, F charset.Filter] = Custom[T, validate.Charset0[T, F]] - - // NonZeroCharset combines [NonZero] and [Charset]. - NonZeroCharset[T constraint.Text, F charset.Filter] = Custom[T, validate.Charset[T, F]] - - // Latitude accepts any number in the range [-90; 90]. - // - // See also [Longitude]. - Latitude[T constraint.Real] = Custom[T, validate.Latitude[T]] - - // Longitude accepts any number in the range [-180; 180]. - // - // See also [Latitude]. - Longitude[T constraint.Real] = Custom[T, validate.Longitude[T]] - - // InPast accepts any time before current timestamp. - // - // See also [InFuture]. - InPast[T constraint.Time] = Custom[T, validate.InPast[T]] - - // InFuture accepts any time after current timestamp. - // - // See also [InPast]. - InFuture[T constraint.Time] = Custom[T, validate.InFuture[T]] - - // Unique accepts a slice-like of unique values. - // - // See [UniqueSlice] for a slice shortcut. - Unique[S ~[]T, T comparable] = Custom[S, validate.Unique[S, T]] - - // Unique accepts a slice of unique values. - // - // See [Unique] for a more generic version. - UniqueSlice[T comparable] = Custom[[]T, validate.UniqueSlice[T]] - - // NonEmpty accepts a non-empty slice-like (len > 0). - // - // See [NonEmptySlice] for a slice shortcut. - NonEmpty[S ~[]T, T any] = Custom[S, validate.NonEmpty[S, T]] - - // NonEmpty accepts a non-empty slice (len > 0). - // - // See [NonEmpty] for a more generic version. - NonEmptySlice[T any] = Custom[[]T, validate.NonEmptySlice[T]] - - // MIME accepts RFC 1521 mime type string. - MIME[T constraint.Text] = Custom[T, validate.MIME[T]] +// Any accepts any value of T. +type Any[T any] = Custom[T, validate.Any[T]] - // UUID accepts a properly formatted UUID in one of the following formats: - // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} - UUID[T constraint.Text] = Custom[T, validate.UUID[T]] - - // JSON accepts valid json encoded text. - JSON[T constraint.Text] = Custom[T, validate.JSON[T]] - - // CountryAlpha2 accepts case-insensitive ISO 3166 2-letter country code. - CountryAlpha2[T constraint.Text] = Custom[T, validate.CountryAlpha2[T]] - - // CountryAlpha3 accepts case-insensitive ISO 3166 3-letter country code. - CountryAlpha3[T constraint.Text] = Custom[T, validate.CountryAlpha3[T]] - - // CountryAlpha accepts either [CountryAlpha2] or [CountryAlpha3]. - CountryAlpha[T constraint.Text] = Custom[T, validate.CountryAlpha[T]] - - // CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code. - CurrencyAlpha[T constraint.Text] = Custom[T, validate.CurrencyAlpha[T]] - - // LangAlpha2 accepts case-insesitive ISO 639 2-letter language code. - LangAlpha2[T constraint.Text] = Custom[T, validate.LangAlpha2[T]] +// Zero accepts all zero values. +// +// The zero value is: +// - 0 for numeric types, +// - false for the boolean type, and +// - "" (the empty string) for strings. +// +// See [NonZero]. +type Zero[T comparable] = Custom[T, validate.Zero[T]] - // LangAlpha2 accepts case-insesitive ISO 639 3-letter language code. - LangAlpha3[T constraint.Text] = Custom[T, validate.LangAlpha3[T]] +// NonZero accepts all non-zero values. +// +// The zero value is: +// - 0 for numeric types, +// - false for the boolean type, and +// - "" (the empty string) for strings. +// +// See [Zero]. +type NonZero[T comparable] = Custom[T, validate.NonZero[T]] - // LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. - LangAlpha[T constraint.Text] = Custom[T, validate.LangAlpha[T]] -) +// Positive accepts all positive real numbers excluding zero. +// +// See [Positive0] for zero including variant. +type Positive[T constraint.Real] = Custom[T, validate.Positive[T]] -// 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 - } +// Negative accepts all negative real numbers excluding zero. +// +// See [Negative0] for zero including variant. +type Negative[T constraint.Real] = Custom[T, validate.Negative[T]] - if err := (*new(V)).Validate(c.value); err != nil { - return validate.ValidationError{Inner: err} - } +// Positive0 accepts all positive real numbers including zero. +// +// See [Positive] for zero excluding variant. +type Positive0[T constraint.Real] = Custom[T, validate.Positive0[T]] - // validate nested types recursively - if err := validate.Validate(&c.value); err != nil { - return err - } +// Negative0 accepts all negative real numbers including zero. +// +// See [Negative] for zero excluding variant. +type Negative0[T constraint.Real] = Custom[T, validate.Negative0[T]] - c.validated = true +// Even accepts integers divisible by two. +type Even[T constraint.Integer] = Custom[T, validate.Even[T]] - return nil -} +// Odd accepts integers not divisible by two. +type Odd[T constraint.Integer] = Custom[T, validate.Odd[T]] -// HasValue returns the presence of the contained value. -func (c Custom[T, V]) HasValue() bool { return c.hasValue } +// Email accepts a single RFC 5322 address, e.g. "Barry Gibbs ". +type Email[T constraint.Text] = Custom[T, validate.Email[T]] -// 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. +// URL accepts a single url. +// The url may be relative (a path, without a host) or absolute (starting with a scheme). // -// 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. +// See also [HTTPURL]. +type URL[T constraint.Text] = Custom[T, validate.URL[T]] + +// HTTPURL accepts a single http(s) url. // -// Passing nil results a valid empty instance. -func (c *Custom[T, V]) Parse(value any) error { - if value == nil { - *c = Custom[T, V]{} +// See also [URL]. +type HTTPURL[T constraint.Text] = Custom[T, validate.HTTPURL[T]] - return nil - } +// IP accepts an IP address. +// The address can be in dotted decimal ("192.0.2.1"), +// IPv6 ("2001:db8::68"), or IPv6 with a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). +type IP[T constraint.Text] = Custom[T, validate.IP[T]] - rValue := reflect.ValueOf(value) +// IPV4 accepts an IP V4 address (e.g. "192.0.2.1"). +type IPV4[T constraint.Text] = Custom[T, validate.IPV4[T]] - if rValue.Kind() == reflect.Pointer && rValue.IsNil() { - *c = Custom[T, V]{} +// IPV6 accepts an IP V6 address, including IPv4-mapped IPv6 addresses. +// The address can be regular IPv6 ("2001:db8::68"), or IPv6 with +// a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). +type IPV6[T constraint.Text] = Custom[T, validate.IPV6[T]] - return nil - } +// MAC accepts an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet IP over InfiniBand link-layer address. +type MAC[T constraint.Text] = Custom[T, validate.MAC[T]] - 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() +// CIDR accepts CIDR notation IP address and prefix length, +// like "192.0.2.0/24" or "2001:db8::/32", as defined in RFC 4632 and RFC 4291. +type CIDR[T constraint.Text] = Custom[T, validate.CIDR[T]] - if !ok { - *c = Custom[T, V]{} +// Base64 accepts valid base64 encoded strings. +type Base64[T constraint.Text] = Custom[T, validate.Base64[T]] - return nil - } +// Charset0 accepts (possibly empty) text which contains only runes acceptable by filter. +// See [Charset] for a non-empty variant. +type Charset0[T constraint.Text, F charset.Filter] = Custom[T, validate.Charset0[T, F]] - rValue = v - } +// Charset accepts non-empty text which contains only runes acceptable by filter. +// See also [Charset0]. +type Charset[T constraint.Text, F charset.Filter] = Custom[T, validate.Charset[T, F]] - v, err := convert[T](rValue) - if err != nil { - return parse.ParseError{Inner: err} - } +// Latitude accepts any number in the range [-90; 90]. +// +// See also [Longitude]. +type Latitude[T constraint.Real] = Custom[T, validate.Latitude[T]] + +// Longitude accepts any number in the range [-180; 180]. +// +// See also [Latitude]. +type Longitude[T constraint.Real] = Custom[T, validate.Longitude[T]] + +// InFuture accepts any time after current timestamp. +// +// See also [InPast]. +type InPast[T constraint.Time] = Custom[T, validate.InPast[T]] + +// InFuture accepts any time after current timestamp. +// +// See also [InPast]. +type InFuture[T constraint.Time] = Custom[T, validate.InFuture[T]] + +// Unique accepts a slice-like of unique values. +// +// See [UniqueSlice] for a slice shortcut. +type Unique[S ~[]T, T comparable] = Custom[S, validate.Unique[S, T]] + +// Unique accepts a slice of unique values. +// +// See [Unique] for a more generic version. +type UniqueSlice[T comparable] = Custom[[]T, validate.UniqueSlice[T]] + +// NonEmpty accepts a non-empty slice-like (len > 0). +// +// See [NonEmptySlice] for a slice shortcut. +type NonEmpty[S ~[]T, T any] = Custom[S, validate.NonEmpty[S, T]] + +// NonEmptySlice accepts a non-empty slice (len > 0). +// +// See [NonEmpty] for a more generic version. +type NonEmptySlice[T comparable] = Custom[[]T, validate.NonEmptySlice[T]] - aux := Custom[T, V]{ - hasValue: true, - value: v, - } +// MIME accepts RFC 1521 mime type string. +type MIME[T constraint.Text] = Custom[T, validate.MIME[T]] - if err := aux.TypeValidate(); err != nil { - return err - } +// UUID accepts a properly formatted UUID in one of the following formats: +// - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// - urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +// - {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} +type UUID[T constraint.Text] = Custom[T, validate.UUID[T]] - *c = aux +// JSON accepts valid json encoded text. +type JSON[T constraint.Text] = Custom[T, validate.JSON[T]] - return nil -} +// CountryAlpha2 accepts case-insensitive ISO 3166 2-letter country code. +type CountryAlpha2[T constraint.Text] = Custom[T, validate.CountryAlpha2[T]] -func (c *Custom[T, V]) MustParse(value any) { - if err := c.Parse(value); err != nil { - panic("MustParse failed") - } -} +// CountryAlpha3 accepts case-insensitive ISO 3166 3-letter country code. +type CountryAlpha3[T constraint.Text] = Custom[T, validate.CountryAlpha3[T]] -func (Custom[T, V]) isOptional() {} +// CountryAlpha accepts either [CountryAlpha2] or [CountryAlpha3]. +type CountryAlpha[T constraint.Text] = Custom[T, validate.CountryAlpha[T]] -func convert[T any](v reflect.Value) (T, error) { - tType := reflect.TypeFor[T]() +// CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code. +type CurrencyAlpha[T constraint.Text] = Custom[T, validate.CurrencyAlpha[T]] - original := v +// LangAlpha2 accepts case-insensitive ISO 639 2-letter language code. +type LangAlpha2[T constraint.Text] = Custom[T, validate.LangAlpha2[T]] - if v.Kind() == reflect.Pointer { - v = v.Elem() - } +// LangAlpha3 accepts case-insensitive ISO 639 3-letter language code. +type LangAlpha3[T constraint.Text] = Custom[T, validate.LangAlpha3[T]] - if v.CanConvert(tType) { - //nolint:forcetypeassert // checked already by CanConvert - return v.Convert(tType).Interface().(T), nil - } +// LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. +type LangAlpha[T constraint.Text] = Custom[T, validate.LangAlpha[T]] - return *new(T), parse.UnconvertableTypeError{ - Target: tType.String(), - Original: original.Type().String(), - } -} diff --git a/optional/optional_example_test.go b/optional/optional_example_test.go index c5d93bd..5657a72 100644 --- a/optional/optional_example_test.go +++ b/optional/optional_example_test.go @@ -492,9 +492,9 @@ func ExampleBase64() { // Invalid base64 error: true } -func ExampleCharset() { - // Charset allows empty strings - var value optional.Charset[string, charset.Letter] +func ExampleCharset0() { + // Charset0 allows empty strings + var value optional.Charset0[string, charset.Letter] value.MustParse("abcDEF") val, exists := value.Get() @@ -523,9 +523,9 @@ func ExampleCharset() { // Non-alphabetic error: true } -func ExampleNonZeroCharset() { - // NonZeroCharset requires non-empty strings - var value optional.NonZeroCharset[string, charset.Or[charset.Letter, charset.Number]] +func ExampleCharset() { + // Charset requires non-empty strings + var value optional.Charset[string, charset.Or[charset.Letter, charset.Number]] value.MustParse("abc123DEF") val, exists := value.Get() @@ -537,7 +537,7 @@ func ExampleNonZeroCharset() { fmt.Printf("After nil: Exists: %t\n", exists) // Empty strings will fail validation - var invalidValue optional.NonZeroCharset[string, charset.Or[charset.Letter, charset.Number]] + var invalidValue optional.Charset[string, charset.Or[charset.Letter, charset.Number]] err := invalidValue.Parse("") fmt.Printf("Empty string error: %t\n", err != nil) diff --git a/required/custom.go b/required/custom.go new file mode 100644 index 0000000..8d89154 --- /dev/null +++ b/required/custom.go @@ -0,0 +1,120 @@ +// Package required provides types whose values must be present and pass validation. +// +// Required types support the following encoding/decoding formats: +// - json +// - sql +// - text +// - binary +// - gob +package required + +import ( + "reflect" + + "github.com/metafates/schema/parse" + "github.com/metafates/schema/validate" +) + +var ( + ErrMissingValue = validate.ValidationError{Msg: "missing required value"} + ErrParseNilValue = parse.ParseError{Msg: "nil value passed for parsing"} +) + +// Custom required type. +// Errors if value is missing or did not pass the validation. +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 ErrMissingValue + } + + 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 +} + +// Get returns the contained value. +// Panics if value was not validated yet. +func (c Custom[T, V]) Get() T { + if !c.validated { + panic("called Get() on unvalidated value") + } + + return c.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 required type. +// +// Parsed type is validated, therefore it is safe to call [Custom.Get] afterwards. +func (c *Custom[T, V]) Parse(value any) error { + if value == nil { + return ErrParseNilValue + } + + rValue := reflect.ValueOf(value) + + if rValue.Kind() == reflect.Pointer { + if rValue.IsNil() { + return ErrParseNilValue + } + + rValue = rValue.Elem() + } + + tType := reflect.TypeFor[T]() + + if _, ok := value.(interface{ isRequired() }); ok { + // NOTE: ensure this method name is in sync with [Custom.Get] + rValue = rValue.MethodByName("Get").Call(nil)[0] + } + + if !rValue.CanConvert(tType) { + return parse.ParseError{ + Inner: parse.UnconvertableTypeError{ + Target: tType.String(), + Original: rValue.Type().String(), + }, + } + } + + //nolint:forcetypeassert // checked already by CanConvert + aux := Custom[T, V]{ + value: rValue.Convert(tType).Interface().(T), + hasValue: true, + } + + 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]) isRequired() {} diff --git a/required/required.go b/required/required.go index e1df9f1..f9da463 100644 --- a/required/required.go +++ b/required/required.go @@ -1,280 +1,177 @@ -// Package required provides types whose values must be present and pass validation. -// -// Required types support the following encoding/decoding formats: -// - json -// - sql -// - text -// - binary -// - gob +// Code generated by validators.py; DO NOT EDIT. package required import ( - "reflect" - "github.com/metafates/schema/constraint" - "github.com/metafates/schema/parse" "github.com/metafates/schema/validate" "github.com/metafates/schema/validate/charset" ) -var ( - ErrMissingValue = validate.ValidationError{Msg: "missing required value"} - ErrParseNilValue = parse.ParseError{Msg: "nil value passed for parsing"} -) +// Any accepts any value of T. +type Any[T any] = Custom[T, validate.Any[T]] + +// Zero accepts all zero values. +// +// The zero value is: +// - 0 for numeric types, +// - false for the boolean type, and +// - "" (the empty string) for strings. +// +// See [NonZero]. +type Zero[T comparable] = Custom[T, validate.Zero[T]] + +// NonZero accepts all non-zero values. +// +// The zero value is: +// - 0 for numeric types, +// - false for the boolean type, and +// - "" (the empty string) for strings. +// +// See [Zero]. +type NonZero[T comparable] = Custom[T, validate.NonZero[T]] -type ( - // Custom required type. - // Errors if value is missing or did not pass the validation. - Custom[T any, V validate.Validator[T]] struct { - value T - hasValue bool - validated bool - } - - // Any accepts any value of T. - Any[T any] = Custom[T, validate.Any[T]] - - // NonZero accepts all non-zero values. - // - // The zero value is: - // - 0 for numeric types, - // - false for the boolean type, and - // - "" (the empty string) for strings. - NonZero[T comparable] = Custom[T, validate.NonZero[T]] - - // Positive accepts all positive real numbers and zero. - // - // See also [Negative]. - Positive[T constraint.Real] = Custom[T, validate.Positive[T]] - - // Negative accepts all negative real numbers and zero. - // - // See also [Positive]. - Negative[T constraint.Real] = Custom[T, validate.Negative[T]] - - // Positive0 accepts all positive real numbers including zero. - // - // See [Positive] for zero excluding variant. - Positive0[T constraint.Real] = Custom[T, validate.Positive0[T]] - - // Negative0 accepts all negative real numbers including zero. - // - // See [Negative] for zero excluding variant. - Negative0[T constraint.Real] = Custom[T, validate.Negative0[T]] - - // Even accepts real numbers divisible by two. - Even[T constraint.Integer] = Custom[T, validate.Even[T]] - - // Odd accepts real numbers not divisible by two. - Odd[T constraint.Integer] = Custom[T, validate.Odd[T]] - - // Email accepts a single RFC 5322 address, e.g. "Barry Gibbs ". - Email[T constraint.Text] = Custom[T, validate.Email[T]] - - // URL accepts a single url. - // The url may be relative (a path, without a host) or absolute (starting with a scheme). - // - // See also [HTTPURL]. - URL[T constraint.Text] = Custom[T, validate.URL[T]] - - // HTTPURL accepts a single http(s) url. - // - // See also [URL]. - HTTPURL[T constraint.Text] = Custom[T, validate.HTTPURL[T]] - - // IP accepts an IP address. - // The address can be in dotted decimal ("192.0.2.1"), - // IPv6 ("2001:db8::68"), or IPv6 with a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). - IP[T constraint.Text] = Custom[T, validate.IP[T]] - - // IPV4 accepts an IP V4 address (e.g. "192.0.2.1"). - IPV4[T constraint.Text] = Custom[T, validate.IPV4[T]] - - // IPV6 accepts an IP V6 address, including IPv4-mapped IPv6 addresses. - // The address can be regular IPv6 ("2001:db8::68"), or IPv6 with - // a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). - IPV6[T constraint.Text] = Custom[T, validate.IPV6[T]] - - // MAC accepts an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet IP over InfiniBand link-layer address. - MAC[T constraint.Text] = Custom[T, validate.MAC[T]] - - // CIDR accepts CIDR notation IP address and prefix length, - // like "192.0.2.0/24" or "2001:db8::/32", as defined in RFC 4632 and RFC 4291. - CIDR[T constraint.Text] = Custom[T, validate.CIDR[T]] - - // Base64 accepts valid base64 encoded strings. - Base64[T constraint.Text] = Custom[T, validate.Base64[T]] - - // Charset0 accepts (possibly empty) text which contains only runes acceptable by filter. - // - // See [Charset] for a non-empty variant. - Charset0[T constraint.Text, F charset.Filter] = Custom[T, validate.Charset0[T, F]] - - // Charset accepts non-empty text which contains only runes acceptable by filter. - Charset[T constraint.Text, F charset.Filter] = Custom[T, validate.Charset[T, F]] - - // Latitude accepts any number in the range [-90; 90]. - // - // See also [Longitude]. - Latitude[T constraint.Real] = Custom[T, validate.Latitude[T]] - - // Longitude accepts any number in the range [-180; 180]. - // - // See also [Latitude]. - Longitude[T constraint.Real] = Custom[T, validate.Longitude[T]] - - // InPast accepts any time before current timestamp. - // - // See also [InFuture]. - InPast[T constraint.Time] = Custom[T, validate.InPast[T]] - - // InFuture accepts any time after current timestamp. - // - // See also [InPast]. - InFuture[T constraint.Time] = Custom[T, validate.InFuture[T]] - - // Unique accepts a slice-like of unique values. - // - // See [UniqueSlice] for a slice shortcut. - Unique[S ~[]T, T comparable] = Custom[S, validate.Unique[S, T]] - - // Unique accepts a slice of unique values. - // - // See [Unique] for a more generic version. - UniqueSlice[T comparable] = Custom[[]T, validate.UniqueSlice[T]] - - // NonEmpty accepts a non-empty slice-like (len > 0). - // - // See [NonEmptySlice] for a slice shortcut. - NonEmpty[S ~[]T, T any] = Custom[S, validate.NonEmpty[S, T]] - - // NonEmpty accepts a non-empty slice (len > 0). - // - // See [NonEmpty] for a more generic version. - NonEmptySlice[T any] = Custom[[]T, validate.NonEmptySlice[T]] - - // MIME accepts RFC 1521 mime type string. - MIME[T constraint.Text] = Custom[T, validate.MIME[T]] +// Positive accepts all positive real numbers excluding zero. +// +// See [Positive0] for zero including variant. +type Positive[T constraint.Real] = Custom[T, validate.Positive[T]] - // UUID accepts a properly formatted UUID in one of the following formats: - // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} - UUID[T constraint.Text] = Custom[T, validate.UUID[T]] - - // JSON accepts valid json encoded text. - JSON[T constraint.Text] = Custom[T, validate.JSON[T]] - - // CountryAlpha2 accepts case-insensitive ISO 3166 2-letter country code. - CountryAlpha2[T constraint.Text] = Custom[T, validate.CountryAlpha2[T]] - - // CountryAlpha3 accepts case-insensitive ISO 3166 3-letter country code. - CountryAlpha3[T constraint.Text] = Custom[T, validate.CountryAlpha3[T]] - - // CountryAlpha accepts either [CountryAlpha2] or [CountryAlpha3]. - CountryAlpha[T constraint.Text] = Custom[T, validate.CountryAlpha[T]] - - // CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code. - CurrencyAlpha[T constraint.Text] = Custom[T, validate.CurrencyAlpha[T]] - - // LangAlpha2 accepts case-insesitive ISO 639 2-letter language code. - LangAlpha2[T constraint.Text] = Custom[T, validate.LangAlpha2[T]] +// Negative accepts all negative real numbers excluding zero. +// +// See [Negative0] for zero including variant. +type Negative[T constraint.Real] = Custom[T, validate.Negative[T]] - // LangAlpha2 accepts case-insesitive ISO 639 3-letter language code. - LangAlpha3[T constraint.Text] = Custom[T, validate.LangAlpha3[T]] +// Positive0 accepts all positive real numbers including zero. +// +// See [Positive] for zero excluding variant. +type Positive0[T constraint.Real] = Custom[T, validate.Positive0[T]] - // LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. - LangAlpha[T constraint.Text] = Custom[T, validate.LangAlpha[T]] -) +// Negative0 accepts all negative real numbers including zero. +// +// See [Negative] for zero excluding variant. +type Negative0[T constraint.Real] = Custom[T, validate.Negative0[T]] + +// Even accepts integers divisible by two. +type Even[T constraint.Integer] = Custom[T, validate.Even[T]] + +// Odd accepts integers not divisible by two. +type Odd[T constraint.Integer] = Custom[T, validate.Odd[T]] + +// Email accepts a single RFC 5322 address, e.g. "Barry Gibbs ". +type Email[T constraint.Text] = Custom[T, validate.Email[T]] + +// URL accepts a single url. +// The url may be relative (a path, without a host) or absolute (starting with a scheme). +// +// See also [HTTPURL]. +type URL[T constraint.Text] = Custom[T, validate.URL[T]] + +// HTTPURL accepts a single http(s) url. +// +// See also [URL]. +type HTTPURL[T constraint.Text] = Custom[T, validate.HTTPURL[T]] + +// IP accepts an IP address. +// The address can be in dotted decimal ("192.0.2.1"), +// IPv6 ("2001:db8::68"), or IPv6 with a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). +type IP[T constraint.Text] = Custom[T, validate.IP[T]] + +// IPV4 accepts an IP V4 address (e.g. "192.0.2.1"). +type IPV4[T constraint.Text] = Custom[T, validate.IPV4[T]] + +// IPV6 accepts an IP V6 address, including IPv4-mapped IPv6 addresses. +// The address can be regular IPv6 ("2001:db8::68"), or IPv6 with +// a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). +type IPV6[T constraint.Text] = Custom[T, validate.IPV6[T]] + +// MAC accepts an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet IP over InfiniBand link-layer address. +type MAC[T constraint.Text] = Custom[T, validate.MAC[T]] + +// CIDR accepts CIDR notation IP address and prefix length, +// like "192.0.2.0/24" or "2001:db8::/32", as defined in RFC 4632 and RFC 4291. +type CIDR[T constraint.Text] = Custom[T, validate.CIDR[T]] + +// Base64 accepts valid base64 encoded strings. +type Base64[T constraint.Text] = Custom[T, validate.Base64[T]] -// TypeValidate implements the [validate.TypeValidateable] interface. -// You should not call this function directly. -func (c *Custom[T, V]) TypeValidate() error { - if !c.hasValue { - return ErrMissingValue - } - - 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 -} - -// Get returns the contained value. -// Panics if value was not validated yet. -func (c Custom[T, V]) Get() T { - if !c.validated { - panic("called Get() on unvalidated value") - } - - return c.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 required type. +// Charset0 accepts (possibly empty) text which contains only runes acceptable by filter. +// See [Charset] for a non-empty variant. +type Charset0[T constraint.Text, F charset.Filter] = Custom[T, validate.Charset0[T, F]] + +// Charset accepts non-empty text which contains only runes acceptable by filter. +// See also [Charset0]. +type Charset[T constraint.Text, F charset.Filter] = Custom[T, validate.Charset[T, F]] + +// Latitude accepts any number in the range [-90; 90]. +// +// See also [Longitude]. +type Latitude[T constraint.Real] = Custom[T, validate.Latitude[T]] + +// Longitude accepts any number in the range [-180; 180]. +// +// See also [Latitude]. +type Longitude[T constraint.Real] = Custom[T, validate.Longitude[T]] + +// InFuture accepts any time after current timestamp. +// +// See also [InPast]. +type InPast[T constraint.Time] = Custom[T, validate.InPast[T]] + +// InFuture accepts any time after current timestamp. +// +// See also [InPast]. +type InFuture[T constraint.Time] = Custom[T, validate.InFuture[T]] + +// Unique accepts a slice-like of unique values. +// +// See [UniqueSlice] for a slice shortcut. +type Unique[S ~[]T, T comparable] = Custom[S, validate.Unique[S, T]] + +// Unique accepts a slice of unique values. +// +// See [Unique] for a more generic version. +type UniqueSlice[T comparable] = Custom[[]T, validate.UniqueSlice[T]] + +// NonEmpty accepts a non-empty slice-like (len > 0). // -// Parsed type is validated, therefore it is safe to call [Custom.Get] afterwards. -func (c *Custom[T, V]) Parse(value any) error { - if value == nil { - return ErrParseNilValue - } - - rValue := reflect.ValueOf(value) - - if rValue.Kind() == reflect.Pointer { - if rValue.IsNil() { - return ErrParseNilValue - } - - rValue = rValue.Elem() - } - - tType := reflect.TypeFor[T]() - - if _, ok := value.(interface{ isRequired() }); ok { - // NOTE: ensure this method name is in sync with [Custom.Get] - rValue = rValue.MethodByName("Get").Call(nil)[0] - } - - if !rValue.CanConvert(tType) { - return parse.ParseError{ - Inner: parse.UnconvertableTypeError{ - Target: tType.String(), - Original: rValue.Type().String(), - }, - } - } - - //nolint:forcetypeassert // checked already by CanConvert - aux := Custom[T, V]{ - value: rValue.Convert(tType).Interface().(T), - hasValue: true, - } - - 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]) isRequired() {} +// See [NonEmptySlice] for a slice shortcut. +type NonEmpty[S ~[]T, T any] = Custom[S, validate.NonEmpty[S, T]] + +// NonEmptySlice accepts a non-empty slice (len > 0). +// +// See [NonEmpty] for a more generic version. +type NonEmptySlice[T comparable] = Custom[[]T, validate.NonEmptySlice[T]] + +// MIME accepts RFC 1521 mime type string. +type MIME[T constraint.Text] = Custom[T, validate.MIME[T]] + +// UUID accepts a properly formatted UUID in one of the following formats: +// - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// - urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +// - {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} +type UUID[T constraint.Text] = Custom[T, validate.UUID[T]] + +// JSON accepts valid json encoded text. +type JSON[T constraint.Text] = Custom[T, validate.JSON[T]] + +// CountryAlpha2 accepts case-insensitive ISO 3166 2-letter country code. +type CountryAlpha2[T constraint.Text] = Custom[T, validate.CountryAlpha2[T]] + +// CountryAlpha3 accepts case-insensitive ISO 3166 3-letter country code. +type CountryAlpha3[T constraint.Text] = Custom[T, validate.CountryAlpha3[T]] + +// CountryAlpha accepts either [CountryAlpha2] or [CountryAlpha3]. +type CountryAlpha[T constraint.Text] = Custom[T, validate.CountryAlpha[T]] + +// CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code. +type CurrencyAlpha[T constraint.Text] = Custom[T, validate.CurrencyAlpha[T]] + +// LangAlpha2 accepts case-insensitive ISO 639 2-letter language code. +type LangAlpha2[T constraint.Text] = Custom[T, validate.LangAlpha2[T]] + +// LangAlpha3 accepts case-insensitive ISO 639 3-letter language code. +type LangAlpha3[T constraint.Text] = Custom[T, validate.LangAlpha3[T]] + +// LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. +type LangAlpha[T constraint.Text] = Custom[T, validate.LangAlpha[T]] + diff --git a/schema.go b/schema.go new file mode 100644 index 0000000..ca7672f --- /dev/null +++ b/schema.go @@ -0,0 +1,10 @@ +// Package schema is schema declaration and validation with static types. +// No field tags or code duplication. +// +// Schema is designed to be as developer-friendly as possible. +// The goal is to eliminate duplicative type declarations. +// You declare a schema once and it will be used as both schema and type itself. +// It's easy to compose simpler types into complex data structures. +package schema + +//go:generate python3 validators.py diff --git a/validate/impl.go b/validate/impl.go new file mode 100644 index 0000000..23f2f80 --- /dev/null +++ b/validate/impl.go @@ -0,0 +1,374 @@ +package validate + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "math" + "mime" + "net" + "net/mail" + "net/netip" + "net/url" + "strings" + "time" + + "github.com/metafates/schema/internal/iso" + "github.com/metafates/schema/internal/uuid" +) + +func (Any[T]) Validate(T) error { + return nil +} + +func (Zero[T]) Validate(value T) error { + var empty T + + if value != empty { + return errors.New("non-zero value") + } + + return nil +} + +func (NonZero[T]) Validate(value T) error { + var empty T + + if value == empty { + return errors.New("zero value") + } + + return nil +} + +func (Positive[T]) Validate(value T) error { + if value < 0 { + return errors.New("negative value") + } + + if value == 0 { + return errors.New("zero value") + } + + return nil +} + +func (Negative[T]) Validate(value T) error { + if value > 0 { + return errors.New("positive value") + } + + if value == 0 { + return errors.New("zero value") + } + + return nil +} + +func (Even[T]) Validate(value T) error { + if value%2 != 0 { + return errors.New("odd value") + } + + return nil +} + +func (Odd[T]) Validate(value T) error { + if value%2 == 0 { + return errors.New("even value") + } + + return nil +} + +func (Email[T]) Validate(value T) error { + _, err := mail.ParseAddress(string(value)) + if err != nil { + return err + } + + return nil +} + +func (URL[T]) Validate(value T) error { + _, err := url.Parse(string(value)) + if err != nil { + return err + } + + return nil +} + +func (HTTPURL[T]) Validate(value T) error { + u, err := url.Parse(string(value)) + if err != nil { + return err + } + + if u.Host == "" { + return errors.New("empty host") + } + + switch u.Scheme { + case "http", "https": + return nil + + default: + return errors.New("non-http(s) scheme") + } +} + +func (IP[T]) Validate(value T) error { + _, err := netip.ParseAddr(string(value)) + if err != nil { + return err + } + + return nil +} + +func (IPV4[T]) Validate(value T) error { + a, err := netip.ParseAddr(string(value)) + if err != nil { + return err + } + + if !a.Is4() { + return errors.New("ipv6 address") + } + + return nil +} + +func (IPV6[T]) Validate(value T) error { + a, err := netip.ParseAddr(string(value)) + if err != nil { + return err + } + + if !a.Is6() { + return errors.New("ipv6 address") + } + + return nil +} + +func (MAC[T]) Validate(value T) error { + _, err := net.ParseMAC(string(value)) + if err != nil { + return err + } + + return nil +} + +func (CIDR[T]) Validate(value T) error { + _, _, err := net.ParseCIDR(string(value)) + if err != nil { + return err + } + + return nil +} + +func (Base64[T]) Validate(value T) error { + // TODO: implement it without allocating buffer and converting to string + _, err := base64.StdEncoding.DecodeString(string(value)) + if err != nil { + return err + } + + return nil +} + +func (Charset0[T, F]) Validate(value T) error { + var f F + + for _, r := range string(value) { + if err := f.Filter(r); err != nil { + return err + } + } + + return nil +} + +func (Charset[T, F]) Validate(value T) error { + if len(value) == 0 { + return errors.New("empty text") + } + + return Charset0[T, F]{}.Validate(value) +} + +func (Latitude[T]) Validate(value T) error { + abs := math.Abs(float64(value)) + + if abs > 90 { + return errors.New("invalid latitude") + } + + return nil +} + +func (Longitude[T]) Validate(value T) error { + abs := math.Abs(float64(value)) + + if abs > 180 { + return errors.New("invalid longitude") + } + + return nil +} + +func (InPast[T]) Validate(value T) error { + if value.Compare(time.Now()) > 0 { + return errors.New("time is not in the past") + } + + return nil +} + +func (InFuture[T]) Validate(value T) error { + if value.Compare(time.Now()) < 0 { + return errors.New("time is not in the future") + } + + return nil +} + +func (Unique[S, T]) Validate(value S) error { + visited := make(map[T]struct{}) + + for _, v := range value { + if _, ok := visited[v]; ok { + return errors.New("duplicate value found") + } + + visited[v] = struct{}{} + } + + return nil +} + +func (NonEmpty[S, T]) Validate(value S) error { + if len(value) == 0 { + return errors.New("empty slice") + } + + return nil +} + +func (MIME[T]) Validate(value T) error { + _, _, err := mime.ParseMediaType(string(value)) + if err != nil { + return err + } + + return nil +} + +func (UUID[T]) Validate(value T) error { + // converting to bytes is cheaper than vice versa + if err := uuid.Validate(string(value)); err != nil { + return err + } + + return nil +} + +func (JSON[T]) Validate(value T) error { + if !json.Valid([]byte(string(value))) { + return errors.New("invalid json") + } + + return nil +} + +func (CountryAlpha2[T]) Validate(value T) error { + v := strings.ToLower(string(value)) + + if _, ok := iso.CountryAlpha2[v]; !ok { + return errors.New("unknown 2-letter country code") + } + + return nil +} + +func (CountryAlpha3[T]) Validate(value T) error { + v := strings.ToLower(string(value)) + + if _, ok := iso.CountryAlpha3[v]; !ok { + return errors.New("unknown 3-letter country code") + } + + return nil +} + +func (CurrencyAlpha[T]) Validate(value T) error { + v := strings.ToLower(string(value)) + + if _, ok := iso.CurrencyAlpha[v]; !ok { + return errors.New("unknown currency alphabetic code") + } + + return nil +} + +func (LangAlpha2[T]) Validate(value T) error { + v := strings.ToLower(string(value)) + + if _, ok := iso.LanguageAlpha2[v]; !ok { + return errors.New("unknown 2-letter language code") + } + + return nil +} + +func (LangAlpha3[T]) Validate(value T) error { + v := strings.ToLower(string(value)) + + if _, ok := iso.LanguageAlpha3[v]; !ok { + return errors.New("unknown 3-letter language code") + } + + return nil +} + +func (And[T, A, B]) Validate(value T) error { + if err := (*new(A)).Validate(value); err != nil { + return err + } + + if err := (*new(B)).Validate(value); err != nil { + return err + } + + return nil +} + +func (Or[T, A, B]) Validate(value T) error { + errA := (*new(A)).Validate(value) + if errA == nil { + return nil + } + + errB := (*new(B)).Validate(value) + if errB == nil { + return nil + } + + return errors.Join(errA, errB) +} + +func (Not[T, V]) Validate(value T) error { + var v V + + //nolint:nilerr + if err := v.Validate(value); err != nil { + return nil + } + + return errors.New(fmt.Sprint(v)) +} diff --git a/validate/validators.go b/validate/validators.go index c298096..52596da 100644 --- a/validate/validators.go +++ b/validate/validators.go @@ -1,577 +1,205 @@ +// Code generated by validators.py; DO NOT EDIT. package validate import ( - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "math" - "mime" - "net" - "net/mail" - "net/netip" - "net/url" - "strings" - "time" - "github.com/metafates/schema/constraint" - "github.com/metafates/schema/internal/iso" - "github.com/metafates/schema/internal/uuid" "github.com/metafates/schema/validate/charset" ) -type ( - // Any accepts any value of T. - Any[T any] struct{} - - // Zero accepts all zero values. - // - // The zero value is: - // - 0 for numeric types, - // - false for the boolean type, and - // - "" (the empty string) for strings. - // - // See [NonZero]. - Zero[T comparable] struct{} - - // NonZero accepts all non-zero values. - // - // The zero value is: - // - 0 for numeric types, - // - false for the boolean type, and - // - "" (the empty string) for strings. - // - // See [Zero]. - NonZero[T comparable] struct{} - - // Positive accepts all positive real numbers excluding zero. - // - // See [Positive0] for zero including variant. - Positive[T constraint.Real] struct{} - - // Negative accepts all negative real numbers excluding zero. - // - // See [Negative0] for zero including variant. - Negative[T constraint.Real] struct{} - - // Positive0 accepts all positive real numbers including zero. - // - // See [Positive] for zero excluding variant. - Positive0[T constraint.Real] struct { - Or[T, Positive[T], Zero[T]] - } - - // Negative0 accepts all negative real numbers including zero. - // - // See [Negative] for zero excluding variant. - Negative0[T constraint.Real] struct { - Or[T, Negative[T], Zero[T]] - } - - // Even accepts integers divisible by two. - Even[T constraint.Integer] struct{} - - // Odd accepts integers not divisible by two. - Odd[T constraint.Integer] struct{} - - // Email accepts a single RFC 5322 address, e.g. "Barry Gibbs ". - Email[T constraint.Text] struct{} - - // URL accepts a single url. - // The url may be relative (a path, without a host) or absolute (starting with a scheme). - // - // See also [HTTPURL]. - URL[T constraint.Text] struct{} - - // HTTPURL accepts a single http(s) url. - // - // See also [URL]. - HTTPURL[T constraint.Text] struct{} - - // IP accepts an IP address. - // The address can be in dotted decimal ("192.0.2.1"), - // IPv6 ("2001:db8::68"), or IPv6 with a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). - IP[T constraint.Text] struct{} - - // IPV4 accepts an IP V4 address (e.g. "192.0.2.1"). - IPV4[T constraint.Text] struct{} - - // IPV6 accepts an IP V6 address, including IPv4-mapped IPv6 addresses. - // The address can be regular IPv6 ("2001:db8::68"), or IPv6 with - // a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). - IPV6[T constraint.Text] struct{} - - // MAC accepts an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet IP over InfiniBand link-layer address. - MAC[T constraint.Text] struct{} - - // CIDR accepts CIDR notation IP address and prefix length, - // like "192.0.2.0/24" or "2001:db8::/32", as defined in RFC 4632 and RFC 4291. - CIDR[T constraint.Text] struct{} - - // Base64 accepts valid base64 encoded strings. - Base64[T constraint.Text] struct{} - - // Charset0 accepts (possibly empty) text which contains only runes acceptable by filter. - // - // See [Charset] for a non-empty variant. - Charset0[T constraint.Text, F charset.Filter] struct{} - - // Charset accepts non-empty text which contains only runes acceptable by filter. - // - // See also [Charset0]. - Charset[T constraint.Text, F charset.Filter] struct{} - - // Latitude accepts any number in the range [-90; 90]. - // - // See also [Longitude]. - Latitude[T constraint.Real] struct{} - - // Longitude accepts any number in the range [-180; 180]. - // - // See also [Latitude]. - Longitude[T constraint.Real] struct{} - - // InPast accepts any time before current timestamp. - // - // See also [InFuture]. - InPast[T constraint.Time] struct{} - - // InFuture accepts any time after current timestamp. - // - // See also [InPast]. - InFuture[T constraint.Time] struct{} - - // Unique accepts a slice-like of unique values. - // - // See [UniqueSlice] for a slice shortcut. - Unique[S ~[]T, T comparable] struct{} - - // Unique accepts a slice of unique values. - // - // See [Unique] for a more generic version. - UniqueSlice[T comparable] struct { - Unique[[]T, T] - } - - // NonEmpty accepts a non-empty slice-like (len > 0). - // - // See [NonEmptySlice] for a slice shortcut. - NonEmpty[S ~[]T, T any] struct{} - - // NonEmpty accepts a non-empty slice (len > 0). - // - // See [NonEmpty] for a more generic version. - NonEmptySlice[T any] struct { - NonEmpty[[]T, T] - } - - // MIME accepts RFC 1521 mime type string. - MIME[T constraint.Text] struct{} - - // UUID accepts a properly formatted UUID in one of the following formats: - // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} - UUID[T constraint.Text] struct{} - - // JSON accepts valid json encoded text. - JSON[T constraint.Text] struct{} - - // CountryAlpha2 accepts case-insensitive ISO 3166 2-letter country code. - CountryAlpha2[T constraint.Text] struct{} - - // CountryAlpha2 accepts case-insensitive ISO 3166 3-letter country code. - CountryAlpha3[T constraint.Text] struct{} - - // CountryAlpha2 accepts either [CountryAlpha2] or [CountryAlpha3]. - CountryAlpha[T constraint.Text] struct { - Or[T, CountryAlpha2[T], CountryAlpha3[T]] - } - - // CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code. - CurrencyAlpha[T constraint.Text] struct{} - - // LangAlpha2 accepts case-insesitive ISO 639 2-letter language code. - LangAlpha2[T constraint.Text] struct{} - - // LangAlpha3 accepts case-insesitive ISO 639 3-letter language code. - LangAlpha3[T constraint.Text] struct{} - - // LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. - LangAlpha[T constraint.Text] struct { - Or[T, LangAlpha2[T], LangAlpha3[T]] - } - - // And is a meta validator that combines other validators with AND operator. - // Validators are called in the same order as specified by type parameters. - // - // See also [Or], [Not]. - And[T any, A Validator[T], B Validator[T]] struct{} - - // Or is a meta validator that combines other validators with OR operator. - // Validators are called in the same order as type parameters. - // - // See also [And], [Not]. - Or[T any, A Validator[T], B Validator[T]] struct{} - - // Not is a meta validator that inverts given validator. - // - // See also [And], [Or]. - Not[T any, V Validator[T]] struct{} -) - -func (Any[T]) Validate(T) error { - return nil -} - -func (Zero[T]) Validate(value T) error { - var empty T - - if value != empty { - return errors.New("non-zero value") - } - - return nil -} - -func (NonZero[T]) Validate(value T) error { - var empty T - - if value == empty { - return errors.New("zero value") - } - - return nil -} - -func (Positive[T]) Validate(value T) error { - if value < 0 { - return errors.New("negative value") - } - - if value == 0 { - return errors.New("zero value") - } - - return nil -} - -func (Negative[T]) Validate(value T) error { - if value > 0 { - return errors.New("positive value") - } - - if value == 0 { - return errors.New("zero value") - } - - return nil -} - -func (Even[T]) Validate(value T) error { - if value%2 != 0 { - return errors.New("odd value") - } - - return nil -} - -func (Odd[T]) Validate(value T) error { - if value%2 == 0 { - return errors.New("even value") - } - - return nil -} - -func (Email[T]) Validate(value T) error { - _, err := mail.ParseAddress(string(value)) - if err != nil { - return err - } - - return nil -} - -func (URL[T]) Validate(value T) error { - _, err := url.Parse(string(value)) - if err != nil { - return err - } - - return nil -} - -func (HTTPURL[T]) Validate(value T) error { - u, err := url.Parse(string(value)) - if err != nil { - return err - } - - if u.Host == "" { - return errors.New("empty host") - } - - switch u.Scheme { - case "http", "https": - return nil - - default: - return errors.New("non-http(s) scheme") - } -} - -func (IP[T]) Validate(value T) error { - _, err := netip.ParseAddr(string(value)) - if err != nil { - return err - } - - return nil -} - -func (IPV4[T]) Validate(value T) error { - a, err := netip.ParseAddr(string(value)) - if err != nil { - return err - } +// Any accepts any value of T. +type Any[T any] struct{} + +// Zero accepts all zero values. +// +// The zero value is: +// - 0 for numeric types, +// - false for the boolean type, and +// - "" (the empty string) for strings. +// +// See [NonZero]. +type Zero[T comparable] struct{} + +// NonZero accepts all non-zero values. +// +// The zero value is: +// - 0 for numeric types, +// - false for the boolean type, and +// - "" (the empty string) for strings. +// +// See [Zero]. +type NonZero[T comparable] struct{} + +// Positive accepts all positive real numbers excluding zero. +// +// See [Positive0] for zero including variant. +type Positive[T constraint.Real] struct{} + +// Negative accepts all negative real numbers excluding zero. +// +// See [Negative0] for zero including variant. +type Negative[T constraint.Real] struct{} + +// Positive0 accepts all positive real numbers including zero. +// +// See [Positive] for zero excluding variant. +type Positive0[T constraint.Real] struct{ + Or[T, Positive[T], Zero[T]] +} + +// Negative0 accepts all negative real numbers including zero. +// +// See [Negative] for zero excluding variant. +type Negative0[T constraint.Real] struct{ + Or[T, Negative[T], Zero[T]] +} + +// Even accepts integers divisible by two. +type Even[T constraint.Integer] struct{} + +// Odd accepts integers not divisible by two. +type Odd[T constraint.Integer] struct{} + +// Email accepts a single RFC 5322 address, e.g. "Barry Gibbs ". +type Email[T constraint.Text] struct{} + +// URL accepts a single url. +// The url may be relative (a path, without a host) or absolute (starting with a scheme). +// +// See also [HTTPURL]. +type URL[T constraint.Text] struct{} + +// HTTPURL accepts a single http(s) url. +// +// See also [URL]. +type HTTPURL[T constraint.Text] struct{} + +// IP accepts an IP address. +// The address can be in dotted decimal ("192.0.2.1"), +// IPv6 ("2001:db8::68"), or IPv6 with a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). +type IP[T constraint.Text] struct{} + +// IPV4 accepts an IP V4 address (e.g. "192.0.2.1"). +type IPV4[T constraint.Text] struct{} + +// IPV6 accepts an IP V6 address, including IPv4-mapped IPv6 addresses. +// The address can be regular IPv6 ("2001:db8::68"), or IPv6 with +// a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). +type IPV6[T constraint.Text] struct{} - if !a.Is4() { - return errors.New("ipv6 address") - } - - return nil -} +// MAC accepts an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet IP over InfiniBand link-layer address. +type MAC[T constraint.Text] struct{} + +// CIDR accepts CIDR notation IP address and prefix length, +// like "192.0.2.0/24" or "2001:db8::/32", as defined in RFC 4632 and RFC 4291. +type CIDR[T constraint.Text] struct{} + +// Base64 accepts valid base64 encoded strings. +type Base64[T constraint.Text] struct{} + +// Charset0 accepts (possibly empty) text which contains only runes acceptable by filter. +// See [Charset] for a non-empty variant. +type Charset0[T constraint.Text, F charset.Filter] struct{} + +// Charset accepts non-empty text which contains only runes acceptable by filter. +// See also [Charset0]. +type Charset[T constraint.Text, F charset.Filter] struct{} + +// Latitude accepts any number in the range [-90; 90]. +// +// See also [Longitude]. +type Latitude[T constraint.Real] struct{} + +// Longitude accepts any number in the range [-180; 180]. +// +// See also [Latitude]. +type Longitude[T constraint.Real] struct{} + +// InFuture accepts any time after current timestamp. +// +// See also [InPast]. +type InPast[T constraint.Time] struct{} -func (IPV6[T]) Validate(value T) error { - a, err := netip.ParseAddr(string(value)) - if err != nil { - return err - } +// InFuture accepts any time after current timestamp. +// +// See also [InPast]. +type InFuture[T constraint.Time] struct{} - if !a.Is6() { - return errors.New("ipv6 address") - } +// Unique accepts a slice-like of unique values. +// +// See [UniqueSlice] for a slice shortcut. +type Unique[S ~[]T, T comparable] struct{} - return nil +// Unique accepts a slice of unique values. +// +// See [Unique] for a more generic version. +type UniqueSlice[T comparable] struct{ + Unique[[]T, T] } -func (MAC[T]) Validate(value T) error { - _, err := net.ParseMAC(string(value)) - if err != nil { - return err - } +// NonEmpty accepts a non-empty slice-like (len > 0). +// +// See [NonEmptySlice] for a slice shortcut. +type NonEmpty[S ~[]T, T any] struct{} - return nil +// NonEmptySlice accepts a non-empty slice (len > 0). +// +// See [NonEmpty] for a more generic version. +type NonEmptySlice[T comparable] struct{ + NonEmpty[[]T, T] } -func (CIDR[T]) Validate(value T) error { - _, _, err := net.ParseCIDR(string(value)) - if err != nil { - return err - } +// MIME accepts RFC 1521 mime type string. +type MIME[T constraint.Text] struct{} - return nil -} - -func (Base64[T]) Validate(value T) error { - // TODO: implement it without allocating buffer and converting to string - _, err := base64.StdEncoding.DecodeString(string(value)) - if err != nil { - return err - } +// UUID accepts a properly formatted UUID in one of the following formats: +// - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// - urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +// - {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} +type UUID[T constraint.Text] struct{} - return nil -} +// JSON accepts valid json encoded text. +type JSON[T constraint.Text] struct{} -func (Charset0[T, F]) Validate(value T) error { - var f F +// CountryAlpha2 accepts case-insensitive ISO 3166 2-letter country code. +type CountryAlpha2[T constraint.Text] struct{} - for _, r := range string(value) { - if err := f.Filter(r); err != nil { - return err - } - } +// CountryAlpha3 accepts case-insensitive ISO 3166 3-letter country code. +type CountryAlpha3[T constraint.Text] struct{} - return nil +// CountryAlpha accepts either [CountryAlpha2] or [CountryAlpha3]. +type CountryAlpha[T constraint.Text] struct{ + Or[T, CountryAlpha2[T], CountryAlpha3[T]] } -func (Charset[T, F]) Validate(value T) error { - if len(value) == 0 { - return errors.New("empty text") - } +// CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code. +type CurrencyAlpha[T constraint.Text] struct{} - return Charset0[T, F]{}.Validate(value) -} - -func (Latitude[T]) Validate(value T) error { - abs := math.Abs(float64(value)) +// LangAlpha2 accepts case-insensitive ISO 639 2-letter language code. +type LangAlpha2[T constraint.Text] struct{} - if abs > 90 { - return errors.New("invalid latitude") - } +// LangAlpha3 accepts case-insensitive ISO 639 3-letter language code. +type LangAlpha3[T constraint.Text] struct{} - return nil +// LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. +type LangAlpha[T constraint.Text] struct{ + Or[T, LangAlpha2[T], LangAlpha3[T]] } -func (Longitude[T]) Validate(value T) error { - abs := math.Abs(float64(value)) - - if abs > 180 { - return errors.New("invalid longitude") - } +// And is a meta validator that combines other validators with AND operator. +// Validators are called in the same order as specified by type parameters. +// +// See also [Or], [Not]. +type And[T any, A Validator[T], B Validator[T]] struct{} - return nil -} - -func (InPast[T]) Validate(value T) error { - if value.Compare(time.Now()) > 0 { - return errors.New("time is not in the past") - } +// Or is a meta validator that combines other validators with OR operator. +// Validators are called in the same order as type parameters. +// +// See also [And], [Not]. +type Or[T any, A Validator[T], B Validator[T]] struct{} - return nil -} +// Not is a meta validator that inverts given validator. +// +// See also [And], [Or]. +type Not[T any, V Validator[T]] struct{} -func (InFuture[T]) Validate(value T) error { - if value.Compare(time.Now()) < 0 { - return errors.New("time is not in the future") - } - - return nil -} - -func (Unique[S, T]) Validate(value S) error { - visited := make(map[T]struct{}) - - for _, v := range value { - if _, ok := visited[v]; ok { - return errors.New("duplicate value found") - } - - visited[v] = struct{}{} - } - - return nil -} - -func (NonEmpty[S, T]) Validate(value S) error { - if len(value) == 0 { - return errors.New("empty slice") - } - - return nil -} - -func (MIME[T]) Validate(value T) error { - _, _, err := mime.ParseMediaType(string(value)) - if err != nil { - return err - } - - return nil -} - -func (UUID[T]) Validate(value T) error { - // converting to bytes is cheaper than vice versa - if err := uuid.Validate(string(value)); err != nil { - return err - } - - return nil -} - -func (JSON[T]) Validate(value T) error { - if !json.Valid([]byte(string(value))) { - return errors.New("invalid json") - } - - return nil -} - -func (CountryAlpha2[T]) Validate(value T) error { - v := strings.ToLower(string(value)) - - if _, ok := iso.CountryAlpha2[v]; !ok { - return errors.New("unknown 2-letter country code") - } - - return nil -} - -func (CountryAlpha3[T]) Validate(value T) error { - v := strings.ToLower(string(value)) - - if _, ok := iso.CountryAlpha3[v]; !ok { - return errors.New("unknown 3-letter country code") - } - - return nil -} - -func (CurrencyAlpha[T]) Validate(value T) error { - v := strings.ToLower(string(value)) - - if _, ok := iso.CurrencyAlpha[v]; !ok { - return errors.New("unknown currency alphabetic code") - } - - return nil -} - -func (LangAlpha2[T]) Validate(value T) error { - v := strings.ToLower(string(value)) - - if _, ok := iso.LanguageAlpha2[v]; !ok { - return errors.New("unknown 2-letter language code") - } - - return nil -} - -func (LangAlpha3[T]) Validate(value T) error { - v := strings.ToLower(string(value)) - - if _, ok := iso.LanguageAlpha3[v]; !ok { - return errors.New("unknown 3-letter language code") - } - - return nil -} - -func (And[T, A, B]) Validate(value T) error { - if err := (*new(A)).Validate(value); err != nil { - return err - } - - if err := (*new(B)).Validate(value); err != nil { - return err - } - - return nil -} - -func (Or[T, A, B]) Validate(value T) error { - errA := (*new(A)).Validate(value) - if errA == nil { - return nil - } - - errB := (*new(B)).Validate(value) - if errB == nil { - return nil - } - - return errors.Join(errA, errB) -} - -func (Not[T, V]) Validate(value T) error { - var v V - - //nolint:nilerr - if err := v.Validate(value); err != nil { - return nil - } - - return errors.New(fmt.Sprint(v)) -} diff --git a/validators.md b/validators.md new file mode 100644 index 0000000..d81816e --- /dev/null +++ b/validators.md @@ -0,0 +1,47 @@ +# Validators + +This table features all available validators. + +| Name | Description | +| ---- | ----------- | +| `Any[T]` | Any accepts any value of T. | +| `Zero[T]` | Zero accepts all zero values.

The zero value is:
- 0 for numeric types,
- false for the boolean type, and
- "" (the empty string) for strings.

See [NonZero]. | +| `NonZero[T]` | NonZero accepts all non-zero values.

The zero value is:
- 0 for numeric types,
- false for the boolean type, and
- "" (the empty string) for strings.

See [Zero]. | +| `Positive[T]` | Positive accepts all positive real numbers excluding zero.

See [Positive0] for zero including variant. | +| `Negative[T]` | Negative accepts all negative real numbers excluding zero.

See [Negative0] for zero including variant. | +| `Positive0[T]` | Positive0 accepts all positive real numbers including zero.

See [Positive] for zero excluding variant. | +| `Negative0[T]` | Negative0 accepts all negative real numbers including zero.

See [Negative] for zero excluding variant. | +| `Even[T]` | Even accepts integers divisible by two. | +| `Odd[T]` | Odd accepts integers not divisible by two. | +| `Email[T]` | Email accepts a single RFC 5322 address, e.g. "Barry Gibbs ". | +| `URL[T]` | URL accepts a single url.
The url may be relative (a path, without a host) or absolute (starting with a scheme).

See also [HTTPURL]. | +| `HTTPURL[T]` | HTTPURL accepts a single http(s) url.

See also [URL]. | +| `IP[T]` | IP accepts an IP address.
The address can be in dotted decimal ("192.0.2.1"),
IPv6 ("2001:db8::68"), or IPv6 with a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). | +| `IPV4[T]` | IPV4 accepts an IP V4 address (e.g. "192.0.2.1"). | +| `IPV6[T]` | IPV6 accepts an IP V6 address, including IPv4-mapped IPv6 addresses.
The address can be regular IPv6 ("2001:db8::68"), or IPv6 with
a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). | +| `MAC[T]` | MAC accepts an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet IP over InfiniBand link-layer address. | +| `CIDR[T]` | CIDR accepts CIDR notation IP address and prefix length,
like "192.0.2.0/24" or "2001:db8::/32", as defined in RFC 4632 and RFC 4291. | +| `Base64[T]` | Base64 accepts valid base64 encoded strings. | +| `Charset0[T, F]` | Charset0 accepts (possibly empty) text which contains only runes acceptable by filter.
See [Charset] for a non-empty variant. | +| `Charset[T, F]` | Charset accepts non-empty text which contains only runes acceptable by filter.
See also [Charset0]. | +| `Latitude[T]` | Latitude accepts any number in the range [-90; 90].

See also [Longitude]. | +| `Longitude[T]` | Longitude accepts any number in the range [-180; 180].

See also [Latitude]. | +| `InPast[T]` | InFuture accepts any time after current timestamp.

See also [InPast]. | +| `InFuture[T]` | InFuture accepts any time after current timestamp.

See also [InPast]. | +| `Unique[S, T]` | Unique accepts a slice-like of unique values.

See [UniqueSlice] for a slice shortcut. | +| `UniqueSlice[T]` | Unique accepts a slice of unique values.

See [Unique] for a more generic version. | +| `NonEmpty[S, T]` | NonEmpty accepts a non-empty slice-like (len > 0).

See [NonEmptySlice] for a slice shortcut. | +| `NonEmptySlice[T]` | NonEmptySlice accepts a non-empty slice (len > 0).

See [NonEmpty] for a more generic version. | +| `MIME[T]` | MIME accepts RFC 1521 mime type string. | +| `UUID[T]` | UUID accepts a properly formatted UUID in one of the following formats:
- xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
- urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} | +| `JSON[T]` | JSON accepts valid json encoded text. | +| `CountryAlpha2[T]` | CountryAlpha2 accepts case-insensitive ISO 3166 2-letter country code. | +| `CountryAlpha3[T]` | CountryAlpha3 accepts case-insensitive ISO 3166 3-letter country code. | +| `CountryAlpha[T]` | CountryAlpha accepts either [CountryAlpha2] or [CountryAlpha3]. | +| `CurrencyAlpha[T]` | CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code. | +| `LangAlpha2[T]` | LangAlpha2 accepts case-insensitive ISO 639 2-letter language code. | +| `LangAlpha3[T]` | LangAlpha3 accepts case-insensitive ISO 639 3-letter language code. | +| `LangAlpha[T]` | LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. | +| `And[T, A, B]` | And is a meta validator that combines other validators with AND operator.
Validators are called in the same order as specified by type parameters.

See also [Or], [Not]. | +| `Or[T, A, B]` | Or is a meta validator that combines other validators with OR operator.
Validators are called in the same order as type parameters.

See also [And], [Not]. | +| `Not[T, V]` | Not is a meta validator that inverts given validator.

See also [And], [Or]. | diff --git a/validators.py b/validators.py new file mode 100644 index 0000000..5768fbb --- /dev/null +++ b/validators.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional, TYPE_CHECKING, Protocol +from pathlib import Path +import tomllib + +if TYPE_CHECKING: + from _typeshed import SupportsWrite + + +@dataclass +class Import: + path: str + pkg: str + + +@dataclass +class ValidatorType: + name: str + constraint: str + + +@dataclass +class Validator: + name: str + internal: bool + desc: str + types: list[ValidatorType] + embed: Optional[str] + aliased: Optional[str] + + +@dataclass +class Data: + validators: list[Validator] + imports: set[str] + + +def read(path: str) -> Data: + with open(path, "rb") as f: + data = tomllib.load(f) + + imports_registry: dict[str, Import] = {} + + for import_ in data["imports"]: + pkg = import_["pkg"] + path = import_["path"] + + imports_registry[pkg] = Import(path=path, pkg=pkg) + + imports: set[str] = set() + + validators: list[Validator] = [] + + for entry in data["validators"]: + types: list[ValidatorType] = [] + + for type_ in entry["types"]: + validator_type = ValidatorType( + name=type_["name"], constraint=type_["constraint"] + ) + + types.append( + ValidatorType(name=type_["name"], constraint=type_["constraint"]) + ) + + if "." in validator_type.constraint: + pkg = validator_type.constraint.split(".")[0] + + if pkg not in imports_registry: + raise Exception( + f"unknown package for constraint: {validator_type.constraint}" + ) + + imports.add(imports_registry[pkg].path) + + validator = Validator( + name=entry["name"], + internal=entry.get("internal", False), + desc=entry["desc"], + types=types, + embed=entry.get("embed"), + aliased=entry.get("aliased"), + ) + + validators.append(validator) + + return Data(validators=validators, imports=imports) + + +class P(Protocol): + def __call__(self, *args: object) -> None: ... + + +def make_p(file: SupportsWrite[str]) -> P: + def p(*s: object): + print(*s, file=file) + + return p + + +def comment(s: str) -> str: + commented_lines: list[str] = [] + + for line in s.splitlines(): + commented_lines.append(f"// {line.rstrip()}".strip()) + + return "\n".join(commented_lines) + + +PREAMBLE = "// Code generated by validators.py; DO NOT EDIT." + + +def generate_imports(file: SupportsWrite[str], imports: set[str]): + if not len(imports): + return + + p = make_p(file) + p("import (") + + for path in sorted(imports): + p(f'\t"{path}"') + + p(")") + p() + + +def generate_validators(file: SupportsWrite[str], data: Data): + p = make_p(file) + + p(PREAMBLE) + p("package validate") + p() + + generate_imports(file, data.imports) + + for v in data.validators: + types_str = "" + types = list(map(lambda t: f"{t.name} {t.constraint}", v.types)) + + if len(types): + types_str = f"[{', '.join(types)}]" + + embed = "" + + if v.embed: + embed = f"\n\t{v.embed}\n" + + desc = comment(v.desc) + + if desc: + p(desc) + + p(f"type {v.name}{types_str} struct{{{embed}}}") + p() + + +def generate_aliases(file: SupportsWrite[str], data: Data, pkg: str): + p = make_p(file) + + p(PREAMBLE) + p(f"package {pkg}") + p() + + imports = data.imports.copy() + imports.add("github.com/metafates/schema/validate") + + generate_imports(file, imports) + + for v in filter(lambda v: not v.internal, data.validators): + types_str = "" + types = list(map(lambda t: f"{t.name} {t.constraint}", v.types)) + + if len(types): + types_str = f"[{', '.join(types)}]" + + desc = comment(v.desc) + + if desc: + p(desc) + + aliased = v.aliased + if not aliased: + types = list(map(lambda t: t.name, v.types)) + aliased = f"Custom[{types[0]}, validate.{v.name}[{', '.join(types)}]]" + + p(f"type {v.name}{types_str} = {aliased}") + p() + + +def generate_markdown(file: SupportsWrite[str], data: Data): + p = make_p(file) + + p("# Validators") + p("") + p("This table features all available validators.") + p("") + p("| Name | Description |") + p("| ---- | ----------- |") + for v in data.validators: + desc = "
".join(v.desc.splitlines()) + types = list(map(lambda t: t.name, v.types)) + + p(f"| `{v.name}[{', '.join(types)}]` | {desc} |") + + +def main(): + data = read("validators.toml") + + with Path("validate").joinpath("validators.go").open("w+") as out: + generate_validators(out, data) + + with Path("required").joinpath("required.go").open("w+") as out: + generate_aliases(out, data, pkg="required") + + with Path("optional").joinpath("optional.go").open("w+") as out: + generate_aliases(out, data, pkg="optional") + + with Path("validators.md").open("w+") as out: + generate_markdown(out, data) + + +if __name__ == "__main__": + main() diff --git a/validators.toml b/validators.toml new file mode 100644 index 0000000..0e25979 --- /dev/null +++ b/validators.toml @@ -0,0 +1,495 @@ +[[imports]] +path = "github.com/metafates/schema/constraint" +pkg = "constraint" + +[[imports]] +path = "github.com/metafates/schema/validate/charset" +pkg = "charset" + +[[validators]] +name = "Any" +desc = "Any accepts any value of T." + + [[validators.types]] + name = "T" + constraint = "any" + +[[validators]] +name = "Zero" +desc = """ +Zero accepts all zero values. + +The zero value is: +- 0 for numeric types, +- false for the boolean type, and +- "" (the empty string) for strings. + +See [NonZero]. +""" + + [[validators.types]] + name = "T" + constraint = "comparable" + +[[validators]] +name = "NonZero" +desc = """ +NonZero accepts all non-zero values. + +The zero value is: +- 0 for numeric types, +- false for the boolean type, and +- "" (the empty string) for strings. + +See [Zero]. +""" + + [[validators.types]] + name = "T" + constraint = "comparable" + +[[validators]] +name = "Positive" +desc = """ +Positive accepts all positive real numbers excluding zero. + +See [Positive0] for zero including variant. +""" + + [[validators.types]] + name = "T" + constraint = "constraint.Real" + +[[validators]] +name = "Negative" +desc = """ +Negative accepts all negative real numbers excluding zero. + +See [Negative0] for zero including variant. +""" + + [[validators.types]] + name = "T" + constraint = "constraint.Real" + imports = "github.com/metafates/schema/constraint" + +[[validators]] +name = "Positive0" +desc = """ +Positive0 accepts all positive real numbers including zero. + +See [Positive] for zero excluding variant. +""" +embed = "Or[T, Positive[T], Zero[T]]" + + [[validators.types]] + name = "T" + constraint = "constraint.Real" + +[[validators]] +name = "Negative0" +desc = """ +Negative0 accepts all negative real numbers including zero. + +See [Negative] for zero excluding variant. +""" +embed = "Or[T, Negative[T], Zero[T]]" + + [[validators.types]] + name = "T" + constraint = "constraint.Real" + +[[validators]] +name = "Even" +desc = "Even accepts integers divisible by two." + + [[validators.types]] + name = "T" + constraint = "constraint.Integer" + +[[validators]] +name = "Odd" +desc = "Odd accepts integers not divisible by two." + + [[validators.types]] + name = "T" + constraint = "constraint.Integer" + +[[validators]] +name = "Email" +desc = 'Email accepts a single RFC 5322 address, e.g. "Barry Gibbs ".' + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "URL" +desc = """ +URL accepts a single url. +The url may be relative (a path, without a host) or absolute (starting with a scheme). + +See also [HTTPURL]. +""" + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "HTTPURL" +desc = """ +HTTPURL accepts a single http(s) url. + +See also [URL]. +""" + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "IP" +desc = """ +IP accepts an IP address. +The address can be in dotted decimal ("192.0.2.1"), +IPv6 ("2001:db8::68"), or IPv6 with a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). +""" + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "IPV4" +desc = 'IPV4 accepts an IP V4 address (e.g. "192.0.2.1").' + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "IPV6" +desc = """ +IPV6 accepts an IP V6 address, including IPv4-mapped IPv6 addresses. +The address can be regular IPv6 ("2001:db8::68"), or IPv6 with +a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). +""" + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "MAC" +desc = "MAC accepts an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet IP over InfiniBand link-layer address." + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "CIDR" +desc = """ +CIDR accepts CIDR notation IP address and prefix length, +like "192.0.2.0/24" or "2001:db8::/32", as defined in RFC 4632 and RFC 4291. +""" + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "Base64" +desc = "Base64 accepts valid base64 encoded strings." + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "Charset0" +desc = """ +Charset0 accepts (possibly empty) text which contains only runes acceptable by filter. +See [Charset] for a non-empty variant.""" + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + + [[validators.types]] + name = "F" + constraint = "charset.Filter" + +[[validators]] +name = "Charset" +desc = """ +Charset accepts non-empty text which contains only runes acceptable by filter. +See also [Charset0].""" + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + + [[validators.types]] + name = "F" + constraint = "charset.Filter" + +[[validators]] +name = "Latitude" +desc = """ +Latitude accepts any number in the range [-90; 90]. + +See also [Longitude]. +""" + + [[validators.types]] + name = "T" + constraint = "constraint.Real" + +[[validators]] +name = "Longitude" +desc = """ +Longitude accepts any number in the range [-180; 180]. + +See also [Latitude]. +""" + + [[validators.types]] + name = "T" + constraint = "constraint.Real" + +[[validators]] +name = "InPast" +desc = """ +InFuture accepts any time after current timestamp. + +See also [InPast]. +""" + + [[validators.types]] + name = "T" + constraint = "constraint.Time" + +[[validators]] +name = "InFuture" +desc = """ +InFuture accepts any time after current timestamp. + +See also [InPast]. +""" + + [[validators.types]] + name = "T" + constraint = "constraint.Time" + +[[validators]] +name = "Unique" +desc = """ +Unique accepts a slice-like of unique values. + +See [UniqueSlice] for a slice shortcut. +""" + + [[validators.types]] + name = "S" + constraint = "~[]T" + + [[validators.types]] + name = "T" + constraint = "comparable" + +[[validators]] +name = "UniqueSlice" +desc = """ +Unique accepts a slice of unique values. + +See [Unique] for a more generic version. +""" +embed = "Unique[[]T, T]" +aliased = "Custom[[]T, validate.UniqueSlice[T]]" + + [[validators.types]] + name = "T" + constraint = "comparable" + +[[validators]] +name = "NonEmpty" +desc = """ +NonEmpty accepts a non-empty slice-like (len > 0). + +See [NonEmptySlice] for a slice shortcut. +""" + + [[validators.types]] + name = "S" + constraint = "~[]T" + + [[validators.types]] + name = "T" + constraint = "any" + +[[validators]] +name = "NonEmptySlice" +desc = """ +NonEmptySlice accepts a non-empty slice (len > 0). + +See [NonEmpty] for a more generic version. +""" +embed = "NonEmpty[[]T, T]" +aliased = "Custom[[]T, validate.NonEmptySlice[T]]" + + [[validators.types]] + name = "T" + constraint = "comparable" + +[[validators]] +name = "MIME" +desc = "MIME accepts RFC 1521 mime type string." + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "UUID" +desc = """ +UUID accepts a properly formatted UUID in one of the following formats: + - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + - urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + - {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} +""" + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "JSON" +desc = "JSON accepts valid json encoded text." + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "CountryAlpha2" +desc = "CountryAlpha2 accepts case-insensitive ISO 3166 2-letter country code." + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "CountryAlpha3" +desc = "CountryAlpha3 accepts case-insensitive ISO 3166 3-letter country code." + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "CountryAlpha" +desc = "CountryAlpha accepts either [CountryAlpha2] or [CountryAlpha3]." +embed = "Or[T, CountryAlpha2[T], CountryAlpha3[T]]" + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "CurrencyAlpha" +desc = "CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code." + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "LangAlpha2" +desc = "LangAlpha2 accepts case-insensitive ISO 639 2-letter language code." + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "LangAlpha3" +desc = "LangAlpha3 accepts case-insensitive ISO 639 3-letter language code." + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "LangAlpha" +desc = "LangAlpha accepts either [LangAlpha2] or [LangAlpha3]." +embed = "Or[T, LangAlpha2[T], LangAlpha3[T]]" + + [[validators.types]] + name = "T" + constraint = "constraint.Text" + +[[validators]] +name = "And" +internal = true +desc = """ +And is a meta validator that combines other validators with AND operator. +Validators are called in the same order as specified by type parameters. + +See also [Or], [Not]. +""" + + [[validators.types]] + name = "T" + constraint = "any" + + [[validators.types]] + name = "A" + constraint = "Validator[T]" + + [[validators.types]] + name = "B" + constraint = "Validator[T]" + + +[[validators]] +name = "Or" +internal = true +desc = """ +Or is a meta validator that combines other validators with OR operator. +Validators are called in the same order as type parameters. + +See also [And], [Not]. +""" + + [[validators.types]] + name = "T" + constraint = "any" + + [[validators.types]] + name = "A" + constraint = "Validator[T]" + + [[validators.types]] + name = "B" + constraint = "Validator[T]" + + +[[validators]] +name = "Not" +internal = true +desc = """ +Not is a meta validator that inverts given validator. + +See also [And], [Or]. +""" + + [[validators.types]] + name = "T" + constraint = "any" + + [[validators.types]] + name = "V" + constraint = "Validator[T]"