From bc3a409c83c65744763de86dbdf95d974b7ac491 Mon Sep 17 00:00:00 2001 From: metafates Date: Fri, 9 May 2025 17:31:39 +0300 Subject: [PATCH 01/12] add data --- validate/data.yaml | 330 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 validate/data.yaml diff --git a/validate/data.yaml b/validate/data.yaml new file mode 100644 index 0000000..74ed224 --- /dev/null +++ b/validate/data.yaml @@ -0,0 +1,330 @@ +imports: + - path: github.com/metafates/schema/constraint + pkg: constraint + + - path: github.com/metafates/schema/validate/charset + pkg: charset + +validators: + - name: Any + desc: Any accepts any value of T. + types: + - name: T + constraint: any + + - 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]. + types: + - name: T + constraint: comparable + + - name: NonZero + decs: | + 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]. + types: + - name: T + constraint: comparable + + - name: Positive + desc: | + Positive accepts all positive real numbers excluding zero. + + See [Positive0] for zero including variant. + types: + - name: T + constraint: constraint.Real + + - name: Negative + desc: | + Negative accepts all negative real numbers excluding zero. + + See [Negative0] for zero including variant. + types: + - name: T + constraint: constraint.Real + imports: github.com/metafates/schema/constraint + + - name: Positive0 + desc: | + Positive0 accepts all positive real numbers including zero. + + See [Positive] for zero excluding variant. + types: + - name: T + constraint: constraint.Real + embed: Or[T, Positive[T], Zero[T]] + + - name: Negative0 + desc: | + Negative0 accepts all negative real numbers including zero. + + See [Negative] for zero excluding variant. + types: + - name: T + constraint: constraint.Real + embed: Or[T, Negative[T], Zero[T]] + + - name: Even + desc: Even accepts integers divisible by two. + types: + - name: T + constraint: constraint.Integer + + - name: Odd + desc: Odd accepts integers not divisible by two. + types: + - name: T + constraint: constraint.Integer + + - name: Email + desc: Email accepts a single RFC 5322 address, e.g. "Barry Gibbs ". + types: + - name: T + constraint: constraint.Text + + - 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]. + types: + - name: T + constraint: constraint.Text + + - name: HTTPURL + desc: | + HTTPURL accepts a single http(s) url. + + See also [URL]. + types: + - name: T + constraint: constraint.Text + + - 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"). + types: + - name: T + constraint: constraint.Text + + - name: IPV4 + desc: IPV4 accepts an IP V4 address (e.g. "192.0.2.1"). + types: + - name: T + constraint: constraint.Text + + - 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"). + types: + - name: T + constraint: constraint.Text + + - name: MAC + desc: MAC accepts an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet IP over InfiniBand link-layer address. + types: + - name: T + constraint: constraint.Text + + - 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. + types: + - name: T + constraint: constraint.Text + + - name: Base64 + desc: Base64 accepts valid base64 encoded strings. + types: + - name: T + constraint: constraint.Text + + - name: Charset0 + desc: + Charset0 accepts (possibly empty) text which contains only runes acceptable by filter. + + See [Charset] for a non-empty variant. + types: + - name: T + constraint: constraint.Text + - name: F + constraint: charset.Filter + + - name: Charset + desc: + Charset accepts non-empty text which contains only runes acceptable by filter. + + See also [Charset0]. + types: + - name: T + constraint: constraint.Text + - name: F + constraint: charset.Filter + + - name: Latitude + desc: | + Latitude accepts any number in the range [-90; 90]. + + See also [Longitude]. + types: + - name: T + constraint: constraint.Real + + - name: Longitude + desc: | + Longitude accepts any number in the range [-180; 180]. + + See also [Latitude]. + types: + - name: T + constraint: constraint.Real + + - name: InPast + desc: | + InFuture accepts any time after current timestamp. + + See also [InPast]. + types: + - name: T + constraint: constraint.Time + + - name: InFuture + desc: | + InFuture accepts any time after current timestamp. + + See also [InPast]. + types: + - name: T + constraint: constraint.Time + + - name: Unique + desc: | + Unique accepts a slice-like of unique values. + + See [UniqueSlice] for a slice shortcut. + types: + - name: S + constraint: ~[]T + - name: T + constraint: comparable + + - name: UniqueSlice + desc: | + Unique accepts a slice of unique values. + + See [Unique] for a more generic version. + types: + - name: T + constraint: comparable + embed: Unique[[]T, T] + aliased: Custom[[]T, validate.UniqueSlice[T]] + + - name: NonEmpty + desc: | + NonEmpty accepts a non-empty slice-like (len > 0). + + See [NonEmptySlice] for a slice shortcut. + types: + - name: S + constraint: ~[]T + - name: T + constraint: any + + - name: NonEmptySlice + desc: | + NonEmptySlice accepts a non-empty slice (len > 0). + + See [NonEmpty] for a more generic version. + types: + - name: T + constraint: comparable + embed: NonEmpty[[]T, T] + aliased: Custom[[]T, validate.NonEmptySlice[T]] + + - name: MIME + desc: MIME accepts RFC 1521 mime type string. + types: + - name: T + constraint: constraint.Text + + - 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} + types: + - name: T + constraint: constraint.Text + + - name: JSON + desc: JSON accepts valid json encoded text. + types: + - name: T + constraint: constraint.Text + + - name: CountryAlpha2 + desc: CountryAlpha2 accepts case-insensitive ISO 3166 2-letter country code. + types: + - name: T + constraint: constraint.Text + + - name: CountryAlpha3 + desc: CountryAlpha3 accepts case-insensitive ISO 3166 3-letter country code. + types: + - name: T + constraint: constraint.Text + + - name: CountryAlpha + desc: CountryAlpha accepts either [CountryAlpha2] or [CountryAlpha3]. + types: + - name: T + constraint: constraint.Text + embed: Or[T, CountryAlpha2[T], CountryAlpha3[T]] + + - name: CurrencyAlpha + desc: CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code. + types: + - name: T + constraint: constraint.Text + + - name: LangAlpha2 + desc: LangAlpha2 accepts case-insesitive ISO 639 2-letter language code. + types: + - name: T + constraint: constraint.Text + + - name: LangAlpha3 + desc: LangAlpha3 accepts case-insesitive ISO 639 3-letter language code. + types: + - name: T + constraint: constraint.Text + + - name: LangAlpha + desc: LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. + types: + - name: T + constraint: constraint.Text + embed: Or[T, LangAlpha2[T], LangAlpha3[T]] From e96f8d8d7d4164913a4453eb7898d7476f2e45be Mon Sep 17 00:00:00 2001 From: metafates Date: Fri, 9 May 2025 18:45:04 +0300 Subject: [PATCH 02/12] generate data --- validate/data.yaml => data.yaml | 2 +- examples/codegen/User_schema.go | 6 +- gen.py | 204 +++++++++ optional/custom.go | 178 ++++++++ optional/optional.go | 435 ++++++------------- optional/optional_example_test.go | 14 +- required/custom.go | 120 ++++++ required/required.go | 428 +++++++----------- validate/impl.go | 393 +++++++++++++++++ validate/validators.go | 695 +++++++----------------------- 10 files changed, 1358 insertions(+), 1117 deletions(-) rename validate/data.yaml => data.yaml (99%) create mode 100644 gen.py create mode 100644 optional/custom.go create mode 100644 required/custom.go create mode 100644 validate/impl.go diff --git a/validate/data.yaml b/data.yaml similarity index 99% rename from validate/data.yaml rename to data.yaml index 74ed224..3bf189d 100644 --- a/validate/data.yaml +++ b/data.yaml @@ -27,7 +27,7 @@ validators: constraint: comparable - name: NonZero - decs: | + desc: | NonZero accepts all non-zero values. The zero value is: 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/gen.py b/gen.py new file mode 100644 index 0000000..ea52bd7 --- /dev/null +++ b/gen.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional, TYPE_CHECKING, Protocol +import yaml +from pathlib import Path + +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 + 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("data.yaml", "r") as f: + data = yaml.safe_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"], + 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 gen.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 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], pkg: str, data: Data): + 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 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 main(): + data = read("data.yaml") + + 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, "required", data) + + with Path("optional").joinpath("optional.go").open("w+") as out: + generate_aliases(out, "optional", data) + + +if __name__ == "__main__": + main() 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..6979d1c 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 gen.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-insesitive 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-insesitive 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..e40201d 100644 --- a/required/required.go +++ b/required/required.go @@ -1,280 +1,176 @@ -// 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 gen.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]] -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]] +// 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]] - // 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]] +// Positive accepts all positive real numbers excluding zero. +// +// See [Positive0] for zero including variant. +type Positive[T constraint.Real] = Custom[T, validate.Positive[T]] - // LangAlpha2 accepts case-insesitive ISO 639 3-letter language code. - LangAlpha3[T constraint.Text] = Custom[T, validate.LangAlpha3[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]] - // LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. - LangAlpha[T constraint.Text] = Custom[T, validate.LangAlpha[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]] -// 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. +// Negative0 accepts all negative real numbers including zero. // -// 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 [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]] + +// 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). +// +// 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-insesitive ISO 639 2-letter language code. +type LangAlpha2[T constraint.Text] = Custom[T, validate.LangAlpha2[T]] + +// LangAlpha3 accepts case-insesitive 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/validate/impl.go b/validate/impl.go new file mode 100644 index 0000000..b77cf4a --- /dev/null +++ b/validate/impl.go @@ -0,0 +1,393 @@ +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" +) + +type ( + // 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 + } + + 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..b616f4c 100644 --- a/validate/validators.go +++ b/validate/validators.go @@ -1,577 +1,188 @@ +// Code generated by gen.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 -} +// 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{} -func (Email[T]) Validate(value T) error { - _, err := mail.ParseAddress(string(value)) - if err != nil { - return err - } +// 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{} - return nil -} +// 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{} -func (URL[T]) Validate(value T) error { - _, err := url.Parse(string(value)) - if err != nil { - return err - } +// Base64 accepts valid base64 encoded strings. +type Base64[T constraint.Text] struct{} - 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] struct{} -func (HTTPURL[T]) Validate(value T) error { - u, err := url.Parse(string(value)) - if err != nil { - return err - } +// Charset accepts non-empty text which contains only runes acceptable by filter. +// See also [Charset0]. +type Charset[T constraint.Text, F charset.Filter] struct{} - if u.Host == "" { - return errors.New("empty host") - } +// Latitude accepts any number in the range [-90; 90]. +// +// See also [Longitude]. +type Latitude[T constraint.Real] struct{} - switch u.Scheme { - case "http", "https": - return nil +// Longitude accepts any number in the range [-180; 180]. +// +// See also [Latitude]. +type Longitude[T constraint.Real] struct{} - 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 -} +// InFuture accepts any time after current timestamp. +// +// See also [InPast]. +type InPast[T constraint.Time] struct{} -func (IPV4[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.Is4() { - 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 (IPV6[T]) Validate(value T) error { - a, err := netip.ParseAddr(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{} - if !a.Is6() { - return errors.New("ipv6 address") - } - - 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 (MAC[T]) Validate(value T) error { - _, err := net.ParseMAC(string(value)) - if err != nil { - return err - } +// MIME accepts RFC 1521 mime type string. +type MIME[T constraint.Text] struct{} - return nil -} +// 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{} -func (CIDR[T]) Validate(value T) error { - _, _, err := net.ParseCIDR(string(value)) - if err != nil { - return err - } +// JSON accepts valid json encoded text. +type JSON[T constraint.Text] struct{} - return nil -} +// CountryAlpha2 accepts case-insensitive ISO 3166 2-letter country code. +type CountryAlpha2[T constraint.Text] struct{} -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 - } +// 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 (Charset0[T, F]) Validate(value T) error { - var f F +// CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code. +type CurrencyAlpha[T constraint.Text] struct{} - for _, r := range string(value) { - if err := f.Filter(r); err != nil { - return err - } - } +// LangAlpha2 accepts case-insesitive ISO 639 2-letter language code. +type LangAlpha2[T constraint.Text] struct{} - return nil -} - -func (Charset[T, F]) Validate(value T) error { - if len(value) == 0 { - return errors.New("empty text") - } +// LangAlpha3 accepts case-insesitive ISO 639 3-letter language code. +type LangAlpha3[T constraint.Text] struct{} - return Charset0[T, F]{}.Validate(value) +// LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. +type LangAlpha[T constraint.Text] struct{ + Or[T, LangAlpha2[T], LangAlpha3[T]] } -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)) -} From 18e030ebc2db1142a3cd2db2d9ec0cbfb550ecbf Mon Sep 17 00:00:00 2001 From: metafates Date: Fri, 9 May 2025 19:57:32 +0300 Subject: [PATCH 03/12] refine --- README.md | 3 +-- optional/optional.go | 2 +- required/required.go | 3 ++- schema.go | 10 +++++++++ validators.md | 41 ++++++++++++++++++++++++++++++++++++ gen.py => validators.py | 26 ++++++++++++++++++----- data.yaml => validators.yaml | 0 7 files changed, 76 insertions(+), 9 deletions(-) create mode 100644 schema.go create mode 100644 validators.md rename gen.py => validators.py (87%) rename data.yaml => validators.yaml (100%) diff --git a/README.md b/README.md index 5cc27b7..85c5562 100644 --- a/README.md +++ b/README.md @@ -381,8 +381,7 @@ 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) +For a list of available validators see [validators](./validators.md) ## TODO diff --git a/optional/optional.go b/optional/optional.go index 6979d1c..a226c20 100644 --- a/optional/optional.go +++ b/optional/optional.go @@ -3,8 +3,8 @@ package optional import ( "github.com/metafates/schema/constraint" - "github.com/metafates/schema/validate" "github.com/metafates/schema/validate/charset" + "github.com/metafates/schema/validate" ) // Any accepts any value of T. diff --git a/required/required.go b/required/required.go index e40201d..4fd6368 100644 --- a/required/required.go +++ b/required/required.go @@ -3,8 +3,8 @@ package required import ( "github.com/metafates/schema/constraint" - "github.com/metafates/schema/validate" "github.com/metafates/schema/validate/charset" + "github.com/metafates/schema/validate" ) // Any accepts any value of T. @@ -174,3 +174,4 @@ 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/validators.md b/validators.md new file mode 100644 index 0000000..f115d11 --- /dev/null +++ b/validators.md @@ -0,0 +1,41 @@ +# Validators +| Name | Description | +| ---- | ----------- | +| `Any` | Any accepts any value of T. | +| `Zero` | 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` | 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` | Positive accepts all positive real numbers excluding zero.

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

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

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

See [Negative] for zero excluding variant. | +| `Even` | Even accepts integers divisible by two. | +| `Odd` | Odd accepts integers not divisible by two. | +| `Email` | Email accepts a single RFC 5322 address, e.g. "Barry Gibbs ". | +| `URL` | 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` | HTTPURL accepts a single http(s) url.

See also [URL]. | +| `IP` | 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` | IPV4 accepts an IP V4 address (e.g. "192.0.2.1"). | +| `IPV6` | 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` | MAC accepts an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet IP over InfiniBand link-layer address. | +| `CIDR` | 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` | Base64 accepts valid base64 encoded strings. | +| `Charset0` | Charset0 accepts (possibly empty) text which contains only runes acceptable by filter.
See [Charset] for a non-empty variant. | +| `Charset` | Charset accepts non-empty text which contains only runes acceptable by filter.
See also [Charset0]. | +| `Latitude` | Latitude accepts any number in the range [-90; 90].

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

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

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

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

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

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

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

See [NonEmpty] for a more generic version. | +| `MIME` | MIME accepts RFC 1521 mime type string. | +| `UUID` | 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` | JSON accepts valid json encoded text. | +| `CountryAlpha2` | CountryAlpha2 accepts case-insensitive ISO 3166 2-letter country code. | +| `CountryAlpha3` | CountryAlpha3 accepts case-insensitive ISO 3166 3-letter country code. | +| `CountryAlpha` | CountryAlpha accepts either [CountryAlpha2] or [CountryAlpha3]. | +| `CurrencyAlpha` | CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code. | +| `LangAlpha2` | LangAlpha2 accepts case-insesitive ISO 639 2-letter language code. | +| `LangAlpha3` | LangAlpha3 accepts case-insesitive ISO 639 3-letter language code. | +| `LangAlpha` | LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. | diff --git a/gen.py b/validators.py similarity index 87% rename from gen.py rename to validators.py index ea52bd7..68371f9 100644 --- a/gen.py +++ b/validators.py @@ -37,7 +37,7 @@ class Data: def read(path: str) -> Data: - with open("data.yaml", "r") as f: + with open(path, "r") as f: data = yaml.safe_load(f) imports_registry: dict[str, Import] = {} @@ -154,7 +154,7 @@ def generate_validators(file: SupportsWrite[str], data: Data): p() -def generate_aliases(file: SupportsWrite[str], pkg: str, data: Data): +def generate_aliases(file: SupportsWrite[str], data: Data, pkg: str): p = make_p(file) p(PREAMBLE) @@ -187,17 +187,33 @@ def generate_aliases(file: SupportsWrite[str], pkg: str, data: Data): p() +def generate_markdown(file: SupportsWrite[str], data: Data): + p = make_p(file) + + p("# Validators") + + p("| Name | Description |") + p("| ---- | ----------- |") + for v in data.validators: + desc = "
".join(v.desc.splitlines()) + + p(f"| `{v.name}` | {desc} |") + + def main(): - data = read("data.yaml") + data = read("validators.yaml") 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, "required", data) + generate_aliases(out, data, pkg="required") with Path("optional").joinpath("optional.go").open("w+") as out: - generate_aliases(out, "optional", data) + generate_aliases(out, data, pkg="optional") + + with Path("validators.md").open("w+") as out: + generate_markdown(out, data) if __name__ == "__main__": diff --git a/data.yaml b/validators.yaml similarity index 100% rename from data.yaml rename to validators.yaml From c8268ad4fc86e3cff4d728a4e2c6c08e729e2fcd Mon Sep 17 00:00:00 2001 From: metafates Date: Fri, 9 May 2025 19:59:06 +0300 Subject: [PATCH 04/12] fix typo --- README.md | 8 ++++---- validators.yaml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 85c5562..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,10 +383,6 @@ BenchmarkUnmarshalJSON/codegen/with_validation-12 45936 ns/op BenchmarkUnmarshalJSON/codegen/without_validation-12 45649 ns/op ``` -## Validators - -For a list of available validators see [validators](./validators.md) - ## TODO - [x] Support for manual construction (similar to `.parse(...)` in zod) (using codegen) diff --git a/validators.yaml b/validators.yaml index 3bf189d..72df670 100644 --- a/validators.yaml +++ b/validators.yaml @@ -311,13 +311,13 @@ validators: constraint: constraint.Text - name: LangAlpha2 - desc: LangAlpha2 accepts case-insesitive ISO 639 2-letter language code. + desc: LangAlpha2 accepts case-insensitive ISO 639 2-letter language code. types: - name: T constraint: constraint.Text - name: LangAlpha3 - desc: LangAlpha3 accepts case-insesitive ISO 639 3-letter language code. + desc: LangAlpha3 accepts case-insensitive ISO 639 3-letter language code. types: - name: T constraint: constraint.Text From 2729b8779bc282417183143f4672b671bf268bb9 Mon Sep 17 00:00:00 2001 From: metafates Date: Fri, 9 May 2025 20:00:35 +0300 Subject: [PATCH 05/12] use list --- optional/optional.go | 14 +++++++------- required/required.go | 14 +++++++------- validate/validators.go | 12 ++++++------ validators.md | 6 +++--- validators.yaml | 8 ++++---- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/optional/optional.go b/optional/optional.go index a226c20..d8caa9c 100644 --- a/optional/optional.go +++ b/optional/optional.go @@ -2,9 +2,9 @@ package optional import ( + "github.com/metafates/schema/validate" "github.com/metafates/schema/constraint" "github.com/metafates/schema/validate/charset" - "github.com/metafates/schema/validate" ) // Any accepts any value of T. @@ -145,10 +145,10 @@ type NonEmptySlice[T comparable] = Custom[[]T, validate.NonEmptySlice[T]] 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} +// - 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. @@ -166,10 +166,10 @@ 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-insesitive ISO 639 2-letter language code. +// LangAlpha2 accepts case-insensitive ISO 639 2-letter language code. type LangAlpha2[T constraint.Text] = Custom[T, validate.LangAlpha2[T]] -// LangAlpha3 accepts case-insesitive ISO 639 3-letter language code. +// 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]. diff --git a/required/required.go b/required/required.go index 4fd6368..eb50b64 100644 --- a/required/required.go +++ b/required/required.go @@ -2,9 +2,9 @@ package required import ( + "github.com/metafates/schema/validate" "github.com/metafates/schema/constraint" "github.com/metafates/schema/validate/charset" - "github.com/metafates/schema/validate" ) // Any accepts any value of T. @@ -145,10 +145,10 @@ type NonEmptySlice[T comparable] = Custom[[]T, validate.NonEmptySlice[T]] 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} +// - 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. @@ -166,10 +166,10 @@ 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-insesitive ISO 639 2-letter language code. +// LangAlpha2 accepts case-insensitive ISO 639 2-letter language code. type LangAlpha2[T constraint.Text] = Custom[T, validate.LangAlpha2[T]] -// LangAlpha3 accepts case-insesitive ISO 639 3-letter language code. +// 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]. diff --git a/validate/validators.go b/validate/validators.go index b616f4c..700e808 100644 --- a/validate/validators.go +++ b/validate/validators.go @@ -152,10 +152,10 @@ type NonEmptySlice[T comparable] struct{ type 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} +// - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// - urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +// - {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} type UUID[T constraint.Text] struct{} // JSON accepts valid json encoded text. @@ -175,10 +175,10 @@ type CountryAlpha[T constraint.Text] struct{ // CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code. type CurrencyAlpha[T constraint.Text] struct{} -// LangAlpha2 accepts case-insesitive ISO 639 2-letter language code. +// LangAlpha2 accepts case-insensitive ISO 639 2-letter language code. type LangAlpha2[T constraint.Text] struct{} -// LangAlpha3 accepts case-insesitive ISO 639 3-letter language code. +// LangAlpha3 accepts case-insensitive ISO 639 3-letter language code. type LangAlpha3[T constraint.Text] struct{} // LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. diff --git a/validators.md b/validators.md index f115d11..3586f27 100644 --- a/validators.md +++ b/validators.md @@ -30,12 +30,12 @@ | `NonEmpty` | NonEmpty accepts a non-empty slice-like (len > 0).

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

See [NonEmpty] for a more generic version. | | `MIME` | MIME accepts RFC 1521 mime type string. | -| `UUID` | 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` | 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` | JSON accepts valid json encoded text. | | `CountryAlpha2` | CountryAlpha2 accepts case-insensitive ISO 3166 2-letter country code. | | `CountryAlpha3` | CountryAlpha3 accepts case-insensitive ISO 3166 3-letter country code. | | `CountryAlpha` | CountryAlpha accepts either [CountryAlpha2] or [CountryAlpha3]. | | `CurrencyAlpha` | CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code. | -| `LangAlpha2` | LangAlpha2 accepts case-insesitive ISO 639 2-letter language code. | -| `LangAlpha3` | LangAlpha3 accepts case-insesitive ISO 639 3-letter language code. | +| `LangAlpha2` | LangAlpha2 accepts case-insensitive ISO 639 2-letter language code. | +| `LangAlpha3` | LangAlpha3 accepts case-insensitive ISO 639 3-letter language code. | | `LangAlpha` | LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. | diff --git a/validators.yaml b/validators.yaml index 72df670..fc2a13f 100644 --- a/validators.yaml +++ b/validators.yaml @@ -271,10 +271,10 @@ 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} + - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + - urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + - {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} types: - name: T constraint: constraint.Text From 1e00008c18172703c20bb108e4f2de1ee74ed2d0 Mon Sep 17 00:00:00 2001 From: metafates Date: Fri, 9 May 2025 20:03:24 +0300 Subject: [PATCH 06/12] update validators markdown file --- optional/optional.go | 4 +-- required/required.go | 4 +-- validate/validators.go | 2 +- validators.md | 76 +++++++++++++++++++++--------------------- validators.py | 3 +- 5 files changed, 45 insertions(+), 44 deletions(-) diff --git a/optional/optional.go b/optional/optional.go index d8caa9c..7c15017 100644 --- a/optional/optional.go +++ b/optional/optional.go @@ -2,9 +2,9 @@ package optional import ( - "github.com/metafates/schema/validate" - "github.com/metafates/schema/constraint" "github.com/metafates/schema/validate/charset" + "github.com/metafates/schema/constraint" + "github.com/metafates/schema/validate" ) // Any accepts any value of T. diff --git a/required/required.go b/required/required.go index eb50b64..ba0813e 100644 --- a/required/required.go +++ b/required/required.go @@ -2,9 +2,9 @@ package required import ( - "github.com/metafates/schema/validate" - "github.com/metafates/schema/constraint" "github.com/metafates/schema/validate/charset" + "github.com/metafates/schema/constraint" + "github.com/metafates/schema/validate" ) // Any accepts any value of T. diff --git a/validate/validators.go b/validate/validators.go index 700e808..5881c30 100644 --- a/validate/validators.go +++ b/validate/validators.go @@ -2,8 +2,8 @@ package validate import ( - "github.com/metafates/schema/constraint" "github.com/metafates/schema/validate/charset" + "github.com/metafates/schema/constraint" ) // Any accepts any value of T. diff --git a/validators.md b/validators.md index 3586f27..f444daf 100644 --- a/validators.md +++ b/validators.md @@ -1,41 +1,41 @@ # Validators | Name | Description | | ---- | ----------- | -| `Any` | Any accepts any value of T. | -| `Zero` | 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` | 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` | Positive accepts all positive real numbers excluding zero.

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

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

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

See [Negative] for zero excluding variant. | -| `Even` | Even accepts integers divisible by two. | -| `Odd` | Odd accepts integers not divisible by two. | -| `Email` | Email accepts a single RFC 5322 address, e.g. "Barry Gibbs ". | -| `URL` | 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` | HTTPURL accepts a single http(s) url.

See also [URL]. | -| `IP` | 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` | IPV4 accepts an IP V4 address (e.g. "192.0.2.1"). | -| `IPV6` | 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` | MAC accepts an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet IP over InfiniBand link-layer address. | -| `CIDR` | 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` | Base64 accepts valid base64 encoded strings. | -| `Charset0` | Charset0 accepts (possibly empty) text which contains only runes acceptable by filter.
See [Charset] for a non-empty variant. | -| `Charset` | Charset accepts non-empty text which contains only runes acceptable by filter.
See also [Charset0]. | -| `Latitude` | Latitude accepts any number in the range [-90; 90].

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

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

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

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

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

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

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

See [NonEmpty] for a more generic version. | -| `MIME` | MIME accepts RFC 1521 mime type string. | -| `UUID` | 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` | JSON accepts valid json encoded text. | -| `CountryAlpha2` | CountryAlpha2 accepts case-insensitive ISO 3166 2-letter country code. | -| `CountryAlpha3` | CountryAlpha3 accepts case-insensitive ISO 3166 3-letter country code. | -| `CountryAlpha` | CountryAlpha accepts either [CountryAlpha2] or [CountryAlpha3]. | -| `CurrencyAlpha` | CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code. | -| `LangAlpha2` | LangAlpha2 accepts case-insensitive ISO 639 2-letter language code. | -| `LangAlpha3` | LangAlpha3 accepts case-insensitive ISO 639 3-letter language code. | -| `LangAlpha` | LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. | +| `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]. | diff --git a/validators.py b/validators.py index 68371f9..7b7283b 100644 --- a/validators.py +++ b/validators.py @@ -196,8 +196,9 @@ def generate_markdown(file: SupportsWrite[str], data: Data): p("| ---- | ----------- |") for v in data.validators: desc = "
".join(v.desc.splitlines()) + types = list(map(lambda t: t.name, v.types)) - p(f"| `{v.name}` | {desc} |") + p(f"| `{v.name}[{', '.join(types)}]` | {desc} |") def main(): From 6689349c0f91b507c2a991405acebc8587fd4e17 Mon Sep 17 00:00:00 2001 From: metafates Date: Sat, 10 May 2025 12:33:41 +0300 Subject: [PATCH 07/12] use toml --- optional/optional.go | 2 +- required/required.go | 2 +- validate/validators.go | 2 +- validators.py | 8 +- validators.toml | 432 +++++++++++++++++++++++++++++++++++++++++ validators.yaml | 330 ------------------------------- 6 files changed, 439 insertions(+), 337 deletions(-) create mode 100644 validators.toml delete mode 100644 validators.yaml diff --git a/optional/optional.go b/optional/optional.go index 7c15017..b4583af 100644 --- a/optional/optional.go +++ b/optional/optional.go @@ -2,9 +2,9 @@ package optional import ( - "github.com/metafates/schema/validate/charset" "github.com/metafates/schema/constraint" "github.com/metafates/schema/validate" + "github.com/metafates/schema/validate/charset" ) // Any accepts any value of T. diff --git a/required/required.go b/required/required.go index ba0813e..fc2f9a7 100644 --- a/required/required.go +++ b/required/required.go @@ -2,9 +2,9 @@ package required import ( - "github.com/metafates/schema/validate/charset" "github.com/metafates/schema/constraint" "github.com/metafates/schema/validate" + "github.com/metafates/schema/validate/charset" ) // Any accepts any value of T. diff --git a/validate/validators.go b/validate/validators.go index 5881c30..700e808 100644 --- a/validate/validators.go +++ b/validate/validators.go @@ -2,8 +2,8 @@ package validate import ( - "github.com/metafates/schema/validate/charset" "github.com/metafates/schema/constraint" + "github.com/metafates/schema/validate/charset" ) // Any accepts any value of T. diff --git a/validators.py b/validators.py index 7b7283b..98c3a33 100644 --- a/validators.py +++ b/validators.py @@ -2,8 +2,8 @@ from dataclasses import dataclass from typing import Optional, TYPE_CHECKING, Protocol -import yaml from pathlib import Path +import tomllib if TYPE_CHECKING: from _typeshed import SupportsWrite @@ -37,8 +37,8 @@ class Data: def read(path: str) -> Data: - with open(path, "r") as f: - data = yaml.safe_load(f) + with open(path, "rb") as f: + data = tomllib.load(f) imports_registry: dict[str, Import] = {} @@ -202,7 +202,7 @@ def generate_markdown(file: SupportsWrite[str], data: Data): def main(): - data = read("validators.yaml") + data = read("validators.toml") with Path("validate").joinpath("validators.go").open("w+") as out: generate_validators(out, data) diff --git a/validators.toml b/validators.toml new file mode 100644 index 0000000..0eb67bf --- /dev/null +++ b/validators.toml @@ -0,0 +1,432 @@ +[[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" diff --git a/validators.yaml b/validators.yaml deleted file mode 100644 index fc2a13f..0000000 --- a/validators.yaml +++ /dev/null @@ -1,330 +0,0 @@ -imports: - - path: github.com/metafates/schema/constraint - pkg: constraint - - - path: github.com/metafates/schema/validate/charset - pkg: charset - -validators: - - name: Any - desc: Any accepts any value of T. - types: - - name: T - constraint: any - - - 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]. - types: - - name: T - constraint: comparable - - - 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]. - types: - - name: T - constraint: comparable - - - name: Positive - desc: | - Positive accepts all positive real numbers excluding zero. - - See [Positive0] for zero including variant. - types: - - name: T - constraint: constraint.Real - - - name: Negative - desc: | - Negative accepts all negative real numbers excluding zero. - - See [Negative0] for zero including variant. - types: - - name: T - constraint: constraint.Real - imports: github.com/metafates/schema/constraint - - - name: Positive0 - desc: | - Positive0 accepts all positive real numbers including zero. - - See [Positive] for zero excluding variant. - types: - - name: T - constraint: constraint.Real - embed: Or[T, Positive[T], Zero[T]] - - - name: Negative0 - desc: | - Negative0 accepts all negative real numbers including zero. - - See [Negative] for zero excluding variant. - types: - - name: T - constraint: constraint.Real - embed: Or[T, Negative[T], Zero[T]] - - - name: Even - desc: Even accepts integers divisible by two. - types: - - name: T - constraint: constraint.Integer - - - name: Odd - desc: Odd accepts integers not divisible by two. - types: - - name: T - constraint: constraint.Integer - - - name: Email - desc: Email accepts a single RFC 5322 address, e.g. "Barry Gibbs ". - types: - - name: T - constraint: constraint.Text - - - 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]. - types: - - name: T - constraint: constraint.Text - - - name: HTTPURL - desc: | - HTTPURL accepts a single http(s) url. - - See also [URL]. - types: - - name: T - constraint: constraint.Text - - - 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"). - types: - - name: T - constraint: constraint.Text - - - name: IPV4 - desc: IPV4 accepts an IP V4 address (e.g. "192.0.2.1"). - types: - - name: T - constraint: constraint.Text - - - 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"). - types: - - name: T - constraint: constraint.Text - - - name: MAC - desc: MAC accepts an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet IP over InfiniBand link-layer address. - types: - - name: T - constraint: constraint.Text - - - 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. - types: - - name: T - constraint: constraint.Text - - - name: Base64 - desc: Base64 accepts valid base64 encoded strings. - types: - - name: T - constraint: constraint.Text - - - name: Charset0 - desc: - Charset0 accepts (possibly empty) text which contains only runes acceptable by filter. - - See [Charset] for a non-empty variant. - types: - - name: T - constraint: constraint.Text - - name: F - constraint: charset.Filter - - - name: Charset - desc: - Charset accepts non-empty text which contains only runes acceptable by filter. - - See also [Charset0]. - types: - - name: T - constraint: constraint.Text - - name: F - constraint: charset.Filter - - - name: Latitude - desc: | - Latitude accepts any number in the range [-90; 90]. - - See also [Longitude]. - types: - - name: T - constraint: constraint.Real - - - name: Longitude - desc: | - Longitude accepts any number in the range [-180; 180]. - - See also [Latitude]. - types: - - name: T - constraint: constraint.Real - - - name: InPast - desc: | - InFuture accepts any time after current timestamp. - - See also [InPast]. - types: - - name: T - constraint: constraint.Time - - - name: InFuture - desc: | - InFuture accepts any time after current timestamp. - - See also [InPast]. - types: - - name: T - constraint: constraint.Time - - - name: Unique - desc: | - Unique accepts a slice-like of unique values. - - See [UniqueSlice] for a slice shortcut. - types: - - name: S - constraint: ~[]T - - name: T - constraint: comparable - - - name: UniqueSlice - desc: | - Unique accepts a slice of unique values. - - See [Unique] for a more generic version. - types: - - name: T - constraint: comparable - embed: Unique[[]T, T] - aliased: Custom[[]T, validate.UniqueSlice[T]] - - - name: NonEmpty - desc: | - NonEmpty accepts a non-empty slice-like (len > 0). - - See [NonEmptySlice] for a slice shortcut. - types: - - name: S - constraint: ~[]T - - name: T - constraint: any - - - name: NonEmptySlice - desc: | - NonEmptySlice accepts a non-empty slice (len > 0). - - See [NonEmpty] for a more generic version. - types: - - name: T - constraint: comparable - embed: NonEmpty[[]T, T] - aliased: Custom[[]T, validate.NonEmptySlice[T]] - - - name: MIME - desc: MIME accepts RFC 1521 mime type string. - types: - - name: T - constraint: constraint.Text - - - 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} - types: - - name: T - constraint: constraint.Text - - - name: JSON - desc: JSON accepts valid json encoded text. - types: - - name: T - constraint: constraint.Text - - - name: CountryAlpha2 - desc: CountryAlpha2 accepts case-insensitive ISO 3166 2-letter country code. - types: - - name: T - constraint: constraint.Text - - - name: CountryAlpha3 - desc: CountryAlpha3 accepts case-insensitive ISO 3166 3-letter country code. - types: - - name: T - constraint: constraint.Text - - - name: CountryAlpha - desc: CountryAlpha accepts either [CountryAlpha2] or [CountryAlpha3]. - types: - - name: T - constraint: constraint.Text - embed: Or[T, CountryAlpha2[T], CountryAlpha3[T]] - - - name: CurrencyAlpha - desc: CurrencyAlpha accepts case-insensitive ISO 4217 alphabetic currency code. - types: - - name: T - constraint: constraint.Text - - - name: LangAlpha2 - desc: LangAlpha2 accepts case-insensitive ISO 639 2-letter language code. - types: - - name: T - constraint: constraint.Text - - - name: LangAlpha3 - desc: LangAlpha3 accepts case-insensitive ISO 639 3-letter language code. - types: - - name: T - constraint: constraint.Text - - - name: LangAlpha - desc: LangAlpha accepts either [LangAlpha2] or [LangAlpha3]. - types: - - name: T - constraint: constraint.Text - embed: Or[T, LangAlpha2[T], LangAlpha3[T]] From f119d33bb5257e3af862d6a1532abdffd470c7a4 Mon Sep 17 00:00:00 2001 From: metafates Date: Sat, 10 May 2025 12:48:40 +0300 Subject: [PATCH 08/12] add contributing guidelines --- CONTRIBUTING.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8cc6880 --- /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 [validators/impl.go](./validators/impl.go) file to add `Validate` method for your new validator. + +Again, if you have any questions - feel free to open an issue. From dda817ddddd345e8da6bef84bc7070ab2dd07398 Mon Sep 17 00:00:00 2001 From: metafates Date: Sat, 10 May 2025 12:50:41 +0300 Subject: [PATCH 09/12] fix typo --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8cc6880..b929608 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,6 @@ After changing this file run: just generate ``` -After that change [validators/impl.go](./validators/impl.go) file to add `Validate` method for your new validator. +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. From d4e74119eb7843cdceeba16d68b40af8e9b2c059 Mon Sep 17 00:00:00 2001 From: metafates Date: Sat, 10 May 2025 13:51:23 +0300 Subject: [PATCH 10/12] sort imports --- validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validators.py b/validators.py index 98c3a33..998ff22 100644 --- a/validators.py +++ b/validators.py @@ -117,7 +117,7 @@ def generate_imports(file: SupportsWrite[str], imports: set[str]): p = make_p(file) p("import (") - for path in imports: + for path in sorted(imports): p(f'\t"{path}"') p(")") From 4c9a673b9b4b16291824412bdda89aa98c9b092d Mon Sep 17 00:00:00 2001 From: metafates Date: Sat, 10 May 2025 13:59:11 +0300 Subject: [PATCH 11/12] add meta validators to gen --- validate/impl.go | 19 ------------- validate/validators.go | 17 ++++++++++++ validators.md | 3 ++ validators.py | 4 ++- validators.toml | 63 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 20 deletions(-) diff --git a/validate/impl.go b/validate/impl.go index b77cf4a..23f2f80 100644 --- a/validate/impl.go +++ b/validate/impl.go @@ -18,25 +18,6 @@ import ( "github.com/metafates/schema/internal/uuid" ) -type ( - // 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 } diff --git a/validate/validators.go b/validate/validators.go index 700e808..5aa0fbf 100644 --- a/validate/validators.go +++ b/validate/validators.go @@ -186,3 +186,20 @@ type 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]. +type 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]. +type Or[T any, A Validator[T], B Validator[T]] struct{} + +// Not is a meta validator that inverts given validator. +// +// See also [And], [Or]. +type Not[T any, V Validator[T]] struct{} + diff --git a/validators.md b/validators.md index f444daf..85e7f90 100644 --- a/validators.md +++ b/validators.md @@ -39,3 +39,6 @@ | `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 index 998ff22..e1f9b84 100644 --- a/validators.py +++ b/validators.py @@ -24,6 +24,7 @@ class ValidatorType: @dataclass class Validator: name: str + internal: bool desc: str types: list[ValidatorType] embed: Optional[str] @@ -76,6 +77,7 @@ def read(path: str) -> Data: validator = Validator( name=entry["name"], + internal=entry.get("internal", False), desc=entry["desc"], types=types, embed=entry.get("embed"), @@ -166,7 +168,7 @@ def generate_aliases(file: SupportsWrite[str], data: Data, pkg: str): generate_imports(file, imports) - for v in data.validators: + 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)) diff --git a/validators.toml b/validators.toml index 0eb67bf..0e25979 100644 --- a/validators.toml +++ b/validators.toml @@ -430,3 +430,66 @@ 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]" From da4fb1baa2f4e8b12a4e63cf4b25c4ad6e0107de Mon Sep 17 00:00:00 2001 From: metafates Date: Sat, 10 May 2025 14:26:17 +0300 Subject: [PATCH 12/12] update generator --- justfile | 4 ++++ optional/optional.go | 2 +- required/required.go | 2 +- validate/validators.go | 2 +- validators.md | 3 +++ validators.py | 6 ++++-- 6 files changed, 14 insertions(+), 5 deletions(-) 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/optional.go b/optional/optional.go index b4583af..4a8f6ba 100644 --- a/optional/optional.go +++ b/optional/optional.go @@ -1,4 +1,4 @@ -// Code generated by gen.py; DO NOT EDIT. +// Code generated by validators.py; DO NOT EDIT. package optional import ( diff --git a/required/required.go b/required/required.go index fc2f9a7..f9da463 100644 --- a/required/required.go +++ b/required/required.go @@ -1,4 +1,4 @@ -// Code generated by gen.py; DO NOT EDIT. +// Code generated by validators.py; DO NOT EDIT. package required import ( diff --git a/validate/validators.go b/validate/validators.go index 5aa0fbf..52596da 100644 --- a/validate/validators.go +++ b/validate/validators.go @@ -1,4 +1,4 @@ -// Code generated by gen.py; DO NOT EDIT. +// Code generated by validators.py; DO NOT EDIT. package validate import ( diff --git a/validators.md b/validators.md index 85e7f90..d81816e 100644 --- a/validators.md +++ b/validators.md @@ -1,4 +1,7 @@ # Validators + +This table features all available validators. + | Name | Description | | ---- | ----------- | | `Any[T]` | Any accepts any value of T. | diff --git a/validators.py b/validators.py index e1f9b84..5768fbb 100644 --- a/validators.py +++ b/validators.py @@ -109,7 +109,7 @@ def comment(s: str) -> str: return "\n".join(commented_lines) -PREAMBLE = "// Code generated by gen.py; DO NOT EDIT." +PREAMBLE = "// Code generated by validators.py; DO NOT EDIT." def generate_imports(file: SupportsWrite[str], imports: set[str]): @@ -193,7 +193,9 @@ 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: