├── .gitignore ├── go.mod ├── go.sum ├── Taskfile.yml ├── valdo ├── array_test.go ├── enum.go ├── tuple_test.go ├── valdo.go ├── meta.go ├── localization.go ├── const.go ├── const_test.go ├── localization_test.go ├── doc.go ├── composers_test.go ├── composers.go ├── array.go ├── object_test.go ├── tuple.go ├── example_test.go ├── primitive.go ├── primitive_test.go ├── constraints.go ├── object.go ├── locales.go ├── constraints_test.go └── errors.go ├── LICENSE ├── internal └── constraints.go ├── README.md └── regexes └── regexes.go /.gitignore: -------------------------------------------------------------------------------- 1 | /old/ 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/orsinium-labs/valdo 2 | 3 | go 1.23.0 4 | 5 | require github.com/orsinium-labs/jsony v1.2.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/orsinium-labs/jsony v0.0.0-20241102161053-cea786c8fb44 h1:zb8eWy24//zn3CMjgab5MZFdBNAwtUlcK7V/SZiTsLM= 2 | github.com/orsinium-labs/jsony v0.0.0-20241102161053-cea786c8fb44/go.mod h1:QWdjM0+NmiPsj6bxGZFpo2xaZMtDqh9rc5qSVGqwQaE= 3 | github.com/orsinium-labs/jsony v1.1.0 h1:x9qRyy917qoqQ6/ZJpZ174SfQdiTVHHtQqDgfikIkGU= 4 | github.com/orsinium-labs/jsony v1.1.0/go.mod h1:QWdjM0+NmiPsj6bxGZFpo2xaZMtDqh9rc5qSVGqwQaE= 5 | github.com/orsinium-labs/jsony v1.2.0 h1:5zfzAblEqE8bK3Dw62dCc5rjXzgNkx3467LqNreWRpQ= 6 | github.com/orsinium-labs/jsony v1.2.0/go.mod h1:QWdjM0+NmiPsj6bxGZFpo2xaZMtDqh9rc5qSVGqwQaE= 7 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | version: "3" 3 | 4 | tasks: 5 | release: 6 | desc: Tag and upload release 7 | cmds: 8 | - which gh 9 | - test v{{.CLI_ARGS}} 10 | - git tag v{{.CLI_ARGS}} 11 | - git push 12 | - git push --tags 13 | - gh release create --generate-notes v{{.CLI_ARGS}} 14 | 15 | lint: 16 | desc: Run Go linters 17 | cmds: 18 | - golangci-lint run 19 | 20 | test: 21 | desc: Run go tests with coverage and timeout and without cache 22 | cmds: 23 | - go test -count 1 -cover -timeout 1s ./... 24 | 25 | all: 26 | desc: Run all tests and linters 27 | cmds: 28 | - task: lint 29 | - task: test 30 | -------------------------------------------------------------------------------- /valdo/array_test.go: -------------------------------------------------------------------------------- 1 | package valdo_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/orsinium-labs/valdo/valdo" 7 | ) 8 | 9 | func TestArray_Validate(t *testing.T) { 10 | t.Parallel() 11 | val := valdo.A(valdo.Int(valdo.Min(0)), valdo.MinItems(1)) 12 | noErr(valdo.Validate(val, []byte(`[1, 3, 4]`))) 13 | isErr[valdo.ErrIndex](valdo.Validate(val, []byte(`[1, -3, 4]`))) 14 | isErr[valdo.ErrIndex](valdo.Validate(val, []byte(`["aragorn"]`))) 15 | isErr[valdo.ErrMinItems](valdo.Validate(val, []byte(`[]`))) 16 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`"aragorn"`))) 17 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`123`))) 18 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`null`))) 19 | } 20 | 21 | func TestArray_Schema(t *testing.T) { 22 | t.Parallel() 23 | { 24 | val := valdo.A(valdo.Int(valdo.Min(0)), valdo.MinItems(1)) 25 | res := string(valdo.Schema(val)) 26 | exp := `{"type":"array","items":{"type":"integer","minimum":0},"minItems":1}` 27 | isEq(res, exp) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /valdo/enum.go: -------------------------------------------------------------------------------- 1 | package valdo 2 | 3 | import "github.com/orsinium-labs/jsony" 4 | 5 | type enum struct { 6 | values []string 7 | } 8 | 9 | // Enum requires the input value to be one of the given constants. 10 | // 11 | // https://json-schema.org/understanding-json-schema/reference/enum 12 | func Enum(vs ...string) Validator { 13 | return enum{vs} 14 | } 15 | 16 | // Validate implements [Validator]. 17 | func (v enum) Validate(data any) Error { 18 | got, err := stringValidator(data) 19 | if err != nil { 20 | return err 21 | } 22 | for _, exp := range v.values { 23 | if got == exp { 24 | return nil 25 | } 26 | } 27 | return ErrEnum{Got: got, Expected: v.values} 28 | } 29 | 30 | // Schema implements [Validator]. 31 | func (v enum) Schema() jsony.Object { 32 | values := make([]jsony.String, len(v.values)) 33 | for i, val := range v.values { 34 | values[i] = jsony.String(val) 35 | } 36 | return jsony.Object{ 37 | jsony.Field{K: "enum", V: jsony.Array[jsony.String](values)}, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /valdo/tuple_test.go: -------------------------------------------------------------------------------- 1 | package valdo_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/orsinium-labs/valdo/valdo" 7 | ) 8 | 9 | func TestTuple_Validate(t *testing.T) { 10 | t.Parallel() 11 | val := valdo.T(valdo.S(), valdo.I()) 12 | noErr(valdo.Validate(val, []byte(`["aragorn", 82]`))) 13 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`{"name": "aragorn"}`))) 14 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`null`))) 15 | isErr[valdo.ErrMinItems](valdo.Validate(val, []byte(`["aragorn"]`))) 16 | isErr[valdo.ErrMaxItems](valdo.Validate(val, []byte(`["aragorn", 82, 42]`))) 17 | isErr[valdo.ErrIndex](valdo.Validate(val, []byte(`["aragorn", "82"]`))) 18 | isErr[valdo.ErrIndex](valdo.Validate(val, []byte(`[14, 82]`))) 19 | isErr[valdo.ErrIndex](valdo.Validate(val, []byte(`[14, "aragorn"]`))) 20 | } 21 | 22 | func TestTuple_Schema(t *testing.T) { 23 | t.Parallel() 24 | val := valdo.T(valdo.S(), valdo.I()) 25 | res := string(valdo.Schema(val)) 26 | exp := `{"type":"array","items":false,"prefixItems":[{"type":"string"},{"type":"integer"}]}` 27 | isEq(res, exp) 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | 2024 Gram 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /valdo/valdo.go: -------------------------------------------------------------------------------- 1 | package valdo 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/orsinium-labs/jsony" 7 | ) 8 | 9 | // One-letter abbreviations for people living on the edge. 10 | var ( 11 | O = Object 12 | A = Array 13 | P = Property 14 | B = Bool 15 | S = String 16 | N = Null 17 | F = Float64 18 | I = Int 19 | T = Tuple 20 | ) 21 | 22 | type Validator interface { 23 | Validate(data any) Error 24 | Schema() jsony.Object 25 | } 26 | 27 | // Schema generates JSON Schema for the validator. 28 | func Schema(v Validator) []byte { 29 | return jsony.EncodeBytes(v.Schema()) 30 | } 31 | 32 | // Read the input JSON, validate it, and unmarshal into the given type. 33 | func Unmarshal[T any](v Validator, input []byte) (T, error) { 34 | var target T 35 | err := Validate(v, input) 36 | if err != nil { 37 | return target, err 38 | } 39 | err = json.Unmarshal(input, &target) 40 | return target, err 41 | } 42 | 43 | // Validate the given JSON. 44 | func Validate(v Validator, input []byte) error { 45 | if len(input) == 0 { 46 | return ErrNoInput{} 47 | } 48 | var data any 49 | err := json.Unmarshal(input, &data) 50 | if err != nil { 51 | return err 52 | } 53 | vErr := v.Validate(data) 54 | if vErr != nil { 55 | return vErr 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /valdo/meta.go: -------------------------------------------------------------------------------- 1 | package valdo 2 | 3 | import "github.com/orsinium-labs/jsony" 4 | 5 | type Meta struct { 6 | Validator Validator 7 | Comment string 8 | Title string 9 | Description string 10 | Deprecated bool 11 | Example jsony.Encoder 12 | Examples []jsony.Encoder 13 | Default jsony.Encoder 14 | } 15 | 16 | // Validate implements [Validator]. 17 | func (m Meta) Validate(data any) Error { 18 | return m.Validator.Validate(data) 19 | } 20 | 21 | // Schema implements [Validator]. 22 | func (m Meta) Schema() jsony.Object { 23 | s := m.Validator.Schema() 24 | if m.Comment != "" { 25 | s = append(s, jsony.Field{K: "$comment", V: jsony.String(m.Comment)}) 26 | } 27 | if m.Title != "" { 28 | s = append(s, jsony.Field{K: "title", V: jsony.String(m.Title)}) 29 | } 30 | if m.Description != "" { 31 | s = append(s, jsony.Field{K: "description", V: jsony.String(m.Description)}) 32 | } 33 | if m.Deprecated { 34 | s = append(s, jsony.Field{K: "deprecated", V: jsony.True}) 35 | } 36 | 37 | var examples jsony.MixedArray 38 | if m.Example != nil { 39 | examples = append(examples, m.Example) 40 | } 41 | if m.Examples != nil { 42 | examples = append(examples, m.Examples...) 43 | } 44 | if len(examples) > 0 { 45 | s = append(s, jsony.Field{K: "examples", V: examples}) 46 | } 47 | return s 48 | } 49 | -------------------------------------------------------------------------------- /valdo/localization.go: -------------------------------------------------------------------------------- 1 | package valdo 2 | 3 | import "github.com/orsinium-labs/jsony" 4 | 5 | // Locales maps language code to [Locale]. 6 | // 7 | // It can [Wrap] a [Validator] to translate error messages to the selected language. 8 | type Locales map[string]Locale 9 | 10 | type Locale map[Error]string 11 | 12 | func (ls Locales) Wrap(lang string, v Validator) Validator { 13 | locale, hasLang := ls[lang] 14 | if !hasLang { 15 | if len(lang) != 5 { 16 | return v 17 | } 18 | locale, hasLang = ls[lang[:2]] 19 | if !hasLang { 20 | return v 21 | } 22 | } 23 | return locale.Wrap(v) 24 | } 25 | 26 | func (loc Locale) Wrap(v Validator) Validator { 27 | return locVal{v: v, loc: loc} 28 | } 29 | 30 | type locVal struct { 31 | v Validator 32 | loc Locale 33 | } 34 | 35 | // Valdiate implements [Validator]. 36 | func (lv locVal) Validate(data any) Error { 37 | err := lv.v.Validate(data) 38 | if err != nil { 39 | return lv.translate(err) 40 | } 41 | return nil 42 | } 43 | 44 | // translate the given error message. 45 | func (lv locVal) translate(err Error) Error { 46 | 47 | switch e := err.(type) { 48 | case Errors: 49 | err = e.Map(lv.translate) 50 | case ErrorWrapper: 51 | err = e.Map(lv.translate) 52 | } 53 | 54 | format, found := lv.loc[err.GetDefault()] 55 | if !found { 56 | return err 57 | } 58 | return err.SetFormat(format) 59 | } 60 | 61 | // Schema implements [Validator]. 62 | func (lv locVal) Schema() jsony.Object { 63 | return lv.v.Schema() 64 | } 65 | -------------------------------------------------------------------------------- /valdo/const.go: -------------------------------------------------------------------------------- 1 | package valdo 2 | 3 | import "github.com/orsinium-labs/jsony" 4 | 5 | type constVal[T string | bool | int] struct { 6 | validator func(any) (T, Error) 7 | value T 8 | } 9 | 10 | func (p constVal[T]) Validate(raw any) Error { 11 | got, fErr := p.validator(raw) 12 | if fErr != nil { 13 | return fErr 14 | } 15 | if got != p.value { 16 | return ErrConst{Got: got, Expected: p.value} 17 | } 18 | return nil 19 | } 20 | 21 | // Schema implements [Validator]. 22 | func (p constVal[T]) Schema() jsony.Object { 23 | field := jsony.Field{K: "const", V: jsony.Detect(p.value)} 24 | return jsony.Object{field} 25 | } 26 | 27 | // DEPRECATED: Use [StringConst] instead. 28 | func Const[T ~string](value T) Validator { 29 | return StringConst(string(value)) 30 | } 31 | 32 | // StringConst restricts a value to a single string value. 33 | // 34 | // https://json-schema.org/understanding-json-schema/reference/const 35 | func StringConst(value string) Validator { 36 | return constVal[string]{ 37 | validator: stringValidator, 38 | value: value, 39 | } 40 | } 41 | 42 | // BoolConst restricts a value to a single boolean value. 43 | // 44 | // https://json-schema.org/understanding-json-schema/reference/const 45 | func BoolConst(value bool) Validator { 46 | return constVal[bool]{ 47 | validator: boolValidator, 48 | value: value, 49 | } 50 | 51 | } 52 | 53 | // IntConst restricts a value to a single integer value. 54 | // 55 | // https://json-schema.org/understanding-json-schema/reference/const 56 | func IntConst(value int) Validator { 57 | return constVal[int]{ 58 | validator: intValidator, 59 | value: value, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /valdo/const_test.go: -------------------------------------------------------------------------------- 1 | package valdo_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/orsinium-labs/valdo/valdo" 7 | ) 8 | 9 | func TestStringConst_Validate(t *testing.T) { 10 | t.Parallel() 11 | val := valdo.StringConst("hi") 12 | noErr(valdo.Validate(val, []byte(`"hi"`))) 13 | isErr[valdo.ErrConst](valdo.Validate(val, []byte(`""`))) 14 | isErr[valdo.ErrConst](valdo.Validate(val, []byte(`"hello"`))) 15 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`13`))) 16 | 17 | val = valdo.StringConst("42") 18 | noErr(valdo.Validate(val, []byte(`"42"`))) 19 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`42`))) 20 | } 21 | 22 | func TestBoolConst_Validate(t *testing.T) { 23 | t.Parallel() 24 | val := valdo.BoolConst(true) 25 | noErr(valdo.Validate(val, []byte(`true`))) 26 | isErr[valdo.ErrConst](valdo.Validate(val, []byte(`false`))) 27 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`"true"`))) 28 | 29 | val = valdo.BoolConst(false) 30 | noErr(valdo.Validate(val, []byte(`false`))) 31 | isErr[valdo.ErrConst](valdo.Validate(val, []byte(`true`))) 32 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`"false"`))) 33 | } 34 | 35 | func TestConst_Int_Validate(t *testing.T) { 36 | t.Parallel() 37 | val := valdo.IntConst(42) 38 | noErr(valdo.Validate(val, []byte(`42`))) 39 | isErr[valdo.ErrConst](valdo.Validate(val, []byte(`13`))) 40 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`"42"`))) 41 | } 42 | 43 | func TestConst_Schema(t *testing.T) { 44 | isEq(string(valdo.Schema(valdo.StringConst("hi"))), `{"const":"hi"}`) 45 | isEq(string(valdo.Schema(valdo.BoolConst(true))), `{"const":true}`) 46 | isEq(string(valdo.Schema(valdo.IntConst(13))), `{"const":13}`) 47 | } 48 | -------------------------------------------------------------------------------- /valdo/localization_test.go: -------------------------------------------------------------------------------- 1 | package valdo_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/orsinium-labs/valdo/valdo" 7 | ) 8 | 9 | func TestTranslate(t *testing.T) { 10 | t.Parallel() 11 | locales := valdo.Locales{ 12 | "ru-RU": valdo.Locale{ 13 | valdo.ErrMin{}: "значение должно быть не меньше {value}", 14 | }, 15 | } 16 | origV := valdo.Int(valdo.Min(2), valdo.Max(8)) 17 | 18 | // supported language 19 | { 20 | val := locales.Wrap("ru-RU", origV) 21 | noErr(valdo.Validate(val, []byte(`4`))) 22 | isEq(valdo.Validate(val, []byte(`1`)).Error(), "значение должно быть не меньше 2") 23 | isEq(string(valdo.Schema(val)), string(valdo.Schema(origV))) 24 | } 25 | 26 | // unknown language 27 | { 28 | val := locales.Wrap("nl-BE", origV) 29 | noErr(valdo.Validate(val, []byte(`4`))) 30 | isEq(valdo.Validate(val, []byte(`1`)).Error(), "must be greater than or equal to 2") 31 | isEq(string(valdo.Schema(val)), string(valdo.Schema(origV))) 32 | } 33 | 34 | // unknown message 35 | { 36 | val := locales.Wrap("ru-RU", origV) 37 | noErr(valdo.Validate(val, []byte(`4`))) 38 | isEq(valdo.Validate(val, []byte(`10`)).Error(), "must be less than or equal to 8") 39 | } 40 | 41 | } 42 | 43 | func TestTranslate_Recursive(t *testing.T) { 44 | t.Parallel() 45 | locales := valdo.Locales{ 46 | "ru-RU": valdo.Locale{ 47 | valdo.ErrProperty{}: "в поле {name}: {error}", 48 | valdo.ErrType{}: "значение должно иметь тип {expected}", 49 | }, 50 | } 51 | origV := valdo.Object( 52 | valdo.P("items", valdo.A(valdo.Int())), 53 | ) 54 | val := locales.Wrap("ru-RU", origV) 55 | noErr(valdo.Validate(val, []byte(`{"items": [1, 2, 3]}`))) 56 | exp := "в поле items: at 1: значение должно иметь тип integer" 57 | isEq(valdo.Validate(val, []byte(`{"items": [1, "hi", 3]}`)).Error(), exp) 58 | } 59 | -------------------------------------------------------------------------------- /valdo/doc.go: -------------------------------------------------------------------------------- 1 | // The main functions are: 2 | // 3 | // - [Validate] validates the given JSON using the validator. 4 | // - [Unmarshal] validates the JSON and unmarshals it into the given type. 5 | // - [Schema] generates JSON Schema for the validator. 6 | // 7 | // # Types 8 | // 9 | // The top-level validator defines the type of the data. 10 | // It can be a primitive type, a collection, or a composition 11 | // of multiple types. 12 | // 13 | // - Primitive types: [Bool], [Float64], [Int], [String], [Null], [Any]. 14 | // - Collections: [Array], [Object], [Map] 15 | // - Composition: [AllOf], [Not] 16 | // 17 | // # Constraints 18 | // 19 | // The types also accept a number of constraints. Either 20 | // as an argument of their constructor or as Constrain method. 21 | // 22 | // - Numeric constraints: [ExclMax], [ExclMin], [Max], [Min], [MultipleOf] 23 | // - String constraints: [MaxLen], [MinLen], [Pattern] 24 | // - Object constraints: [MaxProperties], [MinProperties], [PropertyNames] 25 | // - Array constraints: [Contains], [MaxItems], [MinItems] 26 | // 27 | // # Errors 28 | // 29 | // [Validate] returns one of the following errors: 30 | // 31 | // - [Errors] 32 | // - [ErrNoInput] 33 | // - [ErrProperty] 34 | // - [ErrIndex] 35 | // - [ErrType] 36 | // - [ErrRequired] 37 | // - [ErrUnexpected] 38 | // - [ErrNot] 39 | // 40 | // Or one of the constraint errors: 41 | // 42 | // - [ErrMultipleOf] 43 | // - [ErrMin] 44 | // - [ErrExclMin] 45 | // - [ErrMax] 46 | // - [ErrExclMax] 47 | // - [ErrMinLen] 48 | // - [ErrMaxLen] 49 | // - [ErrPattern] 50 | // - [ErrContains] 51 | // - [ErrMinItems] 52 | // - [ErrMaxItems] 53 | // - [ErrPropertyNames] 54 | // - [ErrMinProperties] 55 | // - [ErrMaxProperties] 56 | // 57 | // The errors can be translated using [Locale]. 58 | // Multiple locales can be combined in a single registry 59 | // using [Locales]. 60 | package valdo 61 | -------------------------------------------------------------------------------- /internal/constraints.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | // Copyright 2021 The Go Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | // Signed is a constraint that permits any signed integer type. 8 | // If future releases of Go add new predeclared signed integer types, 9 | // this constraint will be modified to include them. 10 | type Signed interface { 11 | ~int | ~int8 | ~int16 | ~int32 | ~int64 12 | } 13 | 14 | // Unsigned is a constraint that permits any unsigned integer type. 15 | // If future releases of Go add new predeclared unsigned integer types, 16 | // this constraint will be modified to include them. 17 | type Unsigned interface { 18 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr 19 | } 20 | 21 | // Integer is a constraint that permits any integer type. 22 | // If future releases of Go add new predeclared integer types, 23 | // this constraint will be modified to include them. 24 | type Integer interface { 25 | Signed | Unsigned 26 | } 27 | 28 | // Float is a constraint that permits any floating-point type. 29 | // If future releases of Go add new predeclared floating-point types, 30 | // this constraint will be modified to include them. 31 | type Float interface { 32 | ~float32 | ~float64 33 | } 34 | 35 | type Number interface { 36 | Integer | Float 37 | } 38 | 39 | // Complex is a constraint that permits any complex numeric type. 40 | // If future releases of Go add new predeclared complex numeric types, 41 | // this constraint will be modified to include them. 42 | type Complex interface { 43 | ~complex64 | ~complex128 44 | } 45 | 46 | // Ordered is a constraint that permits any ordered type: any type 47 | // that supports the operators < <= >= >. 48 | // If future releases of Go add new ordered types, 49 | // this constraint will be modified to include them. 50 | type Ordered interface { 51 | Number | ~string 52 | } 53 | 54 | type Primitive interface { 55 | Number | ~string | ~bool 56 | } 57 | -------------------------------------------------------------------------------- /valdo/composers_test.go: -------------------------------------------------------------------------------- 1 | package valdo_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/orsinium-labs/valdo/valdo" 7 | ) 8 | 9 | func TestAllOf(t *testing.T) { 10 | t.Parallel() 11 | val := valdo.AllOf( 12 | valdo.Int(valdo.Min(2)), 13 | valdo.Int(valdo.Max(4)), 14 | ) 15 | noErr(valdo.Validate(val, []byte(`2`))) 16 | noErr(valdo.Validate(val, []byte(`3`))) 17 | noErr(valdo.Validate(val, []byte(`4`))) 18 | isErr[valdo.ErrMin](valdo.Validate(val, []byte(`1`))) 19 | isErr[valdo.ErrMax](valdo.Validate(val, []byte(`5`))) 20 | isEq(string(valdo.Schema(val)), `{"allOf":[{"type":"integer","minimum":2},{"type":"integer","maximum":4}]}`) 21 | } 22 | 23 | func TestAnyOf(t *testing.T) { 24 | t.Parallel() 25 | val := valdo.AnyOf( 26 | valdo.Int(valdo.Min(5)), 27 | valdo.Int(valdo.Max(2)), 28 | ) 29 | noErr(valdo.Validate(val, []byte(`1`))) 30 | noErr(valdo.Validate(val, []byte(`2`))) 31 | noErr(valdo.Validate(val, []byte(`5`))) 32 | noErr(valdo.Validate(val, []byte(`5`))) 33 | isErr[valdo.ErrAnyOf](valdo.Validate(val, []byte(`3`))) 34 | isErr[valdo.ErrAnyOf](valdo.Validate(val, []byte(`4`))) 35 | isEq(string(valdo.Schema(val)), `{"anyOf":[{"type":"integer","minimum":5},{"type":"integer","maximum":2}]}`) 36 | } 37 | 38 | func TestNot(t *testing.T) { 39 | t.Parallel() 40 | val := valdo.Not( 41 | valdo.Int(valdo.Min(4)), 42 | ) 43 | noErr(valdo.Validate(val, []byte(`1`))) 44 | noErr(valdo.Validate(val, []byte(`2`))) 45 | noErr(valdo.Validate(val, []byte(`3`))) 46 | isErr[valdo.ErrNot](valdo.Validate(val, []byte(`4`))) 47 | isErr[valdo.ErrNot](valdo.Validate(val, []byte(`5`))) 48 | isEq(string(valdo.Schema(val)), `{"not":{"type":"integer","minimum":4}}`) 49 | } 50 | 51 | func TestNullable(t *testing.T) { 52 | t.Parallel() 53 | val := valdo.Nullable(valdo.Int()) 54 | noErr(valdo.Validate(val, []byte(`1`))) 55 | noErr(valdo.Validate(val, []byte(`2`))) 56 | noErr(valdo.Validate(val, []byte(`3`))) 57 | noErr(valdo.Validate(val, []byte(`null`))) 58 | isErr[valdo.ErrAnyOf](valdo.Validate(val, []byte(`false`))) 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # valdo 2 | 3 | [ [📚 docs](https://pkg.go.dev/github.com/orsinium-labs/valdo/valdo) ] [ [🐙 github](https://github.com/orsinium-labs/valdo) ] 4 | 5 | Go package for validating JSON. Can generate [JSON Schema](https://json-schema.org/overview/what-is-jsonschema) (100% compatible with [OpenAPI](https://swagger.io/specification/)), produces user-friendly errors, supports translations. 6 | 7 | You could write OpenAPI documentation by hand (which is very painful) and then use it to validate user input in your HTTP service, but then error messages are very confusing, not user-friendly, and only in English. Or you could write input validation by hand and then maintain the OpenAPI documentation separately but then the two will eventually drift and your documentation will be a lie. Valdo solves all these problems: write validation once using a real programming language, use it everywhere. 8 | 9 | Features: 10 | 11 | * Mechanism to translate error messages. 12 | * Out-of-the-box translations for some languages. 13 | * Supports the latest JSON Schema specification (2020-12). 14 | * Pure Go. 15 | * No code generation, no reflection, no unsafe code. 16 | * User-friendly error messages. 17 | * Concurrency-safe, no global state. 18 | * Strict by default, without implicit type casting. 19 | * Type-safe, thanks to generics. 20 | 21 | ## Installation 22 | 23 | ```bash 24 | go get github.com/orsinium-labs/valdo 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```go 30 | validator := valdo.Object( 31 | valdo.Property("name", valdo.String(valdo.MinLen(1))), 32 | valdo.Property("admin", valdo.Bool()), 33 | ) 34 | 35 | // validate JSON 36 | input := []byte(`{"name": "aragorn", "admin": true}`) 37 | err := valdo.Validate(validator, raw) 38 | 39 | // validate and unmarshal JSON 40 | type User struct { 41 | Name string `json:"name"` 42 | Admin bool `json:"admin"` 43 | } 44 | user, err := valdo.Unmarshal[User](validator, input) 45 | 46 | // generate JSON Schema 47 | schema := valdo.Schema(validator) 48 | ``` 49 | 50 | See [documentation](https://pkg.go.dev/github.com/orsinium-labs/valdo/valdo) for more. 51 | -------------------------------------------------------------------------------- /valdo/composers.go: -------------------------------------------------------------------------------- 1 | package valdo 2 | 3 | import "github.com/orsinium-labs/jsony" 4 | 5 | type allOf struct { 6 | vs []Validator 7 | } 8 | 9 | // AllOf requires all of the given validators to pass. 10 | func AllOf(vs ...Validator) Validator { 11 | return allOf{vs: vs} 12 | } 13 | 14 | // Validate implements [Validator]. 15 | func (n allOf) Validate(data any) Error { 16 | for _, v := range n.vs { 17 | err := v.Validate(data) 18 | if err != nil { 19 | return err 20 | } 21 | } 22 | return nil 23 | } 24 | 25 | // Schema implements [Validator]. 26 | func (n allOf) Schema() jsony.Object { 27 | ss := make(jsony.Array[jsony.Object], len(n.vs)) 28 | for i, v := range n.vs { 29 | ss[i] = v.Schema() 30 | } 31 | return jsony.Object{ 32 | jsony.Field{K: "allOf", V: ss}, 33 | } 34 | } 35 | 36 | type anyOf struct { 37 | vs []Validator 38 | } 39 | 40 | // Nullable allows null (nil) value for the given validator. 41 | func Nullable(v Validator) Validator { 42 | return AnyOf(v, Null()) 43 | } 44 | 45 | // AllOf requires at least one of the given validators to pass. 46 | func AnyOf(vs ...Validator) Validator { 47 | return anyOf{vs: vs} 48 | } 49 | 50 | // Validate implements [Validator]. 51 | func (n anyOf) Validate(data any) Error { 52 | errors := Errors{} 53 | for _, v := range n.vs { 54 | err := v.Validate(data) 55 | if err == nil { 56 | return nil 57 | } 58 | errors.Add(err) 59 | } 60 | return ErrAnyOf{Errors: errors} 61 | } 62 | 63 | // Schema implements [Validator]. 64 | func (n anyOf) Schema() jsony.Object { 65 | ss := make(jsony.Array[jsony.Object], len(n.vs)) 66 | for i, v := range n.vs { 67 | ss[i] = v.Schema() 68 | } 69 | return jsony.Object{ 70 | jsony.Field{K: "anyOf", V: ss}, 71 | } 72 | } 73 | 74 | type notType struct { 75 | v Validator 76 | } 77 | 78 | func Not(v Validator) Validator { 79 | return notType{v: v} 80 | } 81 | 82 | // Validate implements [Validator]. 83 | func (n notType) Validate(data any) Error { 84 | err := n.v.Validate(data) 85 | if err == nil { 86 | return ErrNot{} 87 | } 88 | return nil 89 | } 90 | 91 | // Schema implements [Validator]. 92 | func (n notType) Schema() jsony.Object { 93 | return jsony.Object{ 94 | jsony.Field{K: "not", V: n.v.Schema()}, 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /valdo/array.go: -------------------------------------------------------------------------------- 1 | package valdo 2 | 3 | import "github.com/orsinium-labs/jsony" 4 | 5 | // ArrayType is constructed by [Array]. 6 | type ArrayType struct { 7 | elem Validator 8 | cs []Constraint[[]any] 9 | } 10 | 11 | func Array(elem Validator, cs ...Constraint[[]any]) ArrayType { 12 | return ArrayType{ 13 | elem: elem, 14 | }.Constrain(cs...) 15 | } 16 | 17 | func (a ArrayType) Constrain(cs ...Constraint[[]any]) ArrayType { 18 | a.cs = append(a.cs, cs...) 19 | return a 20 | } 21 | 22 | // Validate implements [Validator]. 23 | func (a ArrayType) Validate(data any) Error { 24 | switch d := data.(type) { 25 | case []any: 26 | return a.validateArray(d) 27 | default: 28 | return ErrType{Got: getTypeName(data), Expected: "array"} 29 | } 30 | } 31 | 32 | func (a ArrayType) validateArray(data []any) Error { 33 | if data == nil { 34 | return ErrType{Got: "null", Expected: "array"} 35 | } 36 | res := Errors{} 37 | for i, val := range data { 38 | err := a.elem.Validate(val) 39 | if err != nil { 40 | res.Add(ErrIndex{Index: i, Err: err}) 41 | break 42 | } 43 | } 44 | for _, c := range a.cs { 45 | res.Add(c.check(data)) 46 | } 47 | return res.Flatten() 48 | } 49 | 50 | // Schema implements [Validator]. 51 | func (a ArrayType) Schema() jsony.Object { 52 | res := jsony.Object{ 53 | jsony.Field{K: "type", V: jsony.SafeString("array")}, 54 | } 55 | items := a.elem.Schema() 56 | if len(items) > 0 { 57 | res = append(res, jsony.Field{K: "items", V: items}) 58 | } 59 | for _, c := range a.cs { 60 | res = append(res, c.field) 61 | } 62 | return res 63 | } 64 | 65 | func getTypeName(v any) string { 66 | if v == nil { 67 | return "null" 68 | } 69 | switch v.(type) { 70 | case int, int8, int16, int32, int64: 71 | return "integer" 72 | case jsony.Int, jsony.Int8, jsony.Int16, jsony.Int32, jsony.Int64: 73 | return "integer" 74 | case uint, uint8, uint16, uint32, uint64, uintptr: 75 | return "unsigned integer" 76 | case jsony.UInt, jsony.UInt8, jsony.UInt16, jsony.UInt32, jsony.UInt64: 77 | return "unsigned integer" 78 | case bool, jsony.Bool: 79 | return "boolean" 80 | case string, jsony.String: 81 | return "string" 82 | case float32, float64, jsony.Float32, jsony.Float64: 83 | return "number" 84 | case map[string]any, jsony.Object: 85 | return "object" 86 | case []any, jsony.MixedArray: 87 | return "array" 88 | default: 89 | return "" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /valdo/object_test.go: -------------------------------------------------------------------------------- 1 | package valdo_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/orsinium-labs/valdo/valdo" 8 | ) 9 | 10 | func noErr(e error) { 11 | if e != nil { 12 | panic(fmt.Sprintf("unexpected error: %T: %v", e, e)) 13 | } 14 | } 15 | 16 | func isErr[T error](e error) { 17 | _, ok := e.(T) 18 | if !ok { 19 | panic(fmt.Sprintf("unexpected error type: %T: %v", e, e)) 20 | } 21 | } 22 | 23 | func isEq[T comparable](a, b T) { 24 | if a != b { 25 | fmt.Printf("%v\n", a) 26 | fmt.Printf("%v\n", b) 27 | panic(fmt.Sprintf("%v != %v", a, b)) 28 | } 29 | } 30 | 31 | func TestObject_Validate_Map(t *testing.T) { 32 | t.Parallel() 33 | val := valdo.O( 34 | valdo.P("name", valdo.S(valdo.MinLen(2))), 35 | valdo.P("admin", valdo.B()), 36 | ) 37 | noErr(valdo.Validate(val, []byte(`{"name": "aragorn", "admin": true}`))) 38 | isErr[valdo.ErrRequired](valdo.Validate(val, []byte(`{"name": "aragorn"}`))) 39 | isErr[valdo.ErrProperty](valdo.Validate(val, []byte(`{"name": "", "admin": false}`))) 40 | isErr[valdo.ErrProperty](valdo.Validate(val, []byte(`{"name": 123, "admin": false}`))) 41 | isErr[valdo.ErrProperty](valdo.Validate(val, []byte(`{"name": "aragorn", "admin":""}`))) 42 | isErr[valdo.ErrUnexpected](valdo.Validate(val, []byte(`{"name":"aragorn","admin":true,"hi":1}`))) 43 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`["aragorn"]`))) 44 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`"aragorn"`))) 45 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`123`))) 46 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`null`))) 47 | isErr[valdo.Errors](valdo.Validate(val, []byte(`{}`))) 48 | } 49 | 50 | func TestMap_Validate(t *testing.T) { 51 | t.Parallel() 52 | val := valdo.Map(valdo.Int()) 53 | noErr(valdo.Validate(val, []byte(`{"age": 13}`))) 54 | isErr[valdo.ErrProperty](valdo.Validate(val, []byte(`{"age": "13"}`))) 55 | } 56 | 57 | func TestObject_Schema(t *testing.T) { 58 | t.Parallel() 59 | { 60 | val := valdo.O( 61 | valdo.P("name", valdo.S()), 62 | ) 63 | res := string(valdo.Schema(val)) 64 | exp := `{"type":"object","properties":{"name":{"type":"string"}},"required":["name"],"additionalProperties":false}` 65 | isEq(res, exp) 66 | } 67 | 68 | { 69 | val := valdo.O( 70 | valdo.P("name", valdo.S(valdo.MinLen(2))), 71 | ) 72 | res := string(valdo.Schema(val)) 73 | exp := `{"type":"object","properties":{"name":{"type":"string","minLength":2}},"required":["name"],"additionalProperties":false}` 74 | isEq(res, exp) 75 | } 76 | 77 | { 78 | val := valdo.O( 79 | valdo.P("name", valdo.S()).Optional(), 80 | ) 81 | res := string(valdo.Schema(val)) 82 | exp := `{"type":"object","properties":{"name":{"type":"string"}},"additionalProperties":false}` 83 | isEq(res, exp) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /valdo/tuple.go: -------------------------------------------------------------------------------- 1 | package valdo 2 | 3 | import "github.com/orsinium-labs/jsony" 4 | 5 | type TupleType struct { 6 | cs []Constraint[[]any] 7 | vals []Validator 8 | extra bool 9 | extraVal Validator 10 | } 11 | 12 | var _ Validator = TupleType{} 13 | 14 | // Validate array with every element having a different schema. 15 | // 16 | // https://json-schema.org/understanding-json-schema/reference/array#tupleValidation 17 | func Tuple(vals ...Validator) TupleType { 18 | return TupleType{vals: vals} 19 | } 20 | 21 | func (t TupleType) Constrain(cs ...Constraint[[]any]) TupleType { 22 | t.cs = append(t.cs, cs...) 23 | return t 24 | } 25 | 26 | func (t TupleType) AllowExtra(v Validator) TupleType { 27 | t.extra = true 28 | t.extraVal = v 29 | return t 30 | } 31 | 32 | // Validate implements [Validator]. 33 | func (t TupleType) Validate(data any) Error { 34 | switch d := data.(type) { 35 | case []any: 36 | return t.validateArray(d) 37 | default: 38 | return ErrType{Got: getTypeName(data), Expected: "array"} 39 | } 40 | } 41 | 42 | func (t TupleType) validateArray(data []any) Error { 43 | if data == nil { 44 | return ErrType{Got: "null", Expected: "array"} 45 | } 46 | if len(data) < len(t.vals) { 47 | return ErrMinItems{Value: len(t.vals)} 48 | } 49 | if !t.extra && len(data) > len(t.vals) { 50 | return ErrMaxItems{Value: len(t.vals)} 51 | } 52 | res := Errors{} 53 | for i, validator := range t.vals { 54 | value := data[i] 55 | err := validator.Validate(value) 56 | if err != nil { 57 | res.Add(ErrIndex{Index: i, Err: err}) 58 | break 59 | } 60 | } 61 | if t.extraVal != nil { 62 | for i := len(t.vals); i < len(data); i++ { 63 | value := data[i] 64 | err := t.extraVal.Validate(value) 65 | if err != nil { 66 | res.Add(ErrIndex{Index: i, Err: err}) 67 | break 68 | } 69 | } 70 | } 71 | for _, c := range t.cs { 72 | res.Add(c.check(data)) 73 | } 74 | return res.Flatten() 75 | } 76 | 77 | // Schema implements [Validator]. 78 | func (t TupleType) Schema() jsony.Object { 79 | var items jsony.Encoder 80 | if t.extraVal != nil { 81 | items = t.extraVal.Schema() 82 | } else { 83 | items = jsony.Bool(false) 84 | } 85 | res := jsony.Object{ 86 | jsony.Field{K: "type", V: jsony.SafeString("array")}, 87 | jsony.Field{K: "items", V: items}, 88 | } 89 | if len(t.vals) > 0 { 90 | items := make([]jsony.Object, len(t.vals)) 91 | for i, validator := range t.vals { 92 | items[i] = validator.Schema() 93 | } 94 | res = append(res, jsony.Field{K: "prefixItems", V: jsony.Array[jsony.Object](items)}) 95 | } 96 | for _, c := range t.cs { 97 | res = append(res, c.field) 98 | } 99 | return res 100 | } 101 | -------------------------------------------------------------------------------- /valdo/example_test.go: -------------------------------------------------------------------------------- 1 | package valdo_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/orsinium-labs/valdo/valdo" 7 | ) 8 | 9 | func ExampleDefaultLocales() { 10 | language := "nl" 11 | original := valdo.Int() 12 | translated := valdo.DefaultLocales.Wrap(language, original) 13 | input := []byte(`"hi"`) 14 | err := valdo.Validate(translated, input) 15 | fmt.Println(err) 16 | // Output: ongeldig type: kreeg string, verwachtte integer 17 | } 18 | 19 | func ExampleLocales() { 20 | language := "nl" 21 | original := valdo.Int() 22 | locales := valdo.Locales{ 23 | "nl": valdo.Locale{ 24 | valdo.ErrType{}: "ongeldig type: kreeg {got}, verwachtte {expected}", 25 | }, 26 | } 27 | translated := locales.Wrap(language, original) 28 | input := []byte(`"hi"`) 29 | err := valdo.Validate(translated, input) 30 | fmt.Println(err) 31 | // Output: ongeldig type: kreeg string, verwachtte integer 32 | } 33 | 34 | func ExampleLocale() { 35 | original := valdo.Int() 36 | locale := valdo.Locale{ 37 | valdo.ErrType{}: "ongeldig type: kreeg {got}, verwachtte {expected}", 38 | } 39 | translated := locale.Wrap(original) 40 | input := []byte(`"hi"`) 41 | err := valdo.Validate(translated, input) 42 | fmt.Println(err) 43 | // Output: ongeldig type: kreeg string, verwachtte integer 44 | } 45 | 46 | func ExampleString() { 47 | validator := valdo.String() 48 | input := []byte(`"hi"`) 49 | err := valdo.Validate(validator, input) 50 | fmt.Println(err) 51 | // Output: 52 | } 53 | 54 | func ExampleInt() { 55 | validator := valdo.Int() 56 | input := []byte(`13`) 57 | err := valdo.Validate(validator, input) 58 | fmt.Println(err) 59 | // Output: 60 | } 61 | 62 | func ExampleBool() { 63 | validator := valdo.Bool() 64 | input := []byte(`true`) 65 | err := valdo.Validate(validator, input) 66 | fmt.Println(err) 67 | // Output: 68 | } 69 | 70 | func ExampleFloat64() { 71 | validator := valdo.Float64() 72 | input := []byte(`3.14`) 73 | err := valdo.Validate(validator, input) 74 | fmt.Println(err) 75 | // Output: 76 | } 77 | 78 | func ExampleArray() { 79 | validator := valdo.Array(valdo.Int()) 80 | input := []byte(`[3, 4, 5]`) 81 | err := valdo.Validate(validator, input) 82 | fmt.Println(err) 83 | // Output: 84 | } 85 | 86 | func ExampleObject() { 87 | validator := valdo.Object( 88 | valdo.P("age", valdo.Int()), 89 | ) 90 | input := []byte(`{"age": 42}`) 91 | err := valdo.Validate(validator, input) 92 | fmt.Println(err) 93 | // Output: 94 | } 95 | 96 | func ExampleEnum() { 97 | validator := valdo.Enum("red", "green", "blue") 98 | input := []byte(`"green"`) 99 | err := valdo.Validate(validator, input) 100 | fmt.Println(err) 101 | // Output: 102 | } 103 | 104 | func ExampleValidate() { 105 | validator := valdo.Object( 106 | valdo.P("age", valdo.Int()), 107 | ) 108 | input := []byte(`{"age": 42}`) 109 | err := valdo.Validate(validator, input) 110 | fmt.Println(err) 111 | // Output: 112 | } 113 | 114 | func ExampleSchema() { 115 | validator := valdo.Int() 116 | schema := valdo.Schema(validator) 117 | fmt.Println(string(schema)) 118 | // Output: {"type":"integer"} 119 | } 120 | -------------------------------------------------------------------------------- /valdo/primitive.go: -------------------------------------------------------------------------------- 1 | package valdo 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/orsinium-labs/jsony" 7 | "github.com/orsinium-labs/valdo/internal" 8 | ) 9 | 10 | // PrimitiveType is constructed by [Bool], [String], [Int], or [Float64]. 11 | type PrimitiveType[T internal.Primitive] struct { 12 | val func(any) (T, Error) 13 | cs []Constraint[T] 14 | name string 15 | } 16 | 17 | // Constrain adds constraints to the primitive, like [Min]. 18 | func (p PrimitiveType[T]) Constrain(cs ...Constraint[T]) PrimitiveType[T] { 19 | p.cs = append(p.cs, cs...) 20 | return p 21 | } 22 | 23 | // Validate implements [Validator]. 24 | func (p PrimitiveType[T]) Validate(raw any) Error { 25 | val, fErr := p.val(raw) 26 | if fErr != nil { 27 | return fErr 28 | } 29 | res := Errors{} 30 | for _, c := range p.cs { 31 | res.Add(c.check(val)) 32 | } 33 | return res.Flatten() 34 | } 35 | 36 | // Schema implements [Validator]. 37 | func (p PrimitiveType[T]) Schema() jsony.Object { 38 | res := jsony.Object{ 39 | jsony.Field{K: "type", V: jsony.String(p.name)}, 40 | } 41 | for _, c := range p.cs { 42 | res = append(res, c.field) 43 | } 44 | return res 45 | } 46 | 47 | // Bool maps to "boolean" in JSON and "bool" in Go. 48 | func Bool(cs ...Constraint[bool]) PrimitiveType[bool] { 49 | return PrimitiveType[bool]{ 50 | val: boolValidator, 51 | name: "boolean", 52 | }.Constrain(cs...) 53 | } 54 | 55 | func boolValidator(raw any) (bool, Error) { 56 | switch val := raw.(type) { 57 | case bool: 58 | return val, nil 59 | case jsony.Bool: 60 | return bool(val), nil 61 | case *bool: 62 | return *val, nil 63 | case *jsony.Bool: 64 | return bool(*val), nil 65 | default: 66 | return false, ErrType{Got: getTypeName(raw), Expected: "boolean"} 67 | } 68 | } 69 | 70 | // String maps to "string" in JSON and "string" in Go. 71 | func String(cs ...Constraint[string]) PrimitiveType[string] { 72 | return PrimitiveType[string]{ 73 | val: stringValidator, 74 | name: "string", 75 | }.Constrain(cs...) 76 | } 77 | 78 | func stringValidator(raw any) (string, Error) { 79 | switch val := raw.(type) { 80 | case string: 81 | return val, nil 82 | case *string: 83 | return *val, nil 84 | case jsony.String: 85 | return string(val), nil 86 | case *jsony.String: 87 | return string(*val), nil 88 | default: 89 | return "", ErrType{Got: getTypeName(raw), Expected: "string"} 90 | } 91 | } 92 | 93 | // Int maps to "number" in JSON ("integer" in JSON Schema) and "int" in Go. 94 | func Int(cs ...Constraint[int]) PrimitiveType[int] { 95 | return PrimitiveType[int]{ 96 | val: intValidator, 97 | name: "integer", 98 | }.Constrain(cs...) 99 | } 100 | 101 | func intValidator(raw any) (int, Error) { 102 | switch val := raw.(type) { 103 | case int: 104 | return val, nil 105 | case float64: 106 | if math.Floor(val) == val { 107 | return int(val), nil 108 | } 109 | return 0, ErrType{Got: "number", Expected: "integer"} 110 | case jsony.Int: 111 | return int(val), nil 112 | case *int: 113 | return *val, nil 114 | case *jsony.Int: 115 | return int(*val), nil 116 | default: 117 | return 0, ErrType{Got: getTypeName(raw), Expected: "integer"} 118 | } 119 | } 120 | 121 | // Float64 maps to "number" in JSON and "float64" in Go. 122 | func Float64(cs ...Constraint[float64]) PrimitiveType[float64] { 123 | return PrimitiveType[float64]{ 124 | val: float64Validator, 125 | name: "number", 126 | }.Constrain(cs...) 127 | } 128 | 129 | func float64Validator(raw any) (float64, Error) { 130 | switch val := raw.(type) { 131 | case float64: 132 | return val, nil 133 | case jsony.Float64: 134 | return float64(val), nil 135 | case *float64: 136 | return *val, nil 137 | case *jsony.Float64: 138 | return float64(*val), nil 139 | case int: 140 | return float64(val), nil 141 | case *int: 142 | return float64(*val), nil 143 | default: 144 | return 0, ErrType{Got: getTypeName(raw), Expected: "number"} 145 | } 146 | } 147 | 148 | // nullType is constructed by [Null]. 149 | type nullType struct{} 150 | 151 | // Null maps to "null" in JSON and "nil" in Go. 152 | func Null() Validator { 153 | return nullType{} 154 | } 155 | 156 | // Validate implements [Validator]. 157 | func (n nullType) Validate(data any) Error { 158 | if data == nil { 159 | return nil 160 | } 161 | switch val := data.(type) { 162 | case []any: 163 | if val == nil { 164 | return nil 165 | } 166 | case map[string]any: 167 | if val == nil { 168 | return nil 169 | } 170 | } 171 | return ErrType{Got: getTypeName(data), Expected: "null"} 172 | } 173 | 174 | // Schema implements [Validator]. 175 | func (n nullType) Schema() jsony.Object { 176 | return jsony.Object{ 177 | jsony.Field{K: "type", V: jsony.SafeString("null")}, 178 | } 179 | } 180 | 181 | type anyType struct{} 182 | 183 | // Any is a value of any type. 184 | func Any() Validator { 185 | return anyType{} 186 | } 187 | 188 | // Validate implements [Validator]. 189 | func (anyType) Validate(data any) Error { 190 | return nil 191 | } 192 | 193 | // Schema implements [Validator]. 194 | func (anyType) Schema() jsony.Object { 195 | return jsony.Object{} 196 | } 197 | -------------------------------------------------------------------------------- /valdo/primitive_test.go: -------------------------------------------------------------------------------- 1 | package valdo_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/orsinium-labs/valdo/valdo" 7 | ) 8 | 9 | func TestBool_Validate(t *testing.T) { 10 | t.Parallel() 11 | val := valdo.Bool() 12 | noErr(valdo.Validate(val, []byte(`true`))) 13 | noErr(valdo.Validate(val, []byte(`false`))) 14 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`1`))) 15 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`0`))) 16 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`""`))) 17 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`"hi"`))) 18 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`{}`))) 19 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`[]`))) 20 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`null`))) 21 | isErr[valdo.ErrNoInput](valdo.Validate(val, []byte(``))) 22 | } 23 | 24 | func TestInt_Validate(t *testing.T) { 25 | t.Parallel() 26 | val := valdo.Int() 27 | noErr(valdo.Validate(val, []byte(`1`))) 28 | noErr(valdo.Validate(val, []byte(`1.0`))) 29 | noErr(valdo.Validate(val, []byte(`14`))) 30 | noErr(valdo.Validate(val, []byte(`-14`))) 31 | noErr(valdo.Validate(val, []byte(`-14.0`))) 32 | noErr(valdo.Validate(val, []byte(`0`))) 33 | noErr(valdo.Validate(val, []byte(`0.0`))) 34 | noErr(valdo.Validate(val, []byte(`-0.0`))) 35 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`false`))) 36 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`3.4`))) 37 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`0.1`))) 38 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`""`))) 39 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`"hi"`))) 40 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`{}`))) 41 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`null`))) 42 | isErr[valdo.ErrNoInput](valdo.Validate(val, []byte(``))) 43 | } 44 | 45 | func TestFloat64_Validate(t *testing.T) { 46 | t.Parallel() 47 | val := valdo.Float64() 48 | noErr(valdo.Validate(val, []byte(`1`))) 49 | noErr(valdo.Validate(val, []byte(`1.0`))) 50 | noErr(valdo.Validate(val, []byte(`14`))) 51 | noErr(valdo.Validate(val, []byte(`-14`))) 52 | noErr(valdo.Validate(val, []byte(`-14.0`))) 53 | noErr(valdo.Validate(val, []byte(`0`))) 54 | noErr(valdo.Validate(val, []byte(`0.0`))) 55 | noErr(valdo.Validate(val, []byte(`-0.0`))) 56 | noErr(valdo.Validate(val, []byte(`3.4`))) 57 | noErr(valdo.Validate(val, []byte(`-3.4`))) 58 | noErr(valdo.Validate(val, []byte(`-0.1`))) 59 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`false`))) 60 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`""`))) 61 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`"hi"`))) 62 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`{}`))) 63 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`null`))) 64 | isErr[valdo.ErrNoInput](valdo.Validate(val, []byte(``))) 65 | } 66 | 67 | func TestString_Validate(t *testing.T) { 68 | t.Parallel() 69 | val := valdo.String() 70 | noErr(valdo.Validate(val, []byte(`""`))) 71 | noErr(valdo.Validate(val, []byte(`"hi"`))) 72 | noErr(valdo.Validate(val, []byte(`"hello"`))) 73 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`false`))) 74 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`1`))) 75 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`1.4`))) 76 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`[]`))) 77 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`{}`))) 78 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`["hi"]`))) 79 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`null`))) 80 | isErr[valdo.ErrNoInput](valdo.Validate(val, []byte(``))) 81 | } 82 | 83 | func TestNull_Validate(t *testing.T) { 84 | t.Parallel() 85 | val := valdo.Null() 86 | noErr(valdo.Validate(val, []byte(`null`))) 87 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`false`))) 88 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`1`))) 89 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`1.4`))) 90 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`[]`))) 91 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`{}`))) 92 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`"hi"`))) 93 | isErr[valdo.ErrType](valdo.Validate(val, []byte(`["hi"]`))) 94 | isErr[valdo.ErrNoInput](valdo.Validate(val, []byte(``))) 95 | } 96 | 97 | func TestAny_Validate(t *testing.T) { 98 | t.Parallel() 99 | val := valdo.Any() 100 | noErr(valdo.Validate(val, []byte(`null`))) 101 | noErr(valdo.Validate(val, []byte(`false`))) 102 | noErr(valdo.Validate(val, []byte(`1`))) 103 | noErr(valdo.Validate(val, []byte(`1.4`))) 104 | noErr(valdo.Validate(val, []byte(`[]`))) 105 | noErr(valdo.Validate(val, []byte(`{}`))) 106 | noErr(valdo.Validate(val, []byte(`"hi"`))) 107 | noErr(valdo.Validate(val, []byte(`["hi"]`))) 108 | isErr[valdo.ErrNoInput](valdo.Validate(val, []byte(``))) 109 | } 110 | 111 | func TestPrimitive_Schema(t *testing.T) { 112 | t.Parallel() 113 | isEq(string(valdo.Schema(valdo.Bool())), `{"type":"boolean"}`) 114 | isEq(string(valdo.Schema(valdo.String())), `{"type":"string"}`) 115 | isEq(string(valdo.Schema(valdo.Int())), `{"type":"integer"}`) 116 | isEq(string(valdo.Schema(valdo.Float64())), `{"type":"number"}`) 117 | isEq(string(valdo.Schema(valdo.Null())), `{"type":"null"}`) 118 | isEq(string(valdo.Schema(valdo.Any())), `{}`) 119 | } 120 | -------------------------------------------------------------------------------- /valdo/constraints.go: -------------------------------------------------------------------------------- 1 | package valdo 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/orsinium-labs/jsony" 7 | "github.com/orsinium-labs/valdo/internal" 8 | ) 9 | 10 | type Constraint[T any] struct { 11 | check func(T) Error 12 | field jsony.Field 13 | } 14 | 15 | func jsonyNumber[T internal.Number](v T) jsony.Encoder { 16 | switch any(v).(type) { 17 | case float32: 18 | return jsony.Float32(v) 19 | case float64: 20 | return jsony.Float64(v) 21 | default: 22 | return jsony.Int(v) 23 | } 24 | } 25 | 26 | // The value must be a multiple of the given number. 27 | // 28 | // https://json-schema.org/understanding-json-schema/reference/numeric#multiples 29 | func MultipleOf[T internal.Integer](v T) Constraint[T] { 30 | if v <= 0 { 31 | panic("the value must be positive") 32 | } 33 | c := func(f T) Error { 34 | // TODO: support float64 as well 35 | if f%v == 0 { 36 | return nil 37 | } 38 | return ErrMultipleOf{Value: v} 39 | } 40 | return Constraint[T]{ 41 | check: c, 42 | field: jsony.Field{K: "multipleOf", V: jsonyNumber(v)}, 43 | } 44 | } 45 | 46 | func Min[T internal.Number](v T) Constraint[T] { 47 | c := func(f T) Error { 48 | if f >= v { 49 | return nil 50 | } 51 | return ErrMin{Value: v} 52 | } 53 | return Constraint[T]{ 54 | check: c, 55 | field: jsony.Field{K: "minimum", V: jsonyNumber(v)}, 56 | } 57 | } 58 | 59 | func ExclMin[T internal.Number](v T) Constraint[T] { 60 | c := func(f T) Error { 61 | if f > v { 62 | return nil 63 | } 64 | return ErrExclMin{Value: v} 65 | } 66 | return Constraint[T]{ 67 | check: c, 68 | field: jsony.Field{K: "exclusiveMinimum", V: jsonyNumber(v)}, 69 | } 70 | } 71 | 72 | func Max[T internal.Number](v T) Constraint[T] { 73 | c := func(f T) Error { 74 | if f <= v { 75 | return nil 76 | } 77 | return ErrMax{Value: v} 78 | } 79 | return Constraint[T]{ 80 | check: c, 81 | field: jsony.Field{K: "maximum", V: jsonyNumber(v)}, 82 | } 83 | } 84 | 85 | func ExclMax[T internal.Number](v T) Constraint[T] { 86 | c := func(f T) Error { 87 | if f < v { 88 | return nil 89 | } 90 | return ErrExclMax{Value: v} 91 | } 92 | return Constraint[T]{ 93 | check: c, 94 | field: jsony.Field{K: "exclusiveMaximum", V: jsonyNumber(v)}, 95 | } 96 | } 97 | 98 | func MinLen(min uint) Constraint[string] { 99 | minInt := int(min) 100 | c := func(f string) Error { 101 | if len(f) >= minInt { 102 | return nil 103 | } 104 | return ErrMinLen{Value: minInt} 105 | } 106 | return Constraint[string]{ 107 | check: c, 108 | field: jsony.Field{K: "minLength", V: jsony.UInt(min)}, 109 | } 110 | } 111 | 112 | func MaxLen(min uint) Constraint[string] { 113 | minInt := int(min) 114 | c := func(f string) Error { 115 | if len(f) <= minInt { 116 | return nil 117 | } 118 | return ErrMaxLen{Value: minInt} 119 | } 120 | return Constraint[string]{ 121 | check: c, 122 | field: jsony.Field{K: "maxLength", V: jsony.UInt(min)}, 123 | } 124 | } 125 | 126 | func Pattern(r string) Constraint[string] { 127 | rex := regexp.MustCompile(r) 128 | c := func(f string) Error { 129 | if rex.MatchString(f) { 130 | return nil 131 | } 132 | return ErrPattern{} 133 | } 134 | return Constraint[string]{ 135 | check: c, 136 | field: jsony.Field{K: "pattern", V: jsony.String(r)}, 137 | } 138 | } 139 | 140 | func Contains(v Validator) Constraint[[]any] { 141 | c := func(items []any) Error { 142 | var err Error 143 | for _, item := range items { 144 | err = v.Validate(item) 145 | if err == nil { 146 | return nil 147 | } 148 | } 149 | return ErrContains{Err: err} 150 | } 151 | return Constraint[[]any]{ 152 | check: c, 153 | field: jsony.Field{K: "contains", V: v.Schema()}, 154 | } 155 | } 156 | 157 | func MinItems(min uint) Constraint[[]any] { 158 | minInt := int(min) 159 | c := func(f []any) Error { 160 | if len(f) >= minInt { 161 | return nil 162 | } 163 | return ErrMinItems{Value: minInt} 164 | } 165 | return Constraint[[]any]{ 166 | check: c, 167 | field: jsony.Field{K: "minItems", V: jsony.UInt(min)}, 168 | } 169 | } 170 | 171 | func MaxItems(min uint) Constraint[[]any] { 172 | minInt := int(min) 173 | c := func(f []any) Error { 174 | if len(f) <= minInt { 175 | return nil 176 | } 177 | return ErrMaxItems{Value: minInt} 178 | } 179 | return Constraint[[]any]{ 180 | check: c, 181 | field: jsony.Field{K: "maxItems", V: jsony.UInt(min)}, 182 | } 183 | } 184 | 185 | func PropertyNames(cs ...Constraint[string]) Constraint[map[string]any] { 186 | c := func(items map[string]any) Error { 187 | res := Errors{} 188 | for _, c := range cs { 189 | for name := range items { 190 | err := c.check(name) 191 | if err != nil { 192 | res.Add(ErrPropertyNames{Name: name, Err: err}) 193 | } 194 | } 195 | } 196 | return res.Flatten() 197 | } 198 | schema := make(jsony.Object, len(cs)) 199 | for i, c := range cs { 200 | schema[i] = c.field 201 | } 202 | return Constraint[map[string]any]{ 203 | check: c, 204 | field: jsony.Field{K: "propertyNames", V: schema}, 205 | } 206 | } 207 | 208 | func MinProperties(min uint) Constraint[map[string]any] { 209 | minInt := int(min) 210 | c := func(f map[string]any) Error { 211 | if len(f) >= minInt { 212 | return nil 213 | } 214 | return ErrMinProperties{Value: minInt} 215 | } 216 | return Constraint[map[string]any]{ 217 | check: c, 218 | field: jsony.Field{K: "minProperties", V: jsony.UInt(min)}, 219 | } 220 | } 221 | 222 | func MaxProperties(min uint) Constraint[map[string]any] { 223 | minInt := int(min) 224 | c := func(f map[string]any) Error { 225 | if len(f) <= minInt { 226 | return nil 227 | } 228 | return ErrMaxProperties{Value: minInt} 229 | } 230 | return Constraint[map[string]any]{ 231 | check: c, 232 | field: jsony.Field{K: "maxProperties", V: jsony.UInt(min)}, 233 | } 234 | } 235 | 236 | func Equals[T bool | string | int](exp T) Constraint[T] { 237 | c := func(got T) Error { 238 | if got != exp { 239 | return ErrConst{Got: got, Expected: exp} 240 | } 241 | return nil 242 | } 243 | return Constraint[T]{ 244 | check: c, 245 | field: jsony.Field{K: "const", V: jsony.Detect(exp)}, 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /valdo/object.go: -------------------------------------------------------------------------------- 1 | package valdo 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/orsinium-labs/jsony" 7 | ) 8 | 9 | // ObjectType is constructed by [Object]. 10 | type ObjectType struct { 11 | ps []PropertyType 12 | cs []Constraint[map[string]any] 13 | extra bool 14 | extraVal Validator 15 | } 16 | 17 | func Map(value Validator, cs ...Constraint[map[string]any]) ObjectType { 18 | return Object().AllowExtra(value).Constrain(cs...) 19 | } 20 | 21 | // Object maps to "object" in JSON and "struct" or "map" in Go. 22 | func Object(ps ...PropertyType) ObjectType { 23 | return ObjectType{ps: ps} 24 | } 25 | 26 | // Add constraints to the object. 27 | // 28 | // One of: 29 | // 30 | // - [PropertyNames] 31 | // - [MinProperties] 32 | // - [MaxProperties] 33 | func (obj ObjectType) Constrain(cs ...Constraint[map[string]any]) ObjectType { 34 | obj.cs = append(obj.cs, cs...) 35 | return obj 36 | } 37 | 38 | // Allow additional properties. 39 | // 40 | // The validator, if not nil, will be used to validate the additional properties. 41 | // 42 | // https://json-schema.org/understanding-json-schema/reference/object#additionalproperties 43 | func (obj ObjectType) AllowExtra(v Validator) ObjectType { 44 | obj.extra = true 45 | obj.extraVal = v 46 | return obj 47 | } 48 | 49 | // Validate implements [Validator]. 50 | func (obj ObjectType) Validate(data any) Error { 51 | switch d := data.(type) { 52 | case map[string]any: 53 | return obj.validateMap(d) 54 | default: 55 | return ErrType{Got: getTypeName(data), Expected: "object"} 56 | } 57 | } 58 | 59 | func (obj ObjectType) validateMap(data map[string]any) Error { 60 | if data == nil { 61 | return ErrType{Got: "null", Expected: "object"} 62 | } 63 | res := Errors{} 64 | handledNames := map[string]struct{}{} 65 | for _, p := range obj.ps { 66 | if p.rex != nil { 67 | for name, val := range data { 68 | if !p.rex.MatchString(name) { 69 | continue 70 | } 71 | handledNames[name] = struct{}{} 72 | res.Add(p.validate(val)) 73 | } 74 | continue 75 | } 76 | 77 | val, found := data[p.name] 78 | if !found { 79 | if !p.optional { 80 | res.Add(ErrRequired{Name: p.name}) 81 | } 82 | continue 83 | } 84 | handledNames[p.name] = struct{}{} 85 | res.Add(p.validate(val)) 86 | if len(p.depReq) > 0 { 87 | for _, name := range p.depReq { 88 | _, found := data[name] 89 | if !found { 90 | res.Add(ErrRequired{Name: name}) 91 | } 92 | } 93 | } 94 | } 95 | for _, c := range obj.cs { 96 | res.Add(c.check(data)) 97 | } 98 | if obj.extraVal != nil { 99 | for name, val := range data { 100 | _, handled := handledNames[name] 101 | if !handled { 102 | err := obj.extraVal.Validate(val) 103 | if err != nil { 104 | res.Add(ErrProperty{Name: name, Err: err}) 105 | } 106 | } 107 | } 108 | } else if !obj.extra { 109 | for name := range data { 110 | _, handled := handledNames[name] 111 | if !handled { 112 | res.Add(ErrUnexpected{Name: name}) 113 | } 114 | } 115 | } 116 | return res.Flatten() 117 | } 118 | 119 | // Schema implements [Validator]. 120 | func (obj ObjectType) Schema() jsony.Object { 121 | required := make(jsony.Array[jsony.String], 0) 122 | properties := make(jsony.UnsafeObject, 0, len(obj.ps)) 123 | patternProps := make(jsony.UnsafeObject, 0) 124 | for _, p := range obj.ps { 125 | name := jsony.String(p.name) 126 | if !p.optional { 127 | required = append(required, name) 128 | } 129 | pSchema := p.validator.Schema() 130 | if len(pSchema) > 0 { 131 | f := jsony.UnsafeField{K: name, V: pSchema} 132 | if p.rex != nil { 133 | patternProps = append(patternProps, f) 134 | } else { 135 | properties = append(properties, f) 136 | } 137 | } 138 | } 139 | res := jsony.Object{ 140 | jsony.Field{K: "type", V: jsony.SafeString("object")}, 141 | } 142 | if len(properties) > 0 { 143 | res = append(res, jsony.Field{K: "properties", V: properties}) 144 | } 145 | if len(patternProps) > 0 { 146 | res = append(res, jsony.Field{K: "patternProperties", V: patternProps}) 147 | } 148 | if len(required) != 0 { 149 | res = append(res, jsony.Field{K: "required", V: required}) 150 | } 151 | 152 | depReq := make(jsony.UnsafeObject, 0) 153 | for _, p := range obj.ps { 154 | if len(p.depReq) > 0 { 155 | val := make(jsony.Array[jsony.String], len(p.depReq)) 156 | for i, reqName := range p.depReq { 157 | val[i] = jsony.String(reqName) 158 | } 159 | f := jsony.UnsafeField{K: jsony.String(p.name), V: val} 160 | depReq = append(depReq, f) 161 | } 162 | } 163 | if len(depReq) > 0 { 164 | res = append(res, jsony.Field{K: "dependentRequired", V: depReq}) 165 | } 166 | 167 | if obj.extraVal != nil { 168 | res = append(res, jsony.Field{K: "additionalProperties", V: obj.extraVal.Schema()}) 169 | } else if !obj.extra { 170 | res = append(res, jsony.Field{K: "additionalProperties", V: jsony.False}) 171 | } 172 | for _, c := range obj.cs { 173 | res = append(res, c.field) 174 | } 175 | return res 176 | } 177 | 178 | // PropertyType is constructed by [Property]. 179 | type PropertyType struct { 180 | name string 181 | rex *regexp.Regexp 182 | validator Validator 183 | optional bool 184 | depReq []string 185 | } 186 | 187 | // Property is a key-value pair of an [Object]. 188 | func Property(name string, v Validator) PropertyType { 189 | var rex *regexp.Regexp 190 | if name != "" && name[0] == '^' { 191 | rex = regexp.MustCompile(name) 192 | } 193 | return PropertyType{name: name, validator: v, rex: rex} 194 | } 195 | 196 | // Mark the property as optional. 197 | // 198 | // By default, all properties listed in the object are required. 199 | // 200 | // https://json-schema.org/understanding-json-schema/reference/object#required 201 | func (p PropertyType) Optional() PropertyType { 202 | p.optional = true 203 | return p 204 | } 205 | 206 | // If the property is present, require also the given properties to be present. 207 | // 208 | // https://json-schema.org/understanding-json-schema/reference/conditionals#dependentRequired 209 | func (p PropertyType) AlsoRequire(name string, names ...string) PropertyType { 210 | if p.rex != nil { 211 | panic("pattern properties cannot be required") 212 | } 213 | p.depReq = append(p.depReq, name) 214 | p.depReq = append(p.depReq, names...) 215 | return p 216 | } 217 | 218 | func (p PropertyType) validate(data any) Error { 219 | err := p.validator.Validate(data) 220 | if err != nil { 221 | return ErrProperty{Name: p.name, Err: err} 222 | } 223 | return nil 224 | } 225 | -------------------------------------------------------------------------------- /regexes/regexes.go: -------------------------------------------------------------------------------- 1 | package regexes 2 | 3 | const ( 4 | ASCII = "^[\x00-\x7F]*$" 5 | ASCIIAlpha = "^[a-zA-Z]+$" 6 | ASCIIALphaNum = "^[a-zA-Z0-9]+$" 7 | Base32 = "^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=|[A-Z2-7]{8})$" 8 | Base64 = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$" 9 | Base64RawURL = "^(?:[A-Za-z0-9-_]{4})*(?:[A-Za-z0-9-_]{2,4})$" 10 | Base64URL = "^(?:[A-Za-z0-9-_]{4})*(?:[A-Za-z0-9-_]{2}==|[A-Za-z0-9-_]{3}=|[A-Za-z0-9-_]{4})$" 11 | BIC = `^[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?$` 12 | BtcAddress = `^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$` // bitcoin address 13 | BtcAddressLowerBech32 = `^bc1[02-9ac-hj-np-z]{7,76}$` // bitcoin bech32 address https://en.bitcoin.it/wiki/Bech32 14 | BtcAddressUpperBech32 = `^BC1[02-9AC-HJ-NP-Z]{7,76}$` // bitcoin bech32 address https://en.bitcoin.it/wiki/Bech32 15 | Cron = `(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})` 16 | CVE = `^CVE-(1999|2\d{3})-(0[^0]\d{2}|0\d[^0]\d{1}|0\d{2}[^0]|[1-9]{1}\d{3,})$` // CVE Format Id https://cve.mitre.org/cve/identifiers/syntaxchange.html 17 | DataURI = `^data:((?:\w+\/(?:([^;]|;[^;]).)+)?)` 18 | DnsRFC1035Label = "^[a-z]([-a-z0-9]*[a-z0-9]){0,62}$" 19 | E164 = "^\\+[1-9]?[0-9]{7,14}$" 20 | Email = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" 21 | EthAddress = `^0x[0-9a-fA-F]{40}$` 22 | EthAddressLower = `^0x[0-9a-f]{40}$` 23 | EthAddressUpper = `^0x[0-9A-F]{40}$` 24 | FQDNRFC1123 = `^([a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})(\.[a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})*?(\.[a-zA-Z]{1}[a-zA-Z0-9]{0,62})\.?$` // same as hostnameRFC1123 but must contain a non numerical TLD (possibly ending with '.') 25 | Hexadecimal = "^(0[xX])?[0-9a-fA-F]+$" 26 | HexColor = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$" 27 | HostnameRFC1123 = `^([a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62}){1}(\.[a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})*?$` // accepts hostname starting with a digit https://tools.ietf.org/html/rfc1123 28 | HostnameRFC952 = `^[a-zA-Z]([a-zA-Z0-9\-]+[\.]?)*[a-zA-Z0-9]$` // https://tools.ietf.org/html/rfc952 29 | HSL = "^hsl\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*\\)$" 30 | HSLA = "^hsla\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" 31 | HTML = `<[/]?([a-zA-Z]+).*?>` 32 | HTMLEncoded = `&#[x]?([0-9a-fA-F]{2})|(>)|(<)|(")|(&)+[;]?` 33 | ISBN10 = "^(?:[0-9]{9}X|[0-9]{10})$" 34 | ISBN13 = "^(?:(?:97(?:8|9))[0-9]{10})$" 35 | ISSN = "^(?:[0-9]{4}-[0-9]{3}[0-9X])$" 36 | JWT = "^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$" 37 | Latitude = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$" 38 | Longitude = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$" 39 | MD4 = "^[0-9a-f]{32}$" 40 | MD5 = "^[0-9a-f]{32}$" 41 | MongoDB = "^[a-f\\d]{24}$" 42 | Multibyte = "[^\x00-\x7F]" 43 | Number = "^[0-9]+$" 44 | Numeric = "^[-+]?[0-9]+(?:\\.[0-9]+)?$" 45 | PrintableASCII = "^[\x20-\x7E]*$" 46 | RGB = "^rgb\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*\\)$" 47 | RGBA = "^rgba\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" 48 | RipeMD128 = "^[0-9a-f]{32}$" 49 | RipeMD160 = "^[0-9a-f]{40}$" 50 | SemVer = `^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$` // numbered capture groups https://semver.org/ 51 | Sha256 = "^[0-9a-f]{64}$" 52 | Sha384 = "^[0-9a-f]{96}$" 53 | Sha512 = "^[0-9a-f]{128}$" 54 | SpiceDBID = `^(([a-zA-Z0-9/_|\-=+]{1,})|\*)$` 55 | SpiceDBPermission = "^([a-z][a-z0-9_]{1,62}[a-z0-9])?$" 56 | SpiceDBType = "^([a-z][a-z0-9_]{1,61}[a-z0-9]/)?[a-z][a-z0-9_]{1,62}[a-z0-9]$" 57 | SplitParams = `'[^']*'|\S+` 58 | SSN = `^[0-9]{3}[ -]?(0[1-9]|[1-9][0-9])[ -]?([1-9][0-9]{3}|[0-9][1-9][0-9]{2}|[0-9]{2}[1-9][0-9]|[0-9]{3}[1-9])$` 59 | Tiger128 = "^[0-9a-f]{32}$" 60 | Tiger160 = "^[0-9a-f]{40}$" 61 | Tiger192 = "^[0-9a-f]{48}$" 62 | ULID = "^(?i)[A-HJKMNP-TV-Z0-9]{26}$" 63 | UnicodeAlpha = "^[\\p{L}]+$" 64 | UnicodeAlphaNum = "^[\\p{L}\\p{N}]+$" 65 | URLEncoded = `^(?:[^%]|%[0-9A-Fa-f]{2})*$` 66 | UUID = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" 67 | UUID3 = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$" 68 | UUID3RFC4122 = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-3[0-9a-fA-F]{3}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" 69 | UUID4 = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" 70 | UUID4RFC4122 = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$" 71 | UUID5 = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" 72 | UUID5RFC4122 = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$" 73 | UUIDRFC4122 = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" 74 | ) 75 | -------------------------------------------------------------------------------- /valdo/locales.go: -------------------------------------------------------------------------------- 1 | package valdo 2 | 3 | // Locales registry with all locales supported out-of-the-box. 4 | var DefaultLocales = Locales{ 5 | "nl": Dutch, 6 | "ru": Russian, 7 | "de": German, 8 | "fr": French, 9 | } 10 | 11 | // The original English error mesages, for reference. 12 | var English = Locale{ 13 | ErrNoInput{}: "the input is empty", 14 | ErrProperty{}: "{name}: {error}", 15 | ErrIndex{}: "at {index}: {error}", 16 | ErrType{}: "invalid type: got {got}, expected {expected}", 17 | ErrRequired{}: "{name} is required but not found", 18 | ErrUnexpected{}: "unexpected property: {name}", 19 | ErrMultipleOf{}: "must be a multiple of {value}", 20 | ErrConst{}: `expected the value to be equal to "{expected}"`, 21 | ErrNot{}: "must not match the schema", 22 | ErrAnyOf{}: "must match any of the conditions: {errors}", 23 | ErrMin{}: "must be greater than or equal to {value}", 24 | ErrExclMin{}: "must be greater than {value}", 25 | ErrMax{}: "must be less than or equal to {value}", 26 | ErrExclMax{}: "must be less than {value}", 27 | ErrMinLen{}: "must be at least {value} characters long", 28 | ErrMaxLen{}: "must be at most {value} characters long", 29 | ErrPattern{}: "must match the pattern", 30 | ErrContains{}: "at least one item {error}", 31 | ErrMinItems{}: "must contain at least {value} items", 32 | ErrMaxItems{}: "must contain at most {value} items", 33 | ErrPropertyNames{}: "property name {name} {error}", 34 | ErrMinProperties{}: "must contain at least {value} properties", 35 | ErrMaxProperties{}: "must contain at most {value} properties", 36 | } 37 | 38 | // Dutch translation of all error messages. 39 | var Dutch = Locale{ 40 | ErrNoInput{}: "de invoer is leeg", 41 | ErrProperty{}: "{name}: {error}", 42 | ErrIndex{}: "bij {index}: {error}", 43 | ErrType{}: "ongeldig type: kreeg {got}, verwachtte {expected}", 44 | ErrRequired{}: "{name} is vereist maar niet gevonden", 45 | ErrUnexpected{}: "onverwachte eigenschap: {name}", 46 | ErrMultipleOf{}: "moet een veelvoud van {value} zijn", 47 | ErrConst{}: `verwachtte dat de waarde gelijk zou zijn aan "{expected}"`, 48 | ErrNot{}: "mag niet overeenkomen met het schema", 49 | ErrAnyOf{}: "moet aan een van de voorwaarden voldoen: {errors}", 50 | ErrMin{}: "moet groter zijn dan of gelijk aan {value}", 51 | ErrExclMin{}: "moet groter zijn dan {value}", 52 | ErrMax{}: "moet kleiner zijn dan of gelijk aan {value}", 53 | ErrExclMax{}: "moet kleiner zijn dan {value}", 54 | ErrMinLen{}: "moet minstens {value} tekens lang zijn", 55 | ErrMaxLen{}: "mag maximaal {value} tekens lang zijn", 56 | ErrPattern{}: "moet overeenkomen met het patroon", 57 | ErrContains{}: "ten minste één item {error}", 58 | ErrMinItems{}: "moet minstens {value} items bevatten", 59 | ErrMaxItems{}: "mag maximaal {value} items bevatten", 60 | ErrPropertyNames{}: "eigenschapsnaam {name} {error}", 61 | ErrMinProperties{}: "moet minstens {value} eigenschappen bevatten", 62 | ErrMaxProperties{}: "mag maximaal {value} eigenschappen bevatten", 63 | } 64 | 65 | // Russian translation of all error messages. 66 | var Russian = Locale{ 67 | ErrNoInput{}: "ввод пуст", 68 | ErrProperty{}: "{name}: {error}", 69 | ErrIndex{}: "на {index}: {error}", 70 | ErrType{}: "неверный тип: получено {got}, ожидалось {expected}", 71 | ErrRequired{}: "{name} обязателен, но не найден", 72 | ErrUnexpected{}: "неожиданное свойство: {name}", 73 | ErrMultipleOf{}: "должно быть кратным {value}", 74 | ErrConst{}: `значение должно быть равно "{expected}"`, 75 | ErrNot{}: "не должно соответствовать схеме", 76 | ErrAnyOf{}: "должно соответствовать одному из условий: {errors}", 77 | ErrMin{}: "должно быть больше или равно {value}", 78 | ErrExclMin{}: "должно быть больше {value}", 79 | ErrMax{}: "должно быть меньше или равно {value}", 80 | ErrExclMax{}: "должно быть меньше {value}", 81 | ErrMinLen{}: "должно содержать как минимум {value} символов", 82 | ErrMaxLen{}: "должно содержать не более {value} символов", 83 | ErrPattern{}: "должно соответствовать шаблону", 84 | ErrContains{}: "как минимум один элемент {error}", 85 | ErrMinItems{}: "должно содержать как минимум {value} элементов", 86 | ErrMaxItems{}: "должно содержать не более {value} элементов", 87 | ErrPropertyNames{}: "имя свойства {name} {error}", 88 | ErrMinProperties{}: "должно содержать как минимум {value} свойств", 89 | ErrMaxProperties{}: "должно содержать не более {value} свойств", 90 | } 91 | 92 | // German translation of all error messages. 93 | var German = Locale{ 94 | ErrNoInput{}: "Die Eingabe ist leer", 95 | ErrProperty{}: "{name}: {error}", 96 | ErrIndex{}: "bei {index}: {error}", 97 | ErrType{}: "Ungültiger Typ: erhalten {got}, erwartet {expected}", 98 | ErrRequired{}: "{name} ist erforderlich, wurde aber nicht gefunden", 99 | ErrUnexpected{}: "Unerwartete Eigenschaft: {name}", 100 | ErrMultipleOf{}: "Muss ein Vielfaches von {value} sein", 101 | ErrConst{}: `erwartet, dass der Wert gleich "{expected}" ist`, 102 | ErrNot{}: "Darf nicht dem Schema entsprechen", 103 | ErrAnyOf{}: "muss eine der Bedingungen erfüllen: {errors}", 104 | ErrMin{}: "Muss größer oder gleich {value} sein", 105 | ErrExclMin{}: "Muss größer als {value} sein", 106 | ErrMax{}: "Muss kleiner oder gleich {value} sein", 107 | ErrExclMax{}: "Muss kleiner als {value} sein", 108 | ErrMinLen{}: "Muss mindestens {value} Zeichen lang sein", 109 | ErrMaxLen{}: "Darf höchstens {value} Zeichen lang sein", 110 | ErrPattern{}: "Muss dem Muster entsprechen", 111 | ErrContains{}: "Mindestens ein Element {error}", 112 | ErrMinItems{}: "Muss mindestens {value} Elemente enthalten", 113 | ErrMaxItems{}: "Darf höchstens {value} Elemente enthalten", 114 | ErrPropertyNames{}: "Eigenschaftsname {name} {error}", 115 | ErrMinProperties{}: "Muss mindestens {value} Eigenschaften enthalten", 116 | ErrMaxProperties{}: "Darf höchstens {value} Eigenschaften enthalten", 117 | } 118 | 119 | // French translation of all error messages. 120 | var French = Locale{ 121 | ErrNoInput{}: "l'entrée est vide", 122 | ErrProperty{}: "{name}: {error}", 123 | ErrIndex{}: "à {index}: {error}", 124 | ErrType{}: "type invalide : reçu {got}, attendu {expected}", 125 | ErrRequired{}: "{name} est requis mais non trouvé", 126 | ErrUnexpected{}: "propriété inattendue : {name}", 127 | ErrMultipleOf{}: "doit être un multiple de {value}", 128 | ErrConst{}: "on s'attendait à ce que la valeur soit égale à «{expected}»", 129 | ErrNot{}: "ne doit pas correspondre au schéma", 130 | ErrAnyOf{}: "doit correspondre à l'une des conditions : {errors}", 131 | ErrMin{}: "doit être supérieur ou égal à {value}", 132 | ErrExclMin{}: "doit être supérieur à {value}", 133 | ErrMax{}: "doit être inférieur ou égal à {value}", 134 | ErrExclMax{}: "doit être inférieur à {value}", 135 | ErrMinLen{}: "doit contenir au moins {value} caractères", 136 | ErrMaxLen{}: "doit contenir au maximum {value} caractères", 137 | ErrPattern{}: "doit correspondre au modèle", 138 | ErrContains{}: "au moins un élément {error}", 139 | ErrMinItems{}: "doit contenir au moins {value} éléments", 140 | ErrMaxItems{}: "doit contenir au maximum {value} éléments", 141 | ErrPropertyNames{}: "nom de la propriété {name} {error}", 142 | ErrMinProperties{}: "doit contenir au moins {value} propriétés", 143 | ErrMaxProperties{}: "doit contenir au maximum {value} propriétés", 144 | } 145 | -------------------------------------------------------------------------------- /valdo/constraints_test.go: -------------------------------------------------------------------------------- 1 | package valdo_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/orsinium-labs/valdo/valdo" 7 | ) 8 | 9 | func TestMultipleOf(t *testing.T) { 10 | t.Parallel() 11 | val := valdo.Int(valdo.MultipleOf(3)) 12 | noErr(valdo.Validate(val, []byte(`0`))) 13 | noErr(valdo.Validate(val, []byte(`3`))) 14 | noErr(valdo.Validate(val, []byte(`6`))) 15 | noErr(valdo.Validate(val, []byte(`-6`))) 16 | isErr[valdo.ErrMultipleOf](valdo.Validate(val, []byte(`1`))) 17 | isErr[valdo.ErrMultipleOf](valdo.Validate(val, []byte(`-1`))) 18 | isErr[valdo.ErrMultipleOf](valdo.Validate(val, []byte(`2`))) 19 | isErr[valdo.ErrMultipleOf](valdo.Validate(val, []byte(`-2`))) 20 | isErr[valdo.ErrMultipleOf](valdo.Validate(val, []byte(`10`))) 21 | isErr[valdo.ErrMultipleOf](valdo.Validate(val, []byte(`-10`))) 22 | 23 | isEq(string(valdo.Schema(val)), `{"type":"integer","multipleOf":3}`) 24 | } 25 | 26 | func TestMin(t *testing.T) { 27 | t.Parallel() 28 | val := valdo.Int(valdo.Min(3)) 29 | noErr(valdo.Validate(val, []byte(`3`))) 30 | noErr(valdo.Validate(val, []byte(`4`))) 31 | noErr(valdo.Validate(val, []byte(`6`))) 32 | isErr[valdo.ErrMin](valdo.Validate(val, []byte(`2`))) 33 | isErr[valdo.ErrMin](valdo.Validate(val, []byte(`0`))) 34 | isErr[valdo.ErrMin](valdo.Validate(val, []byte(`-6`))) 35 | 36 | isEq(string(valdo.Schema(val)), `{"type":"integer","minimum":3}`) 37 | } 38 | 39 | func TestExclMin(t *testing.T) { 40 | t.Parallel() 41 | val := valdo.Int(valdo.ExclMin(3)) 42 | noErr(valdo.Validate(val, []byte(`4`))) 43 | noErr(valdo.Validate(val, []byte(`6`))) 44 | isErr[valdo.ErrExclMin](valdo.Validate(val, []byte(`3`))) 45 | isErr[valdo.ErrExclMin](valdo.Validate(val, []byte(`2`))) 46 | isErr[valdo.ErrExclMin](valdo.Validate(val, []byte(`0`))) 47 | isErr[valdo.ErrExclMin](valdo.Validate(val, []byte(`-6`))) 48 | 49 | isEq(string(valdo.Schema(val)), `{"type":"integer","exclusiveMinimum":3}`) 50 | } 51 | 52 | func TestMax(t *testing.T) { 53 | t.Parallel() 54 | val := valdo.Int(valdo.Max(3)) 55 | noErr(valdo.Validate(val, []byte(`3`))) 56 | noErr(valdo.Validate(val, []byte(`2`))) 57 | noErr(valdo.Validate(val, []byte(`-6`))) 58 | isErr[valdo.ErrMax](valdo.Validate(val, []byte(`4`))) 59 | isErr[valdo.ErrMax](valdo.Validate(val, []byte(`5`))) 60 | isErr[valdo.ErrMax](valdo.Validate(val, []byte(`16`))) 61 | 62 | isEq(string(valdo.Schema(val)), `{"type":"integer","maximum":3}`) 63 | } 64 | 65 | func TestExclMax(t *testing.T) { 66 | t.Parallel() 67 | val := valdo.Int(valdo.ExclMax(3)) 68 | noErr(valdo.Validate(val, []byte(`2`))) 69 | noErr(valdo.Validate(val, []byte(`-6`))) 70 | isErr[valdo.ErrExclMax](valdo.Validate(val, []byte(`3`))) 71 | isErr[valdo.ErrExclMax](valdo.Validate(val, []byte(`4`))) 72 | isErr[valdo.ErrExclMax](valdo.Validate(val, []byte(`5`))) 73 | isErr[valdo.ErrExclMax](valdo.Validate(val, []byte(`16`))) 74 | 75 | isEq(string(valdo.Schema(val)), `{"type":"integer","exclusiveMaximum":3}`) 76 | } 77 | 78 | func TestMinLen(t *testing.T) { 79 | t.Parallel() 80 | val := valdo.String(valdo.MinLen(3)) 81 | noErr(valdo.Validate(val, []byte(`"hel"`))) 82 | noErr(valdo.Validate(val, []byte(`"hell"`))) 83 | noErr(valdo.Validate(val, []byte(`"hello"`))) 84 | isErr[valdo.ErrMinLen](valdo.Validate(val, []byte(`"hi"`))) 85 | isErr[valdo.ErrMinLen](valdo.Validate(val, []byte(`"!"`))) 86 | isErr[valdo.ErrMinLen](valdo.Validate(val, []byte(`""`))) 87 | 88 | isEq(string(valdo.Schema(val)), `{"type":"string","minLength":3}`) 89 | } 90 | 91 | func TestMaxLen(t *testing.T) { 92 | t.Parallel() 93 | val := valdo.String(valdo.MaxLen(3)) 94 | noErr(valdo.Validate(val, []byte(`"hel"`))) 95 | noErr(valdo.Validate(val, []byte(`"hi"`))) 96 | noErr(valdo.Validate(val, []byte(`"!"`))) 97 | noErr(valdo.Validate(val, []byte(`""`))) 98 | isErr[valdo.ErrMaxLen](valdo.Validate(val, []byte(`"hell"`))) 99 | isErr[valdo.ErrMaxLen](valdo.Validate(val, []byte(`"hello"`))) 100 | 101 | isEq(string(valdo.Schema(val)), `{"type":"string","maxLength":3}`) 102 | } 103 | 104 | func TestPattern(t *testing.T) { 105 | t.Parallel() 106 | val := valdo.String(valdo.Pattern("[AB]")) 107 | noErr(valdo.Validate(val, []byte(`"A"`))) 108 | noErr(valdo.Validate(val, []byte(`"B"`))) 109 | noErr(valdo.Validate(val, []byte(`"Bxx"`))) 110 | noErr(valdo.Validate(val, []byte(`"xxB"`))) 111 | noErr(valdo.Validate(val, []byte(`"xxBxx"`))) 112 | isErr[valdo.ErrPattern](valdo.Validate(val, []byte(`"xxxx"`))) 113 | isErr[valdo.ErrPattern](valdo.Validate(val, []byte(`"hello"`))) 114 | isErr[valdo.ErrPattern](valdo.Validate(val, []byte(`""`))) 115 | 116 | isEq(string(valdo.Schema(val)), `{"type":"string","pattern":"[AB]"}`) 117 | } 118 | 119 | func TestContains(t *testing.T) { 120 | t.Parallel() 121 | val := valdo.Array(valdo.Any(), valdo.Contains(valdo.Int())) 122 | noErr(valdo.Validate(val, []byte(`[1, 2, 3]`))) 123 | noErr(valdo.Validate(val, []byte(`["", 2]`))) 124 | noErr(valdo.Validate(val, []byte(`[2, ""]`))) 125 | noErr(valdo.Validate(val, []byte(`[2, "", 3]`))) 126 | isErr[valdo.ErrContains](valdo.Validate(val, []byte(`[]`))) 127 | isErr[valdo.ErrContains](valdo.Validate(val, []byte(`[""]`))) 128 | isErr[valdo.ErrContains](valdo.Validate(val, []byte(`["", 2.3]`))) 129 | isErr[valdo.ErrContains](valdo.Validate(val, []byte(`[true, 2.3]`))) 130 | isErr[valdo.ErrContains](valdo.Validate(val, []byte(`[true]`))) 131 | 132 | isEq(string(valdo.Schema(val)), `{"type":"array","contains":{"type":"integer"}}`) 133 | } 134 | 135 | func TestMinItems(t *testing.T) { 136 | t.Parallel() 137 | val := valdo.Array(valdo.Any(), valdo.MinItems(3)) 138 | noErr(valdo.Validate(val, []byte(`[5, 6, 7]`))) 139 | noErr(valdo.Validate(val, []byte(`[5, 6, 7, 8]`))) 140 | isErr[valdo.ErrMinItems](valdo.Validate(val, []byte(`[]`))) 141 | isErr[valdo.ErrMinItems](valdo.Validate(val, []byte(`[5]`))) 142 | isErr[valdo.ErrMinItems](valdo.Validate(val, []byte(`[5, 6]`))) 143 | 144 | isEq(string(valdo.Schema(val)), `{"type":"array","minItems":3}`) 145 | } 146 | 147 | func TestMaxItems(t *testing.T) { 148 | t.Parallel() 149 | val := valdo.Array(valdo.Any(), valdo.MaxItems(3)) 150 | noErr(valdo.Validate(val, []byte(`[]`))) 151 | noErr(valdo.Validate(val, []byte(`[6]`))) 152 | noErr(valdo.Validate(val, []byte(`[6, 7]`))) 153 | noErr(valdo.Validate(val, []byte(`[6, 7, 8]`))) 154 | isErr[valdo.ErrMaxItems](valdo.Validate(val, []byte(`[5, 6, 7, 8]`))) 155 | isErr[valdo.ErrMaxItems](valdo.Validate(val, []byte(`[5, 6, 7, 8, 9]`))) 156 | 157 | isEq(string(valdo.Schema(val)), `{"type":"array","maxItems":3}`) 158 | } 159 | 160 | func TestPropertyNames(t *testing.T) { 161 | t.Parallel() 162 | val := valdo.Map(nil, valdo.PropertyNames(valdo.MaxLen(2))) 163 | noErr(valdo.Validate(val, []byte(`{}`))) 164 | noErr(valdo.Validate(val, []byte(`{"hi": 1, "h": 2}`))) 165 | isErr[valdo.ErrPropertyNames](valdo.Validate(val, []byte(`{"hel": 1, "h": 2}`))) 166 | isErr[valdo.ErrPropertyNames](valdo.Validate(val, []byte(`{"h": 1, "hel": 2}`))) 167 | isErr[valdo.ErrPropertyNames](valdo.Validate(val, []byte(`{"hello": 1, "h": 2}`))) 168 | isErr[valdo.ErrPropertyNames](valdo.Validate(val, []byte(`{"hello": 1}`))) 169 | 170 | isEq(string(valdo.Schema(val)), `{"type":"object","propertyNames":{"maxLength":2}}`) 171 | } 172 | 173 | func TestMinProperties(t *testing.T) { 174 | t.Parallel() 175 | val := valdo.Map(nil, valdo.MinProperties(2)) 176 | noErr(valdo.Validate(val, []byte(`{"a": 1, "b": 2}`))) 177 | noErr(valdo.Validate(val, []byte(`{"a": 1, "b": 2, "c": 3}`))) 178 | isErr[valdo.ErrMinProperties](valdo.Validate(val, []byte(`{"hel": 1}`))) 179 | isErr[valdo.ErrMinProperties](valdo.Validate(val, []byte(`{}`))) 180 | 181 | isEq(string(valdo.Schema(val)), `{"type":"object","minProperties":2}`) 182 | } 183 | 184 | func TestMaxProperties(t *testing.T) { 185 | t.Parallel() 186 | val := valdo.Map(nil, valdo.MaxProperties(2)) 187 | noErr(valdo.Validate(val, []byte(`{"a": 1, "b": 2}`))) 188 | noErr(valdo.Validate(val, []byte(`{"a": 1}`))) 189 | noErr(valdo.Validate(val, []byte(`{}`))) 190 | isErr[valdo.ErrMaxProperties](valdo.Validate(val, []byte(`{"a": 1, "b": 2, "c": 3}`))) 191 | 192 | isEq(string(valdo.Schema(val)), `{"type":"object","maxProperties":2}`) 193 | } 194 | -------------------------------------------------------------------------------- /valdo/errors.go: -------------------------------------------------------------------------------- 1 | package valdo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Error interface { 9 | error 10 | // GetDefault implements [Error] interface. 11 | GetDefault() Error 12 | // SetFormat implements [Error] interface. 13 | SetFormat(f string) Error 14 | } 15 | 16 | // ErrorWrapper is an [Error] that wraps another error. 17 | type ErrorWrapper interface { 18 | Error 19 | // Unwrap makes it possible for errors.Unwrap function to access the wrapped error. 20 | Unwrap() error 21 | 22 | // Map applies the given function to the inner function and returns it, wrapped. 23 | // 24 | // Yes, it's a monad! In Go! What a day. 25 | Map(func(Error) Error) Error 26 | } 27 | 28 | var ( 29 | _ ErrorWrapper = ErrProperty{} 30 | _ ErrorWrapper = ErrIndex{} 31 | _ ErrorWrapper = ErrContains{} 32 | _ ErrorWrapper = ErrPropertyNames{} 33 | _ ErrorWrapper = ErrAnyOf{} 34 | ) 35 | 36 | type pair struct { 37 | name string 38 | value any 39 | } 40 | 41 | // Format substitutes values into a format string with python-style placeholders. 42 | func format(f string, pairs ...pair) string { 43 | args := make([]string, 0, len(pairs)*2) 44 | for _, p := range pairs { 45 | args = append(args, "{"+p.name+"}") 46 | args = append(args, fmt.Sprintf("%v", p.value)) 47 | } 48 | return strings.NewReplacer(args...).Replace(f) 49 | } 50 | 51 | // A collection of multiple errors. 52 | type Errors struct { 53 | Sep string 54 | Errs []Error 55 | } 56 | 57 | // Add the given error (if not nil) to the list of errors. 58 | func (es *Errors) Add(err Error) { 59 | if err != nil { 60 | es.Errs = append(es.Errs, err) 61 | } 62 | } 63 | 64 | func (es Errors) Flatten() Error { 65 | if len(es.Errs) == 0 { 66 | return nil 67 | } 68 | if len(es.Errs) == 1 { 69 | return es.Errs[0] 70 | } 71 | return es 72 | } 73 | 74 | // GetDefault implements [Error] interface. 75 | func (es Errors) GetDefault() Error { 76 | return Errors{} 77 | } 78 | 79 | // SetFormat implements [Error] interface. 80 | func (es Errors) SetFormat(f string) Error { 81 | es.Sep = f 82 | return es 83 | } 84 | 85 | // Error implements [error] interface. 86 | func (es Errors) Error() string { 87 | res := make([]string, len(es.Errs)) 88 | for i, e := range es.Errs { 89 | res[i] = e.Error() 90 | } 91 | sep := es.Sep 92 | if sep == "" { 93 | sep = "; " 94 | } 95 | return strings.Join(res, sep) 96 | } 97 | 98 | // Map applies the given function to the inner function and returns it, wrapped. 99 | // 100 | // It partially implements [ErrorWrapper]: Errors doesn't satisfy the interface 101 | // because [Errors.Unwrap] returns a list of errrors instead of a single error. 102 | func (e Errors) Map(f func(Error) Error) Error { 103 | errors := make([]Error, len(e.Errs)) 104 | for i, sub := range e.Errs { 105 | errors[i] = f(sub) 106 | } 107 | e.Errs = errors 108 | return e 109 | } 110 | 111 | // Unwrap makes errors.Is work. 112 | // 113 | // Note that since it returns a list of errors instead of a single error, 114 | // errors.Unwrap will NOT work. 115 | func (es Errors) Unwrap() []error { 116 | res := make([]error, len(es.Errs)) 117 | for i, subErr := range es.Errs { 118 | res[i] = subErr 119 | } 120 | return res 121 | } 122 | 123 | // An error in a field of an object. 124 | type ErrNoInput struct { 125 | Format string 126 | } 127 | 128 | // GetDefault implements [Error] interface. 129 | func (e ErrNoInput) GetDefault() Error { 130 | return ErrNoInput{} 131 | } 132 | 133 | // SetFormat implements [Error] interface. 134 | func (e ErrNoInput) SetFormat(f string) Error { 135 | e.Format = f 136 | return e 137 | } 138 | 139 | // Error implements [error] interface. 140 | func (e ErrNoInput) Error() string { 141 | f := e.Format 142 | if f == "" { 143 | f = "the input is empty" 144 | } 145 | return f 146 | } 147 | 148 | // An error in a field of an object. 149 | // 150 | // Returned by an [Object] validator. 151 | type ErrProperty struct { 152 | Format string 153 | Name string 154 | Err Error 155 | } 156 | 157 | // GetDefault implements [Error] interface. 158 | func (e ErrProperty) GetDefault() Error { 159 | return ErrProperty{} 160 | } 161 | 162 | // SetFormat implements [Error] interface. 163 | func (e ErrProperty) SetFormat(f string) Error { 164 | e.Format = f 165 | return e 166 | } 167 | 168 | // Error implements [error] interface. 169 | func (e ErrProperty) Error() string { 170 | f := e.Format 171 | if f == "" { 172 | f = "{name}: {error}" 173 | } 174 | return format(f, pair{"name", e.Name}, pair{"error", e.Err}) 175 | } 176 | 177 | // Unwrap implements [ErrorWrapper] interface. 178 | func (e ErrProperty) Unwrap() error { 179 | return e.Err 180 | } 181 | 182 | // Map implements [ErrorWrapper] interface. 183 | func (e ErrProperty) Map(f func(Error) Error) Error { 184 | e.Err = f(e.Err) 185 | return e 186 | } 187 | 188 | // An error in an element of an array. 189 | // 190 | // Returned by an [Array] validator. 191 | type ErrIndex struct { 192 | Format string 193 | Index int 194 | Err Error 195 | } 196 | 197 | // GetDefault implements [Error] interface. 198 | func (e ErrIndex) GetDefault() Error { 199 | return ErrIndex{} 200 | } 201 | 202 | // SetFormat implements [Error] interface. 203 | func (e ErrIndex) SetFormat(f string) Error { 204 | e.Format = f 205 | return e 206 | } 207 | 208 | // Error implements [error] interface. 209 | func (e ErrIndex) Error() string { 210 | f := e.Format 211 | if f == "" { 212 | f = "at {index}: {error}" 213 | } 214 | return format(f, pair{"index", e.Index}, pair{"error", e.Err}) 215 | } 216 | 217 | // Unwrap implements [ErrorWrapper] interface. 218 | func (e ErrIndex) Unwrap() error { 219 | return e.Err 220 | } 221 | 222 | // Map implements [ErrorWrapper] interface. 223 | func (e ErrIndex) Map(f func(Error) Error) Error { 224 | e.Err = f(e.Err) 225 | return e 226 | } 227 | 228 | // An error indicating a value of an unexpected type. 229 | type ErrType struct { 230 | Format string 231 | Got string 232 | Expected string 233 | } 234 | 235 | // GetDefault implements [Error] interface. 236 | func (e ErrType) GetDefault() Error { 237 | return ErrType{} 238 | } 239 | 240 | // SetFormat implements [Error] interface. 241 | func (e ErrType) SetFormat(f string) Error { 242 | e.Format = f 243 | return e 244 | } 245 | 246 | // Error implements [error] interface. 247 | func (e ErrType) Error() string { 248 | f := e.Format 249 | if f == "" { 250 | f = "invalid type: got {got}, expected {expected}" 251 | } 252 | got := e.Got 253 | if got == "" { 254 | got = "unknown type" 255 | } 256 | return format(f, pair{"got", got}, pair{"expected", e.Expected}) 257 | } 258 | 259 | // An error indicating that a value is required but not found. 260 | // 261 | // Returned by an [Object] validator. 262 | type ErrRequired struct { 263 | Format string 264 | Name string 265 | } 266 | 267 | // GetDefault implements [Error] interface. 268 | func (e ErrRequired) GetDefault() Error { 269 | return ErrRequired{} 270 | } 271 | 272 | // SetFormat implements [Error] interface. 273 | func (e ErrRequired) SetFormat(f string) Error { 274 | e.Format = f 275 | return e 276 | } 277 | 278 | // Error implements [error] interface. 279 | func (e ErrRequired) Error() string { 280 | f := e.Format 281 | if f == "" { 282 | f = "{name} is required but not found" 283 | } 284 | return format(f, pair{"name", e.Name}) 285 | } 286 | 287 | // An error indicating that the property is not allowed. 288 | // 289 | // Returned by an [Object] validator. 290 | type ErrUnexpected struct { 291 | Format string 292 | Name string 293 | } 294 | 295 | // GetDefault implements [Error] interface. 296 | func (e ErrUnexpected) GetDefault() Error { 297 | return ErrUnexpected{} 298 | } 299 | 300 | // SetFormat implements [Error] interface. 301 | func (e ErrUnexpected) SetFormat(f string) Error { 302 | e.Format = f 303 | return e 304 | } 305 | 306 | // Error implements [error] interface. 307 | func (e ErrUnexpected) Error() string { 308 | f := e.Format 309 | if f == "" { 310 | f = "unexpected property: {name}" 311 | } 312 | return format(f, pair{"name", e.Name}) 313 | } 314 | 315 | // An error indicating that the value isn't equal to the expected constant. 316 | // 317 | // Returned by [StringConst], [IntConst], and [BoolConst] validators. 318 | type ErrConst struct { 319 | Format string 320 | Got any 321 | Expected any 322 | } 323 | 324 | // GetDefault implements [Error] interface. 325 | func (e ErrConst) GetDefault() Error { 326 | return ErrConst{} 327 | } 328 | 329 | // SetFormat implements [Error] interface. 330 | func (e ErrConst) SetFormat(f string) Error { 331 | e.Format = f 332 | return e 333 | } 334 | 335 | // Error implements [error] interface. 336 | func (e ErrConst) Error() string { 337 | f := e.Format 338 | if f == "" { 339 | f = `expected the value to be equal to "{expected}"` 340 | } 341 | return format(f, pair{"expected", e.Expected}) 342 | } 343 | 344 | // An error indicating that the value isn't equal to any of the allowed values. 345 | // 346 | // Returned by the [Enum] validator. 347 | type ErrEnum struct { 348 | Format string 349 | Got string 350 | Expected []string 351 | } 352 | 353 | // GetDefault implements [Error] interface. 354 | func (e ErrEnum) GetDefault() Error { 355 | return ErrEnum{} 356 | } 357 | 358 | // SetFormat implements [Error] interface. 359 | func (e ErrEnum) SetFormat(f string) Error { 360 | e.Format = f 361 | return e 362 | } 363 | 364 | // Error implements [error] interface. 365 | func (e ErrEnum) Error() string { 366 | f := e.Format 367 | if f == "" { 368 | f = `expected the value to be one of: {expected}` 369 | } 370 | return format(f, pair{"expected", strings.Join(e.Expected, ", ")}) 371 | } 372 | 373 | // A constraint error returned by [MultipleOf]. 374 | type ErrMultipleOf struct { 375 | Format string 376 | Value any 377 | } 378 | 379 | // GetDefault implements [Error] interface. 380 | func (e ErrMultipleOf) GetDefault() Error { 381 | return ErrMultipleOf{} 382 | } 383 | 384 | // SetFormat implements [Error] interface. 385 | func (e ErrMultipleOf) SetFormat(f string) Error { 386 | e.Format = f 387 | return e 388 | } 389 | 390 | // Error implements [error] interface. 391 | func (e ErrMultipleOf) Error() string { 392 | f := e.Format 393 | if f == "" { 394 | f = "must be a multiple of {value}" 395 | } 396 | return format(f, pair{"value", e.Value}) 397 | } 398 | 399 | // An error returned by [Not] validator. 400 | type ErrNot struct { 401 | Format string 402 | } 403 | 404 | // GetDefault implements [Error] interface. 405 | func (e ErrNot) GetDefault() Error { 406 | return ErrNot{} 407 | } 408 | 409 | // SetFormat implements [Error] interface. 410 | func (e ErrNot) SetFormat(f string) Error { 411 | e.Format = f 412 | return e 413 | } 414 | 415 | // Error implements [error] interface. 416 | func (e ErrNot) Error() string { 417 | f := e.Format 418 | if f == "" { 419 | f = "must not match the schema" 420 | } 421 | return f 422 | } 423 | 424 | // An error returned by [AnyOf] validator. 425 | type ErrAnyOf struct { 426 | Format string 427 | Errors Error 428 | } 429 | 430 | // Map implements [ErrorWrapper] interface. 431 | func (e ErrAnyOf) Map(f func(Error) Error) Error { 432 | oldErrors := e.Errors.(Errors).Errs 433 | errors := make([]Error, len(oldErrors)) 434 | for i, sub := range oldErrors { 435 | errors[i] = f(sub) 436 | } 437 | e.Errors = Errors{Errs: errors} 438 | return e 439 | } 440 | 441 | // Unwrap implements [ErrorWrapper] interface. 442 | func (e ErrAnyOf) Unwrap() error { 443 | return e.Errors 444 | } 445 | 446 | // GetDefault implements [Error] interface. 447 | func (e ErrAnyOf) GetDefault() Error { 448 | return ErrAnyOf{} 449 | } 450 | 451 | // SetFormat implements [Error] interface. 452 | func (e ErrAnyOf) SetFormat(f string) Error { 453 | e.Format = f 454 | return e 455 | } 456 | 457 | // Error implements [error] interface. 458 | func (e ErrAnyOf) Error() string { 459 | f := e.Format 460 | if f == "" { 461 | f = "must match any of the conditions: {errors}" 462 | } 463 | return format(f, pair{"errors", e.Errors}) 464 | } 465 | 466 | // A constraint error returned by [Min]. 467 | type ErrMin struct { 468 | Format string 469 | Value any 470 | } 471 | 472 | // GetDefault implements [Error] interface. 473 | func (e ErrMin) GetDefault() Error { 474 | return ErrMin{} 475 | } 476 | 477 | // SetFormat implements [Error] interface. 478 | func (e ErrMin) SetFormat(f string) Error { 479 | e.Format = f 480 | return e 481 | } 482 | 483 | // Error implements [error] interface. 484 | func (e ErrMin) Error() string { 485 | f := e.Format 486 | if f == "" { 487 | f = "must be greater than or equal to {value}" 488 | } 489 | return format(f, pair{"value", e.Value}) 490 | } 491 | 492 | // A constraint error returned by [ExclMin]. 493 | type ErrExclMin struct { 494 | Format string 495 | Value any 496 | } 497 | 498 | // GetDefault implements [Error] interface. 499 | func (e ErrExclMin) GetDefault() Error { 500 | return ErrExclMin{} 501 | } 502 | 503 | // SetFormat implements [Error] interface. 504 | func (e ErrExclMin) SetFormat(f string) Error { 505 | e.Format = f 506 | return e 507 | } 508 | 509 | // Error implements [error] interface. 510 | func (e ErrExclMin) Error() string { 511 | f := e.Format 512 | if f == "" { 513 | f = "must be greater than {value}" 514 | } 515 | return format(f, pair{"value", e.Value}) 516 | } 517 | 518 | // A constraint error returned by [Max]. 519 | type ErrMax struct { 520 | Format string 521 | Value any 522 | } 523 | 524 | // GetDefault implements [Error] interface. 525 | func (e ErrMax) GetDefault() Error { 526 | return ErrMax{} 527 | } 528 | 529 | // SetFormat implements [Error] interface. 530 | func (e ErrMax) SetFormat(f string) Error { 531 | e.Format = f 532 | return e 533 | } 534 | 535 | // Error implements [error] interface. 536 | func (e ErrMax) Error() string { 537 | f := e.Format 538 | if f == "" { 539 | f = "must be less than or equal to {value}" 540 | } 541 | return format(f, pair{"value", e.Value}) 542 | } 543 | 544 | // A constraint error returned by [ExclMax]. 545 | type ErrExclMax struct { 546 | Format string 547 | Value any 548 | } 549 | 550 | // GetDefault implements [Error] interface. 551 | func (e ErrExclMax) GetDefault() Error { 552 | return ErrExclMax{} 553 | } 554 | 555 | // SetFormat implements [Error] interface. 556 | func (e ErrExclMax) SetFormat(f string) Error { 557 | e.Format = f 558 | return e 559 | } 560 | 561 | // Error implements [error] interface. 562 | func (e ErrExclMax) Error() string { 563 | f := e.Format 564 | if f == "" { 565 | f = "must be less than {value}" 566 | } 567 | return format(f, pair{"value", e.Value}) 568 | } 569 | 570 | // A constraint error returned by [MinLen]. 571 | type ErrMinLen struct { 572 | Format string 573 | Value int 574 | } 575 | 576 | // GetDefault implements [Error] interface. 577 | func (e ErrMinLen) GetDefault() Error { 578 | return ErrMinLen{} 579 | } 580 | 581 | // SetFormat implements [Error] interface. 582 | func (e ErrMinLen) SetFormat(f string) Error { 583 | e.Format = f 584 | return e 585 | } 586 | 587 | // Error implements [error] interface. 588 | func (e ErrMinLen) Error() string { 589 | f := e.Format 590 | if f == "" { 591 | f = "must be at least {value} characters long" 592 | } 593 | return format(f, pair{"value", e.Value}) 594 | } 595 | 596 | // A constraint error returned by [MaxLen]. 597 | type ErrMaxLen struct { 598 | Format string 599 | Value int 600 | } 601 | 602 | // GetDefault implements [Error] interface. 603 | func (e ErrMaxLen) GetDefault() Error { 604 | return ErrMaxLen{} 605 | } 606 | 607 | // SetFormat implements [Error] interface. 608 | func (e ErrMaxLen) SetFormat(f string) Error { 609 | e.Format = f 610 | return e 611 | } 612 | 613 | // Error implements [error] interface. 614 | func (e ErrMaxLen) Error() string { 615 | f := e.Format 616 | if f == "" { 617 | f = "must be at most {value} characters long" 618 | } 619 | return format(f, pair{"value", e.Value}) 620 | } 621 | 622 | // A constraint error returned by [Pattern]. 623 | type ErrPattern struct { 624 | Format string 625 | } 626 | 627 | // GetDefault implements [Error] interface. 628 | func (e ErrPattern) GetDefault() Error { 629 | return ErrPattern{} 630 | } 631 | 632 | // SetFormat implements [Error] interface. 633 | func (e ErrPattern) SetFormat(f string) Error { 634 | e.Format = f 635 | return e 636 | } 637 | 638 | // Error implements [error] interface. 639 | func (e ErrPattern) Error() string { 640 | f := e.Format 641 | if f == "" { 642 | f = "must match the pattern" 643 | } 644 | return f 645 | } 646 | 647 | // A constraint error returned by [Contains]. 648 | type ErrContains struct { 649 | Format string 650 | Err Error 651 | } 652 | 653 | // GetDefault implements [Error] interface. 654 | func (e ErrContains) GetDefault() Error { 655 | return ErrContains{} 656 | } 657 | 658 | // SetFormat implements [Error] interface. 659 | func (e ErrContains) SetFormat(f string) Error { 660 | e.Format = f 661 | return e 662 | } 663 | 664 | // Error implements [error] interface. 665 | func (e ErrContains) Error() string { 666 | f := e.Format 667 | if f == "" { 668 | f = "at least one item {error}" 669 | } 670 | return format(f, pair{"error", e.Err}) 671 | } 672 | 673 | // Unwrap implements [ErrorWrapper] interface. 674 | func (e ErrContains) Unwrap() error { 675 | return e.Err 676 | } 677 | 678 | // Map implements [ErrorWrapper] interface. 679 | func (e ErrContains) Map(f func(Error) Error) Error { 680 | e.Err = f(e.Err) 681 | return e 682 | } 683 | 684 | // A constraint error returned by [MinItems]. 685 | type ErrMinItems struct { 686 | Format string 687 | Value int 688 | } 689 | 690 | // GetDefault implements [Error] interface. 691 | func (e ErrMinItems) GetDefault() Error { 692 | return ErrMinItems{} 693 | } 694 | 695 | // SetFormat implements [Error] interface. 696 | func (e ErrMinItems) SetFormat(f string) Error { 697 | e.Format = f 698 | return e 699 | } 700 | 701 | // Error implements [error] interface. 702 | func (e ErrMinItems) Error() string { 703 | f := e.Format 704 | if f == "" { 705 | f = "must contain at least {value} items" 706 | } 707 | return format(f, pair{"value", e.Value}) 708 | } 709 | 710 | // A constraint error returned by [MaxItems]. 711 | type ErrMaxItems struct { 712 | Format string 713 | Value int 714 | } 715 | 716 | // GetDefault implements [Error] interface. 717 | func (e ErrMaxItems) GetDefault() Error { 718 | return ErrMaxItems{} 719 | } 720 | 721 | // SetFormat implements [Error] interface. 722 | func (e ErrMaxItems) SetFormat(f string) Error { 723 | e.Format = f 724 | return e 725 | } 726 | 727 | // Error implements [error] interface. 728 | func (e ErrMaxItems) Error() string { 729 | f := e.Format 730 | if f == "" { 731 | f = "must contain at most {value} items" 732 | } 733 | return format(f, pair{"value", e.Value}) 734 | } 735 | 736 | // A constraint error returned by [PropertyNames]. 737 | type ErrPropertyNames struct { 738 | Format string 739 | Name string 740 | Err Error 741 | } 742 | 743 | // GetDefault implements [Error] interface. 744 | func (e ErrPropertyNames) GetDefault() Error { 745 | return ErrPropertyNames{} 746 | } 747 | 748 | // SetFormat implements [Error] interface. 749 | func (e ErrPropertyNames) SetFormat(f string) Error { 750 | e.Format = f 751 | return e 752 | } 753 | 754 | // Error implements [error] interface. 755 | func (e ErrPropertyNames) Error() string { 756 | f := e.Format 757 | if f == "" { 758 | f = "property name {name} {error}" 759 | } 760 | return format(f, pair{"name", e.Name}, pair{"error", e.Err}) 761 | } 762 | 763 | // Unwrap implements [ErrorWrapper] interface. 764 | func (e ErrPropertyNames) Unwrap() error { 765 | return e.Err 766 | } 767 | 768 | // Map implements [ErrorWrapper] interface. 769 | func (e ErrPropertyNames) Map(f func(Error) Error) Error { 770 | e.Err = f(e.Err) 771 | return e 772 | } 773 | 774 | // A constraint error returned by [MinProperties]. 775 | type ErrMinProperties struct { 776 | Format string 777 | Value int 778 | } 779 | 780 | // GetDefault implements [Error] interface. 781 | func (e ErrMinProperties) GetDefault() Error { 782 | return ErrMinProperties{} 783 | } 784 | 785 | // SetFormat implements [Error] interface. 786 | func (e ErrMinProperties) SetFormat(f string) Error { 787 | e.Format = f 788 | return e 789 | } 790 | 791 | // Error implements [error] interface. 792 | func (e ErrMinProperties) Error() string { 793 | f := e.Format 794 | if f == "" { 795 | f = "must contain at least {value} properties" 796 | } 797 | return format(f, pair{"value", e.Value}) 798 | } 799 | 800 | // A constraint error returned by [MaxProperties]. 801 | type ErrMaxProperties struct { 802 | Format string 803 | Value int 804 | } 805 | 806 | // GetDefault implements [Error] interface. 807 | func (e ErrMaxProperties) GetDefault() Error { 808 | return ErrMaxProperties{} 809 | } 810 | 811 | // SetFormat implements [Error] interface. 812 | func (e ErrMaxProperties) SetFormat(f string) Error { 813 | e.Format = f 814 | return e 815 | } 816 | 817 | // Error implements [error] interface. 818 | func (e ErrMaxProperties) Error() string { 819 | f := e.Format 820 | if f == "" { 821 | f = "must contain at most {value} properties" 822 | } 823 | return format(f, pair{"value", e.Value}) 824 | } 825 | --------------------------------------------------------------------------------