├── .gitignore ├── .travis.sh ├── .travis.yml ├── LICENSE ├── README.md ├── array.go ├── array_test.go ├── basic.go ├── bool.go ├── builder ├── array.go ├── bool.go ├── builder.go ├── builder_test.go ├── enum.go ├── number.go ├── object.go ├── reference.go └── string.go ├── cmd └── jsval │ └── jsval.go ├── combinations.go ├── enum.go ├── example_test.go ├── generated_test.go ├── generated_validator_test.go ├── generator.go ├── interface.go ├── internal └── cmd │ ├── genmaybe │ └── genmaybe.go │ └── gentest │ └── gentest.go ├── jsval.go ├── maybe.go ├── maybe_gen_test.go ├── maybe_test.go ├── number.go ├── number_test.go ├── object.go ├── object_test.go ├── reference.go ├── schema.json ├── server └── server.go ├── string.go ├── string_test.go └── structinfo.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | DIFF=$(git diff) 6 | if [[ ! -z "$DIFF" ]]; then 7 | echo "git diff found modified source after code generation" 8 | echo "$DIFF" 9 | exit 1 10 | fi 11 | 12 | go test -v ./... 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - 1.11.x 5 | - tip 6 | before_script: go generate 7 | script: ./.travis.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 lestrrat 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 in all 13 | 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-jsval 2 | 3 | Validator toolset, with code generation from JSON Schema 4 | 5 | [![Build Status](https://travis-ci.org/lestrrat-go/jsval.svg?branch=master)](https://travis-ci.org/lestrrat-go/jsval) 6 | 7 | [![GoDoc](https://godoc.org/github.com/lestrrat-go/jsval?status.svg)](https://godoc.org/github.com/lestrrat-go/jsval) 8 | 9 | 10 | # Description 11 | 12 | The `go-jsval` package is a data validation toolset, with 13 | a tool to generate validators in Go from JSON schemas. 14 | 15 | # Synopsis 16 | 17 | Read a schema file and create a validator using `jsval` command: 18 | 19 | ```shell 20 | jsval -s /path/to/schema.json -o validator.go 21 | jsval -s /path/to/hyperschema.json -o validator.go -p "/links/0/schema" -p "/links/1/schema" -p "/links/2/schema" 22 | ``` 23 | 24 | Read a schema file and create a validator programatically: 25 | 26 | ```go 27 | package jsval_test 28 | 29 | import ( 30 | "log" 31 | 32 | "github.com/lestrrat-go/jsschema" 33 | "github.com/lestrrat-go/jsval/builder" 34 | ) 35 | 36 | func ExampleBuild() { 37 | s, err := schema.ReadFile(`/path/to/schema.json`) 38 | if err != nil { 39 | log.Printf("failed to open schema: %s", err) 40 | return 41 | } 42 | 43 | b := builder.New() 44 | v, err := b.Build(s) 45 | if err != nil { 46 | log.Printf("failed to build validator: %s", err) 47 | return 48 | } 49 | 50 | var input interface{} 51 | if err := v.Validate(input); err != nil { 52 | log.Printf("validation failed: %s", err) 53 | return 54 | } 55 | } 56 | ``` 57 | 58 | Build a validator by hand: 59 | 60 | ```go 61 | func ExampleManual() { 62 | v := jsval.Object(). 63 | AddProp(`zip`, jsval.String().RegexpString(`^\d{5}$`)). 64 | AddProp(`address`, jsval.String()). 65 | AddProp(`name`, jsval.String()). 66 | AddProp(`phone_number`, jsval.String().RegexpString(`^[\d-]+$`)). 67 | Required(`zip`, `address`, `name`) 68 | 69 | var input interface{} 70 | if err := v.Validate(input); err != nil { 71 | log.Printf("validation failed: %s", err) 72 | return 73 | } 74 | } 75 | ``` 76 | 77 | # Install 78 | 79 | ``` 80 | go get -u github.com/lestrrat-go/jsval 81 | ``` 82 | 83 | If you want to install the `jsval` tool, do 84 | 85 | ``` 86 | go get -u github.com/lestrrat-go/jsval/cmd/jsval 87 | ``` 88 | 89 | # Features 90 | 91 | ## Can generate validators from JSON Schema definition 92 | 93 | The following command creates a file named `jsval.go` 94 | which contains various variables containing `*jsval.JSVal` 95 | structures so you can include them in your code: 96 | 97 | ``` 98 | jsval -s schema.json -o jsval.go 99 | ``` 100 | 101 | See the file `generated_validator_test.go` for a sample 102 | generated from JSON Schema schema. 103 | 104 | If your document isn't a real JSON schema but contains one 105 | or more JSON schema (like JSON Hyper Schema) somewhere inside 106 | the document, you can use the `-p` argument to access a 107 | specific portion of a JSON document: 108 | 109 | ``` 110 | jsval -s hyper.json -p "/links/0" -p "/lnks/1" 111 | ``` 112 | 113 | This will generate a set of validators, with JSON references 114 | within the file `hyper.json` properly resolved. 115 | 116 | ## Can handle JSON References in JSON Schema definitions 117 | 118 | Note: Not very well tested. Test cases welcome 119 | 120 | This packages tries to handle JSON References properly. 121 | For example, in the schema below, "age" input is validated 122 | against the `positiveInteger` schema: 123 | 124 | ```json 125 | { 126 | "definitions": { 127 | "positiveInteger": { 128 | "type": "integer", 129 | "minimum": 0, 130 | } 131 | }, 132 | "properties": { 133 | "age": { "$ref": "#/definitions/positiveInteger" } 134 | } 135 | } 136 | ``` 137 | 138 | ## Run a playground server 139 | 140 | ``` 141 | jsval server -listen :8080 142 | ``` 143 | 144 | You can specify a JSON schema, and see what kind of validator gets generated. 145 | 146 | # Tricks 147 | 148 | ## Specifying structs with values that may or may not be initialized 149 | 150 | With maps, it's easy to check if a property exists. But if you are validating a struct, 151 | however, all of the fields exist all the time, and you basically cannot detect if you 152 | have a missing field to apply defaults, etc. 153 | 154 | For such cases you should use the `Maybe` interface provided in this package: 155 | 156 | ```go 157 | type Foo struct { 158 | Name MaybeString `json:"name"` 159 | } 160 | ``` 161 | 162 | This will declare the value as "optional", and the JSVal validation mechanism does 163 | the correct thing to process this field. 164 | 165 | # References 166 | 167 | | Name | Notes | 168 | |:--------------------------------------------------------:|:---------------------------------| 169 | | [go-jsschema](https://github.com/lestrrat-go/jsschema) | JSON Schema implementation | 170 | | [go-jshschema](https://github.com/lestrrat-go/jshschema) | JSON Hyper Schema implementation | 171 | | [go-jsref](https://github.com/lestrrat-go/jsref) | JSON Reference implementation | 172 | | [go-jspointer](https://github.com/lestrrat-go/jspointer) | JSON Pointer implementations | 173 | 174 | -------------------------------------------------------------------------------- /array.go: -------------------------------------------------------------------------------- 1 | package jsval 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/lestrrat-go/pdebug" 9 | ) 10 | 11 | // Array creates a new ArrayConstraint 12 | func Array() *ArrayConstraint { 13 | return &ArrayConstraint{ 14 | additionalItems: EmptyConstraint, 15 | maxItems: -1, 16 | minItems: -1, 17 | } 18 | } 19 | 20 | // Validate validates the given value against this Constraint 21 | func (c *ArrayConstraint) Validate(v interface{}) (err error) { 22 | if pdebug.Enabled { 23 | g := pdebug.IPrintf("START ArrayConstraint.Validate") 24 | defer func() { 25 | if err == nil { 26 | g.IRelease("END ArrayConstraint.Validate (PASS)") 27 | } else { 28 | g.IRelease("END ArrayConstraint.Validate (FAIL): %s", err) 29 | } 30 | }() 31 | } 32 | 33 | rv := reflect.ValueOf(v) 34 | switch rv.Kind() { 35 | case reflect.Slice: 36 | default: 37 | var typ string 38 | if rv == zeroval { 39 | typ = "invalid" 40 | } else { 41 | typ = rv.Type().String() 42 | } 43 | return errors.New("value must be a slice (was: " + typ + ")") 44 | } 45 | 46 | l := rv.Len() 47 | 48 | if mi := c.minItems; mi > -1 && l < mi { 49 | return errors.New("fewer items than minItems") 50 | } 51 | 52 | if mi := c.maxItems; mi > -1 && l > mi { 53 | return errors.New("more items than maxItems") 54 | } 55 | 56 | var uitems map[string]struct{} 57 | if c.uniqueItems { 58 | pdebug.Printf("Check for unique items enabled") 59 | uitems = make(map[string]struct{}) 60 | for i := 0; i < l; i++ { 61 | iv := rv.Index(i).Interface() 62 | kv := fmt.Sprintf("%s", iv) 63 | pdebug.Printf("unique? -> %s", kv) 64 | if _, ok := uitems[kv]; ok { 65 | return errors.New("duplicate element found") 66 | } 67 | uitems[kv] = struct{}{} 68 | } 69 | } 70 | 71 | if celem := c.items; celem != nil { 72 | if pdebug.Enabled { 73 | pdebug.Printf("Checking if all items match a spec") 74 | } 75 | // if this is set, then all items must fulfill this. 76 | // additional items are ignored 77 | for i := 0; i < l; i++ { 78 | iv := rv.Index(i).Interface() 79 | if err := celem.Validate(iv); err != nil { 80 | return err 81 | } 82 | } 83 | } else { 84 | // otherwise, check the positional specs, and apply the 85 | // additionalItems constraint 86 | lv := rv.Len() 87 | for i, cpos := range c.positionalItems { 88 | if lv <= i { 89 | break 90 | } 91 | if pdebug.Enabled { 92 | pdebug.Printf("Checking positional item at '%d'", i) 93 | } 94 | iv := rv.Index(i).Interface() 95 | if err := cpos.Validate(iv); err != nil { 96 | return err 97 | } 98 | } 99 | 100 | lp := len(c.positionalItems) 101 | if lp > 0 && l > lp { // we got more than positional schemas 102 | cadd := c.additionalItems 103 | if cadd == nil { // you can't have additionalItems! 104 | return errors.New("additional elements found in array") 105 | } 106 | for i := lp - 1; i < l; i++ { 107 | iv := rv.Index(i).Interface() 108 | if err := cadd.Validate(iv); err != nil { 109 | return err 110 | } 111 | } 112 | } 113 | } 114 | return nil 115 | } 116 | 117 | // AdditionalItems specifies the constraint that additional items 118 | // must be validated against. Note that if you specify `Items` 119 | // with this constraint, `AdditionalItems` has no effect. This 120 | // constraint only makes sense when used along side with the 121 | // `PositionalItems` constraint. 122 | // 123 | // If you want to allow any additional items, pass `EmptyConstraint` 124 | // to this method. If unspecified, no extra items are allowed. 125 | func (c *ArrayConstraint) AdditionalItems(ac Constraint) *ArrayConstraint { 126 | c.additionalItems = ac 127 | return c 128 | } 129 | 130 | // Items specifies the constraint that all items in the array 131 | // must be validated against 132 | func (c *ArrayConstraint) Items(ac Constraint) *ArrayConstraint { 133 | c.items = ac 134 | return c 135 | } 136 | 137 | // MinItems specifies the minimum number of items in the value. 138 | // If unspecified, the check is not performed. 139 | func (c *ArrayConstraint) MinItems(i int) *ArrayConstraint { 140 | c.minItems = i 141 | return c 142 | } 143 | 144 | // MaxItems specifies the maximum number of items in the value. 145 | // If unspecified, the check is not performed. 146 | func (c *ArrayConstraint) MaxItems(i int) *ArrayConstraint { 147 | c.maxItems = i 148 | return c 149 | } 150 | 151 | // PositionalItems specifies the constraint that elements in 152 | // each position of the array being validated. Extra items in 153 | // the array are validated against the constraint specified for 154 | // `AdditionalItems`. 155 | func (c *ArrayConstraint) PositionalItems(ac []Constraint) *ArrayConstraint { 156 | c.positionalItems = ac 157 | return c 158 | } 159 | 160 | // UniqueItems specifies if the array can hold non-unique items. 161 | // When set to true, the validation will fail unless all of your 162 | // elements are unique. 163 | // 164 | // Caveat: Currently we stringify the elements before comparing 165 | // for uniqueness. This could/should be fixed. 166 | func (c *ArrayConstraint) UniqueItems(b bool) *ArrayConstraint { 167 | if pdebug.Enabled { 168 | pdebug.Printf("Setting uniqueItems = %t", b) 169 | } 170 | c.uniqueItems = b 171 | return c 172 | } -------------------------------------------------------------------------------- /array_test.go: -------------------------------------------------------------------------------- 1 | package jsval_test 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/lestrrat-go/jsschema" 9 | "github.com/lestrrat-go/jsval" 10 | "github.com/lestrrat-go/jsval/builder" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | // test against github#2 15 | func TestArrayItemsReference(t *testing.T) { 16 | const src = `{ 17 | "definitions": { 18 | "uint": { 19 | "type": "integer", 20 | "minimum": 0 21 | } 22 | }, 23 | "type": "object", 24 | "properties": { 25 | "numbers": { 26 | "type": "array", 27 | "items": { "$ref": "#/definitions/uint" } 28 | }, 29 | "tuple": { 30 | "items": [ { "type": "string" }, { "type": "boolean" }, { "type": "number" } ] 31 | } 32 | } 33 | }` 34 | s, err := schema.Read(strings.NewReader(src)) 35 | if !assert.NoError(t, err, "schema.Reader should succeed") { 36 | return 37 | } 38 | 39 | b := builder.New() 40 | v, err := b.Build(s) 41 | if !assert.NoError(t, err, "builder.Build should succeed") { 42 | return 43 | } 44 | 45 | buf := bytes.Buffer{} 46 | g := jsval.NewGenerator() 47 | if !assert.NoError(t, g.Process(&buf, v), "Generator.Process should succeed") { 48 | return 49 | } 50 | 51 | code := buf.String() 52 | if !assert.True(t, strings.Contains(code, "\tItems("), "Generated code chould contain `.Items()`") { 53 | return 54 | } 55 | 56 | if !assert.True(t, strings.Contains(code, "\tPositionalItems("), "Generated code should contain `.PositionalItems()`") { 57 | return 58 | } 59 | } -------------------------------------------------------------------------------- /basic.go: -------------------------------------------------------------------------------- 1 | package jsval 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | 7 | "github.com/lestrrat-go/pdebug" 8 | ) 9 | 10 | func (dv defaultValue) HasDefault() bool { 11 | return dv.initialized 12 | } 13 | 14 | func (dv defaultValue) DefaultValue() interface{} { 15 | return dv.value 16 | } 17 | 18 | func (nc emptyConstraint) Validate(_ interface{}) error { 19 | return nil 20 | } 21 | 22 | func (nc emptyConstraint) HasDefault() bool { 23 | return false 24 | } 25 | 26 | func (nc emptyConstraint) DefaultValue() interface{} { 27 | return nil 28 | } 29 | 30 | func (nc nullConstraint) HasDefault() bool { 31 | return false 32 | } 33 | 34 | func (nc nullConstraint) DefaultValue() interface{} { 35 | return nil 36 | } 37 | 38 | func (nc nullConstraint) Validate(v interface{}) (err error) { 39 | if pdebug.Enabled { 40 | g := pdebug.Marker("NullConstraint.Validate").BindError(&err) 41 | defer g.End() 42 | } 43 | 44 | rv := reflect.ValueOf(v) 45 | if rv == zeroval { 46 | return nil 47 | } 48 | 49 | switch rv.Kind() { 50 | case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: 51 | if rv.IsNil() { 52 | return nil 53 | } 54 | } 55 | return errors.New("value is not null") 56 | } 57 | 58 | // Not creates a new NotConstraint. You must pass in the 59 | // child constraint to be run 60 | func Not(c Constraint) *NotConstraint { 61 | return &NotConstraint{child: c} 62 | } 63 | 64 | // HasDefault is a no op for this constraint 65 | func (nc NotConstraint) HasDefault() bool { 66 | return false 67 | } 68 | 69 | // DefaultValue is a no op for this constraint 70 | func (nc NotConstraint) DefaultValue() interface{} { 71 | return nil 72 | } 73 | 74 | // Validate runs the validation, and returns an error unless 75 | // the child constraint fails 76 | func (nc NotConstraint) Validate(v interface{}) (err error) { 77 | if pdebug.Enabled { 78 | g := pdebug.Marker("NotConstraint.Validate").BindError(&err) 79 | defer g.End() 80 | } 81 | 82 | if nc.child == nil { 83 | return errors.New("'not' constraint does not have a child constraint") 84 | } 85 | 86 | if err := nc.child.Validate(v); err == nil { 87 | return errors.New("'not' validation failed") 88 | } 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /bool.go: -------------------------------------------------------------------------------- 1 | package jsval 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | ) 7 | 8 | // Boolean creates a new BooleanConsraint 9 | func Boolean() *BooleanConstraint { 10 | return &BooleanConstraint{} 11 | } 12 | 13 | // Default specifies the default value to apply 14 | func (bc *BooleanConstraint) Default(v interface{}) *BooleanConstraint { 15 | bc.defaultValue.initialized = true 16 | bc.defaultValue.value = v 17 | return bc 18 | } 19 | 20 | // Validate vaidates the value against the given value 21 | func (bc *BooleanConstraint) Validate(v interface{}) error { 22 | rv := reflect.ValueOf(v) 23 | switch rv.Kind() { 24 | case reflect.Bool: 25 | default: 26 | return errors.New("value is not a boolean") 27 | } 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /builder/array.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "github.com/lestrrat-go/jsschema" 5 | "github.com/lestrrat-go/jsval" 6 | "github.com/lestrrat-go/pdebug" 7 | ) 8 | 9 | func buildArrayConstraint(ctx *buildctx, c *jsval.ArrayConstraint, s *schema.Schema) (err error) { 10 | if pdebug.Enabled { 11 | g := pdebug.IPrintf("START buildArrayConstraint") 12 | defer func() { 13 | if err == nil { 14 | g.IRelease("END buildArrayConstraint (PASS)") 15 | } else { 16 | g.IRelease("END buildArrayConstraint (FAIL): %s", err) 17 | } 18 | }() 19 | } 20 | 21 | if items := s.Items; items != nil { 22 | if !items.TupleMode { 23 | specs, err := buildFromSchema(ctx, items.Schemas[0]) 24 | if err != nil { 25 | return err 26 | } 27 | c.Items(specs) 28 | } else { 29 | specs := make([]jsval.Constraint, len(items.Schemas)) 30 | for i, espec := range items.Schemas { 31 | item, err := buildFromSchema(ctx, espec) 32 | if err != nil { 33 | return err 34 | } 35 | specs[i] = item 36 | } 37 | c.PositionalItems(specs) 38 | 39 | aitems := s.AdditionalItems 40 | if aitems == nil { 41 | if pdebug.Enabled { 42 | pdebug.Printf("Disabling additional items") 43 | } 44 | // No additional items 45 | c.AdditionalItems(nil) 46 | } else if as := aitems.Schema; as != nil { 47 | spec, err := buildFromSchema(ctx, as) 48 | if err != nil { 49 | return err 50 | } 51 | if pdebug.Enabled { 52 | pdebug.Printf("Using constraint for additional items ") 53 | } 54 | c.AdditionalItems(spec) 55 | } else { 56 | if pdebug.Enabled { 57 | pdebug.Printf("Additional items will be allowed freely") 58 | } 59 | c.AdditionalItems(jsval.EmptyConstraint) 60 | } 61 | } 62 | } 63 | 64 | if s.MinItems.Initialized { 65 | c.MinItems(s.MinItems.Val) 66 | } 67 | 68 | if s.MaxItems.Initialized { 69 | c.MaxItems(s.MaxItems.Val) 70 | } 71 | 72 | if s.UniqueItems.Initialized { 73 | c.UniqueItems(s.UniqueItems.Val) 74 | } 75 | 76 | return nil 77 | } -------------------------------------------------------------------------------- /builder/bool.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "github.com/lestrrat-go/jsschema" 5 | "github.com/lestrrat-go/jsval" 6 | ) 7 | 8 | func buildBooleanConstraint(_ *buildctx, c *jsval.BooleanConstraint, s *schema.Schema) error { 9 | v := s.Default 10 | switch v.(type) { 11 | case bool: 12 | c.Default(v.(bool)) 13 | } 14 | return nil 15 | } -------------------------------------------------------------------------------- /builder/builder.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | /* Package builder contains structures and methods responsible for 4 | * generating a jsval.JSVal structure from a JSON schema 5 | */ 6 | 7 | import ( 8 | "errors" 9 | "math" 10 | "reflect" 11 | "sort" 12 | 13 | "github.com/lestrrat-go/jsref" 14 | "github.com/lestrrat-go/jsschema" 15 | "github.com/lestrrat-go/jsval" 16 | "github.com/lestrrat-go/pdebug" 17 | ) 18 | 19 | // Builder builds Validator objects from JSON schemas 20 | type Builder struct{} 21 | 22 | type buildctx struct { 23 | V *jsval.JSVal 24 | S *schema.Schema 25 | R map[string]struct{} 26 | } 27 | 28 | // New creates a new builder object 29 | func New() *Builder { 30 | return &Builder{} 31 | } 32 | 33 | // Build creates a new validator from the specified schema 34 | func (b *Builder) Build(s *schema.Schema) (v *jsval.JSVal, err error) { 35 | if pdebug.Enabled { 36 | g := pdebug.IPrintf("START Builder.Build") 37 | defer func() { 38 | if err == nil { 39 | g.IRelease("END Builder.Build (OK)") 40 | } else { 41 | g.IRelease("END Builder.Build (FAIL): %s", err) 42 | } 43 | }() 44 | } 45 | 46 | if s == nil { 47 | return nil, errors.New("nil schema") 48 | } 49 | 50 | return b.BuildWithCtx(s, nil) 51 | } 52 | 53 | // BuildWithCtx creates a new validator from the specified schema, using 54 | // the jsctx parameter as the context to resolve JSON References with. 55 | // If you expect your schema to contain JSON references to itself, 56 | // you will have to pass the context as a map with raw decoded JSON data 57 | func (b *Builder) BuildWithCtx(s *schema.Schema, jsctx interface{}) (v *jsval.JSVal, err error) { 58 | if pdebug.Enabled { 59 | g := pdebug.IPrintf("START Builder.BuildWithCtx") 60 | defer func() { 61 | if err == nil { 62 | g.IRelease("END Builder.BuildWithCtx (OK)") 63 | } else { 64 | g.IRelease("END Builder.BuildWithCtx (FAIL): %s", err) 65 | } 66 | }() 67 | } 68 | 69 | if s == nil { 70 | return nil, errors.New("nil schema") 71 | } 72 | 73 | v = jsval.New() 74 | ctx := buildctx{ 75 | V: v, 76 | S: s, 77 | R: map[string]struct{}{}, // names of references used 78 | } 79 | 80 | c, err := buildFromSchema(&ctx, s) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | if _, ok := ctx.R["#"]; ok { 86 | v.SetReference("#", c) 87 | delete(ctx.R, "#") 88 | } 89 | 90 | // Now, resolve references that were used in the schema 91 | if len(ctx.R) > 0 { 92 | if pdebug.Enabled { 93 | pdebug.Printf("Checking references now") 94 | } 95 | if jsctx == nil { 96 | jsctx = s 97 | } 98 | 99 | r := jsref.New() 100 | for ref := range ctx.R { 101 | if err := compileReferences(&ctx, r, v, ref, jsctx); err != nil { 102 | return nil, err 103 | } 104 | } 105 | } 106 | v.SetRoot(c) 107 | return v, nil 108 | } 109 | 110 | func compileReferences(ctx *buildctx, r *jsref.Resolver, v *jsval.JSVal, ref string, jsctx interface{}) error { 111 | if _, err := v.GetReference(ref); err == nil { 112 | if pdebug.Enabled { 113 | pdebug.Printf("Already resolved constraints for reference '%s'", ref) 114 | } 115 | return nil 116 | } 117 | 118 | if pdebug.Enabled { 119 | pdebug.Printf("Building constraints for reference '%s'", ref) 120 | } 121 | 122 | thing, err := r.Resolve(jsctx, ref) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | if pdebug.Enabled { 128 | pdebug.Printf("'%s' resolves to the main schema", ref) 129 | } 130 | 131 | var s1 *schema.Schema 132 | switch thing.(type) { 133 | case *schema.Schema: 134 | s1 = thing.(*schema.Schema) 135 | case map[string]interface{}: 136 | s1 = schema.New() 137 | if err := s1.Extract(thing.(map[string]interface{})); err != nil { 138 | return err 139 | } 140 | } 141 | 142 | c1, err := buildFromSchema(ctx, s1) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | v.SetReference(ref, c1) 148 | for ref := range ctx.R { 149 | if err := compileReferences(ctx, r, v, ref, jsctx); err != nil { 150 | return err 151 | } 152 | } 153 | return nil 154 | } 155 | 156 | func buildFromSchema(ctx *buildctx, s *schema.Schema) (jsval.Constraint, error) { 157 | if ref := s.Reference; ref != "" { 158 | c := jsval.Reference(ctx.V) 159 | if err := buildReferenceConstraint(ctx, c, s); err != nil { 160 | return nil, err 161 | } 162 | ctx.R[ref] = struct{}{} 163 | return c, nil 164 | } 165 | 166 | ct := jsval.All() 167 | 168 | switch { 169 | case s.Not != nil: 170 | if pdebug.Enabled { 171 | pdebug.Printf("Not constraint") 172 | } 173 | c1, err := buildFromSchema(ctx, s.Not) 174 | if err != nil { 175 | return nil, err 176 | } 177 | ct.Add(jsval.Not(c1)) 178 | case len(s.AllOf) > 0: 179 | if pdebug.Enabled { 180 | pdebug.Printf("AllOf constraint") 181 | } 182 | ac := jsval.All() 183 | for _, s1 := range s.AllOf { 184 | c1, err := buildFromSchema(ctx, s1) 185 | if err != nil { 186 | return nil, err 187 | } 188 | ac.Add(c1) 189 | } 190 | ct.Add(ac.Reduce()) 191 | case len(s.AnyOf) > 0: 192 | if pdebug.Enabled { 193 | pdebug.Printf("AnyOf constraint") 194 | } 195 | ac := jsval.Any() 196 | for _, s1 := range s.AnyOf { 197 | c1, err := buildFromSchema(ctx, s1) 198 | if err != nil { 199 | return nil, err 200 | } 201 | ac.Add(c1) 202 | } 203 | ct.Add(ac.Reduce()) 204 | case len(s.OneOf) > 0: 205 | if pdebug.Enabled { 206 | pdebug.Printf("OneOf constraint") 207 | } 208 | oc := jsval.OneOf() 209 | for _, s1 := range s.OneOf { 210 | c1, err := buildFromSchema(ctx, s1) 211 | if err != nil { 212 | return nil, err 213 | } 214 | oc.Add(c1) 215 | } 216 | ct.Add(oc.Reduce()) 217 | } 218 | 219 | var sts schema.PrimitiveTypes 220 | if l := len(s.Type); l > 0 { 221 | sts = make(schema.PrimitiveTypes, l) 222 | copy(sts, s.Type) 223 | } else { 224 | if pdebug.Enabled { 225 | pdebug.Printf("Schema doesn't seem to contain a 'type' field. Now guessing...") 226 | } 227 | sts = guessSchemaType(s) 228 | } 229 | sort.Sort(sts) 230 | 231 | if len(sts) > 0 { 232 | tct := jsval.Any() 233 | for _, st := range sts { 234 | var c jsval.Constraint 235 | switch st { 236 | case schema.StringType: 237 | sc := jsval.String() 238 | if err := buildStringConstraint(ctx, sc, s); err != nil { 239 | return nil, err 240 | } 241 | c = sc 242 | case schema.NumberType: 243 | nc := jsval.Number() 244 | if err := buildNumberConstraint(ctx, nc, s); err != nil { 245 | return nil, err 246 | } 247 | c = nc 248 | case schema.IntegerType: 249 | ic := jsval.Integer() 250 | if err := buildIntegerConstraint(ctx, ic, s); err != nil { 251 | return nil, err 252 | } 253 | c = ic 254 | case schema.BooleanType: 255 | bc := jsval.Boolean() 256 | if err := buildBooleanConstraint(ctx, bc, s); err != nil { 257 | return nil, err 258 | } 259 | c = bc 260 | case schema.ArrayType: 261 | ac := jsval.Array() 262 | if err := buildArrayConstraint(ctx, ac, s); err != nil { 263 | return nil, err 264 | } 265 | c = ac 266 | case schema.ObjectType: 267 | oc := jsval.Object() 268 | if err := buildObjectConstraint(ctx, oc, s); err != nil { 269 | return nil, err 270 | } 271 | c = oc 272 | case schema.NullType: 273 | c = jsval.NullConstraint 274 | default: 275 | return nil, errors.New("unknown type: " + st.String()) 276 | } 277 | tct.Add(c) 278 | } 279 | ct.Add(tct.Reduce()) 280 | } else { 281 | // All else failed, check if we have some enumeration? 282 | if len(s.Enum) > 0 { 283 | ec := jsval.Enum(s.Enum...) 284 | ct.Add(ec) 285 | } 286 | } 287 | 288 | return ct.Reduce(), nil 289 | } 290 | 291 | func guessSchemaType(s *schema.Schema) schema.PrimitiveTypes { 292 | if pdebug.Enabled { 293 | g := pdebug.IPrintf("START guessSchemaType") 294 | defer g.IRelease("END guessSchemaType") 295 | } 296 | 297 | var sts schema.PrimitiveTypes 298 | if schemaLooksLikeObject(s) { 299 | if pdebug.Enabled { 300 | pdebug.Printf("Looks like it could be an object...") 301 | } 302 | sts = append(sts, schema.ObjectType) 303 | } 304 | 305 | if schemaLooksLikeArray(s) { 306 | if pdebug.Enabled { 307 | pdebug.Printf("Looks like it could be an array...") 308 | } 309 | sts = append(sts, schema.ArrayType) 310 | } 311 | 312 | if schemaLooksLikeString(s) { 313 | if pdebug.Enabled { 314 | pdebug.Printf("Looks like it could be a string...") 315 | } 316 | sts = append(sts, schema.StringType) 317 | } 318 | 319 | if ok, typ := schemaLooksLikeNumber(s); ok { 320 | if pdebug.Enabled { 321 | pdebug.Printf("Looks like it could be a number...") 322 | } 323 | sts = append(sts, typ) 324 | } 325 | 326 | if schemaLooksLikeBool(s) { 327 | if pdebug.Enabled { 328 | pdebug.Printf("Looks like it could be a bool...") 329 | } 330 | sts = append(sts, schema.BooleanType) 331 | } 332 | 333 | if pdebug.Enabled { 334 | pdebug.Printf("Guessed types: %#v", sts) 335 | } 336 | 337 | return sts 338 | } 339 | 340 | func schemaLooksLikeObject(s *schema.Schema) bool { 341 | if len(s.Properties) > 0 { 342 | return true 343 | } 344 | 345 | if s.AdditionalProperties == nil { 346 | return true 347 | } 348 | 349 | if s.AdditionalProperties.Schema != nil { 350 | return true 351 | } 352 | 353 | if s.MinProperties.Initialized { 354 | return true 355 | } 356 | 357 | if s.MaxProperties.Initialized { 358 | return true 359 | } 360 | 361 | if len(s.Required) > 0 { 362 | return true 363 | } 364 | 365 | if len(s.PatternProperties) > 0 { 366 | return true 367 | } 368 | 369 | for _, v := range s.Enum { 370 | rv := reflect.ValueOf(v) 371 | switch rv.Kind() { 372 | case reflect.Map, reflect.Struct: 373 | return true 374 | } 375 | } 376 | 377 | return false 378 | } 379 | 380 | func schemaLooksLikeArray(s *schema.Schema) bool { 381 | if s.Items != nil { 382 | return true 383 | } 384 | 385 | if s.AdditionalItems == nil { 386 | return true 387 | } 388 | 389 | if s.AdditionalItems.Schema != nil { 390 | return true 391 | } 392 | 393 | if s.Items != nil { 394 | return true 395 | } 396 | 397 | if s.MinItems.Initialized { 398 | return true 399 | } 400 | 401 | if s.MaxItems.Initialized { 402 | return true 403 | } 404 | 405 | if s.UniqueItems.Initialized { 406 | return true 407 | } 408 | 409 | for _, v := range s.Enum { 410 | rv := reflect.ValueOf(v) 411 | switch rv.Kind() { 412 | case reflect.Slice: 413 | return true 414 | } 415 | } 416 | 417 | return false 418 | } 419 | 420 | func schemaLooksLikeString(s *schema.Schema) bool { 421 | if s.MinLength.Initialized { 422 | return true 423 | } 424 | 425 | if s.MaxLength.Initialized { 426 | return true 427 | } 428 | 429 | if s.Pattern != nil { 430 | return true 431 | } 432 | 433 | switch s.Format { 434 | case schema.FormatDateTime, schema.FormatEmail, schema.FormatHostname, schema.FormatIPv4, schema.FormatIPv6, schema.FormatURI: 435 | return true 436 | } 437 | 438 | for _, v := range s.Enum { 439 | rv := reflect.ValueOf(v) 440 | switch rv.Kind() { 441 | case reflect.String: 442 | return true 443 | } 444 | } 445 | 446 | return false 447 | } 448 | 449 | func isInteger(n schema.Number) bool { 450 | return math.Floor(n.Val) == n.Val 451 | } 452 | 453 | func schemaLooksLikeNumber(s *schema.Schema) (bool, schema.PrimitiveType) { 454 | if s.MultipleOf.Initialized { 455 | if isInteger(s.MultipleOf) { 456 | return true, schema.IntegerType 457 | } 458 | return true, schema.NumberType 459 | } 460 | 461 | if s.Minimum.Initialized { 462 | if isInteger(s.Minimum) { 463 | return true, schema.IntegerType 464 | } 465 | return true, schema.NumberType 466 | } 467 | 468 | if s.Maximum.Initialized { 469 | if isInteger(s.Maximum) { 470 | return true, schema.IntegerType 471 | } 472 | return true, schema.NumberType 473 | } 474 | 475 | if s.ExclusiveMinimum.Initialized { 476 | return true, schema.NumberType 477 | } 478 | 479 | if s.ExclusiveMaximum.Initialized { 480 | return true, schema.NumberType 481 | } 482 | 483 | for _, v := range s.Enum { 484 | rv := reflect.ValueOf(v) 485 | switch rv.Kind() { 486 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 487 | return true, schema.IntegerType 488 | case reflect.Float32, reflect.Float64: 489 | return true, schema.NumberType 490 | } 491 | } 492 | 493 | return false, schema.UnspecifiedType 494 | } 495 | 496 | func schemaLooksLikeBool(s *schema.Schema) bool { 497 | for _, v := range s.Enum { 498 | rv := reflect.ValueOf(v) 499 | switch rv.Kind() { 500 | case reflect.Bool: 501 | return true 502 | } 503 | } 504 | 505 | return false 506 | } -------------------------------------------------------------------------------- /builder/builder_test.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/lestrrat-go/jsschema" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGuessType(t *testing.T) { 12 | data := map[string]schema.PrimitiveTypes{ 13 | `{ "items": { "type": "number" } }`: {schema.ArrayType}, 14 | `{ "properties": { "foo": { "type": "number" } } }`: {schema.ObjectType}, 15 | `{ "additionalProperties": false }`: {schema.ObjectType}, 16 | `{ "maxLength": 10 }`: {schema.StringType}, 17 | `{ "pattern": "^[a-fA-F0-9]+$" }`: {schema.StringType}, 18 | `{ "format": "email" }`: {schema.StringType}, 19 | `{ "multipleOf": 1 }`: {schema.IntegerType}, 20 | `{ "multipleOf": 1.1 }`: {schema.NumberType}, 21 | `{ "minimum": 0, "pattern": "^[a-z]+$" }`: {schema.StringType, schema.IntegerType}, 22 | } 23 | 24 | for src, expected := range data { 25 | t.Logf("Testing '%s'", src) 26 | s, err := schema.Read(strings.NewReader(src)) 27 | if !assert.NoError(t, err, "schema.Read should succeed") { 28 | return 29 | } 30 | 31 | if !assert.Equal(t, guessSchemaType(s), expected, "types match") { 32 | return 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /builder/enum.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "github.com/lestrrat-go/jsschema" 5 | "github.com/lestrrat-go/jsval" 6 | ) 7 | 8 | func buildEnumConstraint(_ *buildctx, c *jsval.EnumConstraint, s *schema.Schema) error { 9 | l := make([]interface{}, len(s.Enum)) 10 | copy(l, s.Enum) 11 | c.Enum(l) 12 | return nil 13 | } 14 | -------------------------------------------------------------------------------- /builder/number.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/lestrrat-go/jsschema" 7 | "github.com/lestrrat-go/jsval" 8 | ) 9 | 10 | func buildIntegerConstraint(ctx *buildctx, nc *jsval.IntegerConstraint, s *schema.Schema) error { 11 | if len(s.Type) > 0 { 12 | if !s.Type.Contains(schema.IntegerType) { 13 | return errors.New("schema is not for integer") 14 | } 15 | } 16 | 17 | if s.Minimum.Initialized { 18 | nc.Minimum(s.Minimum.Val) 19 | if s.ExclusiveMinimum.Initialized { 20 | nc.ExclusiveMinimum(s.ExclusiveMinimum.Val) 21 | } 22 | } 23 | 24 | if s.Maximum.Initialized { 25 | nc.Maximum(s.Maximum.Val) 26 | if s.ExclusiveMaximum.Initialized { 27 | nc.ExclusiveMaximum(s.ExclusiveMaximum.Val) 28 | } 29 | } 30 | 31 | if s.MultipleOf.Initialized { 32 | nc.MultipleOf(s.MultipleOf.Val) 33 | } 34 | 35 | if lst := s.Enum; len(lst) > 0 { 36 | nc.Enum(lst...) 37 | } 38 | 39 | if v := s.Default; v != nil { 40 | nc.Default(v) 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func buildNumberConstraint(ctx *buildctx, nc *jsval.NumberConstraint, s *schema.Schema) error { 47 | if len(s.Type) > 0 { 48 | if !s.Type.Contains(schema.NumberType) { 49 | return errors.New("schema is not for number") 50 | } 51 | } 52 | 53 | if s.Minimum.Initialized { 54 | nc.Minimum(s.Minimum.Val) 55 | if s.ExclusiveMinimum.Initialized { 56 | nc.ExclusiveMinimum(s.ExclusiveMinimum.Val) 57 | } 58 | } 59 | 60 | if s.Maximum.Initialized { 61 | nc.Maximum(s.Maximum.Val) 62 | if s.ExclusiveMaximum.Initialized { 63 | nc.ExclusiveMaximum(s.ExclusiveMaximum.Val) 64 | } 65 | } 66 | 67 | if s.MultipleOf.Initialized { 68 | nc.MultipleOf(s.MultipleOf.Val) 69 | } 70 | 71 | if lst := s.Enum; len(lst) > 0 { 72 | nc.Enum(lst) 73 | } 74 | 75 | if v := s.Default; v != nil { 76 | nc.Default(v) 77 | } 78 | 79 | return nil 80 | } -------------------------------------------------------------------------------- /builder/object.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "github.com/lestrrat-go/jsschema" 5 | "github.com/lestrrat-go/jsval" 6 | "github.com/lestrrat-go/pdebug" 7 | ) 8 | 9 | func buildObjectConstraint(ctx *buildctx, c *jsval.ObjectConstraint, s *schema.Schema) error { 10 | if pdebug.Enabled { 11 | g := pdebug.IPrintf("START ObjectConstraint.FromSchema") 12 | defer g.IRelease("END ObjectConstraint.FromSchema") 13 | } 14 | 15 | if l := s.Required; len(l) > 0 { 16 | c.Required(l...) 17 | } 18 | 19 | if s.MinProperties.Initialized { 20 | c.MinProperties(s.MinProperties.Val) 21 | } 22 | 23 | if s.MaxProperties.Initialized { 24 | c.MaxProperties(s.MaxProperties.Val) 25 | } 26 | 27 | for pname, pdef := range s.Properties { 28 | cprop, err := buildFromSchema(ctx, pdef) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | c.AddProp(pname, cprop) 34 | } 35 | 36 | for rx, pdef := range s.PatternProperties { 37 | cprop, err := buildFromSchema(ctx, pdef) 38 | if err != nil { 39 | return err 40 | } 41 | c.PatternProperties(rx, cprop) 42 | } 43 | 44 | if aprops := s.AdditionalProperties; aprops != nil { 45 | if sc := aprops.Schema; sc != nil { 46 | aitem, err := buildFromSchema(ctx, sc) 47 | if err != nil { 48 | return err 49 | } 50 | c.AdditionalProperties(aitem) 51 | } else { 52 | c.AdditionalProperties(jsval.EmptyConstraint) 53 | } 54 | } 55 | 56 | for from, to := range s.Dependencies.Names { 57 | c.PropDependency(from, to...) 58 | } 59 | 60 | if depschemas := s.Dependencies.Schemas; len(depschemas) > 0 { 61 | for pname, depschema := range depschemas { 62 | depc, err := buildFromSchema(ctx, depschema) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | c.SchemaDependency(pname, depc) 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /builder/reference.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/lestrrat-go/jsschema" 7 | "github.com/lestrrat-go/jsval" 8 | "github.com/lestrrat-go/pdebug" 9 | ) 10 | 11 | func buildReferenceConstraint(_ *buildctx, r *jsval.ReferenceConstraint, s *schema.Schema) error { 12 | pdebug.Printf("ReferenceConstraint.buildFromSchema '%s'", s.Reference) 13 | if s.Reference == "" { 14 | return errors.New("schema does not contain a reference") 15 | } 16 | r.RefersTo(s.Reference) 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /builder/string.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/lestrrat-go/jsschema" 7 | "github.com/lestrrat-go/jsval" 8 | ) 9 | 10 | func buildStringConstraint(ctx *buildctx, c *jsval.StringConstraint, s *schema.Schema) error { 11 | if len(s.Type) > 0 { 12 | if !s.Type.Contains(schema.StringType) { 13 | return errors.New("schema is not for string") 14 | } 15 | } 16 | 17 | if s.MaxLength.Initialized { 18 | c.MaxLength(s.MaxLength.Val) 19 | } 20 | 21 | if s.MinLength.Initialized { 22 | c.MinLength(s.MinLength.Val) 23 | } 24 | 25 | if pat := s.Pattern; pat != nil { 26 | c.Regexp(pat) 27 | } 28 | 29 | if f := s.Format; f != "" { 30 | c.Format(string(f)) 31 | } 32 | 33 | if lst := s.Enum; len(lst) > 0 { 34 | c.Enum(lst...) 35 | } 36 | 37 | if v := s.Default; v != nil { 38 | c.Default(v) 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /cmd/jsval/jsval.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/jessevdk/go-flags" 11 | "github.com/lestrrat-go/jspointer" 12 | "github.com/lestrrat-go/jsschema" 13 | "github.com/lestrrat-go/jsval" 14 | "github.com/lestrrat-go/jsval/builder" 15 | "github.com/lestrrat-go/jsval/server" 16 | ) 17 | 18 | func main() { 19 | os.Exit(_main()) 20 | } 21 | 22 | // jsval schema.json [ref] 23 | // jsval hyper-schema.json -ptr /path/to/schema1 -ptr /path/to/schema2 -ptr /path/to/schema3 24 | // jsval server -listen :8080 25 | 26 | func _main() int { 27 | if len(os.Args) > 1 && os.Args[1] == "server" { 28 | return _server() 29 | } 30 | 31 | return _cli() 32 | } 33 | 34 | type serverOptions struct { 35 | Listen string `short:"l" long:"listen" description:"the address to listen to" default:":8080"` 36 | } 37 | 38 | func _server() int { 39 | var opts serverOptions 40 | if _, err := flags.Parse(&opts); err != nil { 41 | log.Printf("%s", err) 42 | return 1 43 | } 44 | 45 | s := server.New() 46 | if err := s.Run(opts.Listen); err != nil { 47 | return 1 48 | } 49 | 50 | return 0 51 | } 52 | 53 | type cliOptions struct { 54 | Schema string `short:"s" long:"schema" description:"the source JSON schema file"` 55 | OutFile string `short:"o" long:"outfile" description:"output file to generate"` 56 | Pointer []string `short:"p" long:"ptr" description:"JSON pointer(s) within the document to create validators with"` 57 | Prefix string `short:"P" long:"prefix" description:"prefix for validator name(s)"` 58 | } 59 | 60 | func _cli() int { 61 | var opts cliOptions 62 | if _, err := flags.Parse(&opts); err != nil { 63 | log.Printf("%s", err) 64 | return 1 65 | } 66 | 67 | f, err := os.Open(opts.Schema) 68 | if err != nil { 69 | log.Printf("%s", err) 70 | return 1 71 | } 72 | defer f.Close() 73 | 74 | var m map[string]interface{} 75 | if err := json.NewDecoder(f).Decode(&m); err != nil { 76 | log.Printf("%s", err) 77 | return 1 78 | } 79 | 80 | // Extract possibly multiple schemas out of the main JSON document. 81 | var schemas []*schema.Schema 82 | ptrs := opts.Pointer 83 | if len(ptrs) == 0 { 84 | s, err := schema.ReadFile(opts.Schema) 85 | if err != nil { 86 | log.Printf("%s", err) 87 | return 1 88 | } 89 | schemas = []*schema.Schema{s} 90 | } else { 91 | for _, ptr := range ptrs { 92 | log.Printf("Resolving pointer '%s'", ptr) 93 | resolver, err := jspointer.New(ptr) 94 | if err != nil { 95 | log.Printf("%s", err) 96 | return 1 97 | } 98 | 99 | resolved, err := resolver.Get(m) 100 | if err != nil { 101 | log.Printf("%s", err) 102 | return 1 103 | } 104 | 105 | m2, ok := resolved.(map[string]interface{}) 106 | if !ok { 107 | log.Printf("Expected map") 108 | return 1 109 | } 110 | 111 | s := schema.New() 112 | if err := s.Extract(m2); err != nil { 113 | log.Printf("%s", err) 114 | return 1 115 | } 116 | schemas = append(schemas, s) 117 | } 118 | } 119 | 120 | b := builder.New() 121 | 122 | validators := make([]*jsval.JSVal, len(schemas)) 123 | for i, s := range schemas { 124 | v, err := b.BuildWithCtx(s, m) 125 | if err != nil { 126 | log.Printf("%s", err) 127 | return 1 128 | } 129 | if p := opts.Prefix; p != "" { 130 | v.Name = fmt.Sprintf("%s%d", p, i) 131 | } 132 | validators[i] = v 133 | } 134 | 135 | var out io.Writer 136 | 137 | out = os.Stdout 138 | if fn := opts.OutFile; fn != "" { 139 | f, err := os.Create(fn) 140 | if err != nil { 141 | log.Printf("%s", err) 142 | return 1 143 | } 144 | defer f.Close() 145 | 146 | out = f 147 | } 148 | 149 | g := jsval.NewGenerator() 150 | if err := g.Process(out, validators...); err != nil { 151 | log.Printf("%s", err) 152 | return 1 153 | } 154 | 155 | return 0 156 | } -------------------------------------------------------------------------------- /combinations.go: -------------------------------------------------------------------------------- 1 | package jsval 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/lestrrat-go/pdebug" 7 | ) 8 | 9 | func (c *comboconstraint) Add(v Constraint) { 10 | c.constraints = append(c.constraints, v) 11 | } 12 | 13 | func (c *comboconstraint) Constraints() []Constraint { 14 | return c.constraints 15 | } 16 | 17 | func reduceCombined(cc interface { 18 | Constraint 19 | Constraints() []Constraint 20 | }) Constraint { 21 | l := cc.Constraints() 22 | if len(l) == 1 { 23 | return l[0] 24 | } 25 | return cc 26 | } 27 | 28 | // Any creates a new AnyConstraint 29 | func Any() *AnyConstraint { 30 | return &AnyConstraint{} 31 | } 32 | 33 | // Reduce returns the child Constraint, if this constraint 34 | // has only 1 child constraint. 35 | func (c *AnyConstraint) Reduce() Constraint { 36 | return reduceCombined(c) 37 | } 38 | 39 | // Add appends a new Constraint 40 | func (c *AnyConstraint) Add(c2 Constraint) *AnyConstraint { 41 | c.comboconstraint.Add(c2) 42 | return c 43 | } 44 | 45 | // Validate validates the value against the input value. 46 | // For AnyConstraints, it will return success the moment 47 | // one child Constraint succeeds. It will return an error 48 | // if none of the child Constraints succeeds 49 | func (c *AnyConstraint) Validate(v interface{}) (err error) { 50 | if pdebug.Enabled { 51 | g := pdebug.Marker("AnyConstraint.Validate").BindError(&err) 52 | defer g.End() 53 | } 54 | for _, celem := range c.constraints { 55 | if err := celem.Validate(v); err == nil { 56 | return nil 57 | } 58 | } 59 | return errors.New("could not validate against any of the constraints") 60 | } 61 | 62 | // All creates a new AllConstraint 63 | func All() *AllConstraint { 64 | return &AllConstraint{} 65 | } 66 | 67 | // Reduce returns the child Constraint, if this constraint 68 | // has only 1 child constraint. 69 | func (c *AllConstraint) Reduce() Constraint { 70 | return reduceCombined(c) 71 | } 72 | 73 | // Add appends a new Constraint 74 | func (c *AllConstraint) Add(c2 Constraint) *AllConstraint { 75 | c.comboconstraint.Add(c2) 76 | return c 77 | } 78 | 79 | // Validate validates the value against the input value. 80 | // For AllConstraints, it will only return success if 81 | // all of the child Constraints succeeded. 82 | func (c *AllConstraint) Validate(v interface{}) (err error) { 83 | if pdebug.Enabled { 84 | g := pdebug.Marker("AllConstraint.Validate").BindError(&err) 85 | defer g.End() 86 | } 87 | 88 | for _, celem := range c.constraints { 89 | if err := celem.Validate(v); err != nil { 90 | return err 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | // OneOf creates a new OneOfConstraint 97 | func OneOf() *OneOfConstraint { 98 | return &OneOfConstraint{} 99 | } 100 | 101 | // Reduce returns the child Constraint, if this constraint 102 | // has only 1 child constraint. 103 | func (c *OneOfConstraint) Reduce() Constraint { 104 | return reduceCombined(c) 105 | } 106 | 107 | // Add appends a new Constraint 108 | func (c *OneOfConstraint) Add(c2 Constraint) *OneOfConstraint { 109 | c.comboconstraint.Add(c2) 110 | return c 111 | } 112 | 113 | // Validate validates the value against the input value. 114 | // For OneOfConstraints, it will return success only if 115 | // exactly 1 child Constraint succeeds. 116 | func (c *OneOfConstraint) Validate(v interface{}) (err error) { 117 | if pdebug.Enabled { 118 | g := pdebug.Marker("OneOfConstraint.Validate").BindError(&err) 119 | defer g.End() 120 | } 121 | 122 | count := 0 123 | for _, celem := range c.constraints { 124 | if err := celem.Validate(v); err == nil { 125 | count++ 126 | } 127 | } 128 | 129 | if count == 0 { 130 | return errors.New("none of the constraints passed") 131 | } else if count > 1 { 132 | return errors.New("more than 1 of the constraints passed") 133 | } 134 | return nil // Yes! 135 | } 136 | -------------------------------------------------------------------------------- /enum.go: -------------------------------------------------------------------------------- 1 | package jsval 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/lestrrat-go/pdebug" 7 | ) 8 | 9 | // Enum creates a new EnumConstraint 10 | func Enum(v ...interface{}) *EnumConstraint { 11 | return &EnumConstraint{enums: v} 12 | } 13 | 14 | // Enum method sets the possible enumerations 15 | func (c *EnumConstraint) Enum(v ...interface{}) *EnumConstraint { 16 | c.enums = v 17 | return c 18 | } 19 | 20 | // Validate validates the value against the list of enumerations 21 | func (c *EnumConstraint) Validate(v interface{}) (err error) { 22 | if pdebug.Enabled { 23 | g := pdebug.Marker("EnumConstraint.Validate (%s)", v).BindError(&err) 24 | defer g.End() 25 | } 26 | for _, e := range c.enums { 27 | if e == v { 28 | return nil 29 | } 30 | } 31 | return errors.New("value is not in enumeration") 32 | } 33 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package jsval_test 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/lestrrat-go/jsschema" 7 | "github.com/lestrrat-go/jsval" 8 | "github.com/lestrrat-go/jsval/builder" 9 | ) 10 | 11 | func ExampleBuild() { 12 | s, err := schema.ReadFile(`/path/to/schema.json`) 13 | if err != nil { 14 | log.Printf("failed to open schema: %s", err) 15 | return 16 | } 17 | 18 | b := builder.New() 19 | v, err := b.Build(s) 20 | if err != nil { 21 | log.Printf("failed to build validator: %s", err) 22 | return 23 | } 24 | 25 | var input interface{} 26 | if err := v.Validate(input); err != nil { 27 | log.Printf("validation failed: %s", err) 28 | return 29 | } 30 | } 31 | 32 | func ExampleManual() { 33 | v := jsval.Object(). 34 | AddProp(`zip`, jsval.String().RegexpString(`^\d{5}$`)). 35 | AddProp(`address`, jsval.String()). 36 | AddProp(`name`, jsval.String()). 37 | AddProp(`phone_number`, jsval.String().RegexpString(`^[\d-]+$`)). 38 | Required(`zip`, `address`, `name`) 39 | 40 | var input interface{} 41 | if err := v.Validate(input); err != nil { 42 | log.Printf("validation failed: %s", err) 43 | return 44 | } 45 | } -------------------------------------------------------------------------------- /generated_test.go: -------------------------------------------------------------------------------- 1 | package jsval_test 2 | 3 | import "testing" 4 | 5 | func TestGenerated(t *testing.T) { 6 | err := V0.Validate(map[string]interface{}{ 7 | "minItems": -1, 8 | }) 9 | if err == nil { 10 | t.Errorf("Validation failed: %s", err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /generated_validator_test.go: -------------------------------------------------------------------------------- 1 | package jsval_test 2 | 3 | import "github.com/lestrrat-go/jsval" 4 | 5 | var V0 *jsval.JSVal 6 | var M *jsval.ConstraintMap 7 | var R0 jsval.Constraint 8 | var R1 jsval.Constraint 9 | var R2 jsval.Constraint 10 | var R3 jsval.Constraint 11 | var R4 jsval.Constraint 12 | var R5 jsval.Constraint 13 | 14 | func init() { 15 | M = &jsval.ConstraintMap{} 16 | R0 = jsval.Object(). 17 | AdditionalProperties( 18 | jsval.EmptyConstraint, 19 | ). 20 | AddProp( 21 | "$schema", 22 | jsval.String().Format("uri"), 23 | ). 24 | AddProp( 25 | "additionalItems", 26 | jsval.Any(). 27 | Add( 28 | jsval.Boolean(), 29 | ). 30 | Add( 31 | jsval.Reference(M).RefersTo("#"), 32 | ), 33 | ). 34 | AddProp( 35 | "additionalProperties", 36 | jsval.Any(). 37 | Add( 38 | jsval.Boolean(), 39 | ). 40 | Add( 41 | jsval.Reference(M).RefersTo("#"), 42 | ), 43 | ). 44 | AddProp( 45 | "allOf", 46 | jsval.Reference(M).RefersTo("#/definitions/schemaArray"), 47 | ). 48 | AddProp( 49 | "anyOf", 50 | jsval.Reference(M).RefersTo("#/definitions/schemaArray"), 51 | ). 52 | AddProp( 53 | "default", 54 | jsval.EmptyConstraint, 55 | ). 56 | AddProp( 57 | "definitions", 58 | jsval.Object(). 59 | AdditionalProperties( 60 | jsval.Reference(M).RefersTo("#"), 61 | ), 62 | ). 63 | AddProp( 64 | "dependencies", 65 | jsval.Object(). 66 | AdditionalProperties( 67 | jsval.Any(). 68 | Add( 69 | jsval.Reference(M).RefersTo("#"), 70 | ). 71 | Add( 72 | jsval.Reference(M).RefersTo("#/definitions/stringArray"), 73 | ), 74 | ), 75 | ). 76 | AddProp( 77 | "description", 78 | jsval.String(), 79 | ). 80 | AddProp( 81 | "enum", 82 | jsval.Array(). 83 | AdditionalItems( 84 | jsval.EmptyConstraint, 85 | ). 86 | MinItems(1). 87 | UniqueItems(true), 88 | ). 89 | AddProp( 90 | "exclusiveMaximum", 91 | jsval.Boolean().Default(false), 92 | ). 93 | AddProp( 94 | "exclusiveMinimum", 95 | jsval.Boolean().Default(false), 96 | ). 97 | AddProp( 98 | "id", 99 | jsval.String().Format("uri"), 100 | ). 101 | AddProp( 102 | "items", 103 | jsval.Any(). 104 | Add( 105 | jsval.Reference(M).RefersTo("#"), 106 | ). 107 | Add( 108 | jsval.Reference(M).RefersTo("#/definitions/schemaArray"), 109 | ), 110 | ). 111 | AddProp( 112 | "maxItems", 113 | jsval.Reference(M).RefersTo("#/definitions/positiveInteger"), 114 | ). 115 | AddProp( 116 | "maxLength", 117 | jsval.Reference(M).RefersTo("#/definitions/positiveInteger"), 118 | ). 119 | AddProp( 120 | "maxProperties", 121 | jsval.Reference(M).RefersTo("#/definitions/positiveInteger"), 122 | ). 123 | AddProp( 124 | "maximum", 125 | jsval.Number(), 126 | ). 127 | AddProp( 128 | "minItems", 129 | jsval.Reference(M).RefersTo("#/definitions/positiveIntegerDefault0"), 130 | ). 131 | AddProp( 132 | "minLength", 133 | jsval.Reference(M).RefersTo("#/definitions/positiveIntegerDefault0"), 134 | ). 135 | AddProp( 136 | "minProperties", 137 | jsval.Reference(M).RefersTo("#/definitions/positiveIntegerDefault0"), 138 | ). 139 | AddProp( 140 | "minimum", 141 | jsval.Number(), 142 | ). 143 | AddProp( 144 | "multipleOf", 145 | jsval.Number().Minimum(0.000000).ExclusiveMinimum(true), 146 | ). 147 | AddProp( 148 | "not", 149 | jsval.Reference(M).RefersTo("#"), 150 | ). 151 | AddProp( 152 | "oneOf", 153 | jsval.Reference(M).RefersTo("#/definitions/schemaArray"), 154 | ). 155 | AddProp( 156 | "pattern", 157 | jsval.String().Format("regex"), 158 | ). 159 | AddProp( 160 | "patternProperties", 161 | jsval.Object(). 162 | AdditionalProperties( 163 | jsval.Reference(M).RefersTo("#"), 164 | ), 165 | ). 166 | AddProp( 167 | "properties", 168 | jsval.Object(). 169 | AdditionalProperties( 170 | jsval.Reference(M).RefersTo("#"), 171 | ), 172 | ). 173 | AddProp( 174 | "required", 175 | jsval.Reference(M).RefersTo("#/definitions/stringArray"), 176 | ). 177 | AddProp( 178 | "title", 179 | jsval.String(), 180 | ). 181 | AddProp( 182 | "type", 183 | jsval.Any(). 184 | Add( 185 | jsval.Reference(M).RefersTo("#/definitions/simpleTypes"), 186 | ). 187 | Add( 188 | jsval.Array(). 189 | Items( 190 | jsval.Reference(M).RefersTo("#/definitions/simpleTypes"), 191 | ). 192 | AdditionalItems( 193 | jsval.EmptyConstraint, 194 | ). 195 | MinItems(1). 196 | UniqueItems(true), 197 | ), 198 | ). 199 | AddProp( 200 | "uniqueItems", 201 | jsval.Boolean().Default(false), 202 | ). 203 | PropDependency("exclusiveMaximum", "maximum"). 204 | PropDependency("exclusiveMinimum", "minimum") 205 | R1 = jsval.Integer().Minimum(0) 206 | R2 = jsval.All(). 207 | Add( 208 | jsval.Reference(M).RefersTo("#/definitions/positiveInteger"), 209 | ). 210 | Add( 211 | jsval.EmptyConstraint, 212 | ) 213 | R3 = jsval.Array(). 214 | Items( 215 | jsval.Reference(M).RefersTo("#"), 216 | ). 217 | AdditionalItems( 218 | jsval.EmptyConstraint, 219 | ). 220 | MinItems(1) 221 | R4 = jsval.String().Enum("array", "boolean", "integer", "null", "number", "object", "string") 222 | R5 = jsval.Array(). 223 | Items( 224 | jsval.String(), 225 | ). 226 | AdditionalItems( 227 | jsval.EmptyConstraint, 228 | ). 229 | MinItems(1). 230 | UniqueItems(true) 231 | M.SetReference("#", R0) 232 | M.SetReference("#/definitions/positiveInteger", R1) 233 | M.SetReference("#/definitions/positiveIntegerDefault0", R2) 234 | M.SetReference("#/definitions/schemaArray", R3) 235 | M.SetReference("#/definitions/simpleTypes", R4) 236 | M.SetReference("#/definitions/stringArray", R5) 237 | V0 = jsval.New(). 238 | SetName("V0"). 239 | SetConstraintMap(M). 240 | SetRoot(R0) 241 | } -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | package jsval 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "go/format" 8 | "io" 9 | "os" 10 | "reflect" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | "unicode" 15 | ) 16 | 17 | // Generator is responsible for generating Go code that 18 | // sets up a validator 19 | type Generator struct{} 20 | 21 | // NewGenerator creates a new Generator 22 | func NewGenerator() *Generator { 23 | return &Generator{} 24 | } 25 | 26 | // Process takes a validator and prints out Go code to out. 27 | func (g *Generator) Process(out io.Writer, validators ...*JSVal) error { 28 | ctx := genctx{ 29 | pkgname: "jsval", 30 | refnames: make(map[string]string), 31 | vname: "V", 32 | } 33 | 34 | buf := bytes.Buffer{} 35 | 36 | // First get all of the references so we can refer to it later 37 | refs := map[string]Constraint{} 38 | refnames := []string{} 39 | valnames := []string{} 40 | for i, v := range validators { 41 | for rname, rc := range v.refs { 42 | if _, ok := refs[rname]; ok { 43 | continue 44 | } 45 | refs[rname] = rc 46 | refnames = append(refnames, rname) 47 | } 48 | 49 | if v.Name == "" { 50 | v.Name = fmt.Sprintf("V%d", i) 51 | } 52 | valnames = append(valnames, v.Name) 53 | } 54 | 55 | sort.Strings(valnames) 56 | for _, vname := range valnames { 57 | fmt.Fprintf(&buf, "\nvar %s *%s.JSVal", vname, ctx.pkgname) 58 | } 59 | 60 | ctx.refs = refs 61 | if len(refs) > 0 { // have refs 62 | ctx.cmname = "M" 63 | // sort them by reference name 64 | sort.Strings(refnames) 65 | fmt.Fprintf(&buf, "\nvar %s *%s.ConstraintMap", ctx.cmname, ctx.pkgname) 66 | 67 | // Generate reference constraint names 68 | for i, rname := range refnames { 69 | vname := fmt.Sprintf("R%d", i) 70 | ctx.refnames[rname] = vname 71 | fmt.Fprintf(&buf, "\nvar %s %s.Constraint", vname, ctx.pkgname) 72 | } 73 | } 74 | 75 | fmt.Fprintf(&buf, "\nfunc init() {") 76 | if len(refs) > 0 { 77 | fmt.Fprintf(&buf, "\n%s = &%s.ConstraintMap{}", ctx.cmname, ctx.pkgname) 78 | // Now generate code for references 79 | for _, rname := range refnames { 80 | fmt.Fprintf(&buf, "\n%s = ", ctx.refnames[rname]) 81 | rbuf := bytes.Buffer{} 82 | if err := generateCode(&ctx, &rbuf, ctx.refs[rname]); err != nil { 83 | return err 84 | } 85 | // Remove indentation here 86 | rs := rbuf.String() 87 | for i, r := range rs { 88 | if !unicode.IsSpace(r) { 89 | rs = rs[i:] 90 | break 91 | } 92 | } 93 | fmt.Fprint(&buf, rs) 94 | } 95 | 96 | for _, rname := range refnames { 97 | fmt.Fprintf(&buf, "\n%s.SetReference(%s, %s)", ctx.cmname, strconv.Quote(rname), ctx.refnames[rname]) 98 | } 99 | } 100 | 101 | // Now dump the validators 102 | sort.Sort(JSValSlice(validators)) 103 | for _, v := range validators { 104 | fmt.Fprintf(&buf, "\n%s = ", v.Name) 105 | if err := generateCode(&ctx, &buf, v); err != nil { 106 | return err 107 | } 108 | } 109 | fmt.Fprintf(&buf, "\n}") 110 | 111 | fsrc, err := format.Source(buf.Bytes()) 112 | if err != nil { 113 | os.Stderr.Write(buf.Bytes()) 114 | return err 115 | } 116 | out.Write(fsrc) 117 | return nil 118 | } 119 | 120 | type genctx struct { 121 | cmname string 122 | pkgname string 123 | refs map[string]Constraint 124 | refnames map[string]string 125 | vname string 126 | } 127 | 128 | func generateEmptyCode(ctx *genctx, out io.Writer, c emptyConstraint) error { 129 | fmt.Fprintf(out, "%s.EmptyConstraint", ctx.pkgname) 130 | return nil 131 | } 132 | 133 | func generateNullCode(ctx *genctx, out io.Writer, c nullConstraint) error { 134 | fmt.Fprintf(out, "%s.NullConstraint", ctx.pkgname) 135 | return nil 136 | } 137 | func generateValidatorCode(ctx *genctx, out io.Writer, v *JSVal) error { 138 | found := false 139 | fmt.Fprintf(out, "%s.New()", ctx.pkgname) 140 | fmt.Fprintf(out, ".\nSetName(%s)", strconv.Quote(v.Name)) 141 | 142 | if cmname := ctx.cmname; cmname != "" { 143 | fmt.Fprintf(out, ".\nSetConstraintMap(%s)", cmname) 144 | } 145 | 146 | for rname, rc := range ctx.refs { 147 | if v.root == rc { 148 | fmt.Fprintf(out, ".\nSetRoot(%s)", ctx.refnames[rname]) 149 | found = true 150 | break 151 | } 152 | } 153 | 154 | if !found { 155 | fmt.Fprint(out, ".\nSetRoot(\n") 156 | if err := generateCode(ctx, out, v.root); err != nil { 157 | return err 158 | } 159 | fmt.Fprint(out, ",\n)\n") 160 | } 161 | 162 | return nil 163 | } 164 | 165 | func generateCode(ctx *genctx, out io.Writer, c interface { 166 | Validate(interface{}) error 167 | }) error { 168 | buf := &bytes.Buffer{} 169 | 170 | switch c.(type) { 171 | case nullConstraint: 172 | if err := generateNullCode(ctx, buf, c.(nullConstraint)); err != nil { 173 | return err 174 | } 175 | case emptyConstraint: 176 | if err := generateEmptyCode(ctx, buf, c.(emptyConstraint)); err != nil { 177 | return err 178 | } 179 | case *JSVal: 180 | if err := generateValidatorCode(ctx, buf, c.(*JSVal)); err != nil { 181 | return err 182 | } 183 | case *AnyConstraint: 184 | if err := generateAnyCode(ctx, buf, c.(*AnyConstraint)); err != nil { 185 | return err 186 | } 187 | case *AllConstraint: 188 | if err := generateAllCode(ctx, buf, c.(*AllConstraint)); err != nil { 189 | return err 190 | } 191 | case *ArrayConstraint: 192 | if err := generateArrayCode(ctx, buf, c.(*ArrayConstraint)); err != nil { 193 | return err 194 | } 195 | case *BooleanConstraint: 196 | if err := generateBooleanCode(ctx, buf, c.(*BooleanConstraint)); err != nil { 197 | return err 198 | } 199 | case *IntegerConstraint: 200 | if err := generateIntegerCode(ctx, buf, c.(*IntegerConstraint)); err != nil { 201 | return err 202 | } 203 | case *NotConstraint: 204 | if err := generateNotCode(ctx, buf, c.(*NotConstraint)); err != nil { 205 | return err 206 | } 207 | case *NumberConstraint: 208 | if err := generateNumberCode(ctx, buf, c.(*NumberConstraint)); err != nil { 209 | return err 210 | } 211 | case *ObjectConstraint: 212 | if err := generateObjectCode(ctx, buf, c.(*ObjectConstraint)); err != nil { 213 | return err 214 | } 215 | case *OneOfConstraint: 216 | if err := generateOneOfCode(ctx, buf, c.(*OneOfConstraint)); err != nil { 217 | return err 218 | } 219 | case *ReferenceConstraint: 220 | if err := generateReferenceCode(ctx, buf, c.(*ReferenceConstraint)); err != nil { 221 | return err 222 | } 223 | case *StringConstraint: 224 | if err := generateStringCode(ctx, buf, c.(*StringConstraint)); err != nil { 225 | return err 226 | } 227 | } 228 | 229 | s := buf.String() 230 | s = strings.TrimSuffix(s, ".\n") 231 | fmt.Fprintf(out, s) 232 | 233 | return nil 234 | } 235 | 236 | func generateReferenceCode(ctx *genctx, out io.Writer, c *ReferenceConstraint) error { 237 | fmt.Fprintf(out, "%s.Reference(%s).RefersTo(%s)", ctx.pkgname, ctx.cmname, strconv.Quote(c.reference)) 238 | 239 | return nil 240 | } 241 | 242 | func generateComboCode(ctx *genctx, out io.Writer, name string, clist []Constraint) error { 243 | if len(clist) == 0 { 244 | return generateEmptyCode(ctx, out, EmptyConstraint) 245 | } 246 | fmt.Fprintf(out, "%s.%s()", ctx.pkgname, name) 247 | for _, c1 := range clist { 248 | fmt.Fprint(out, ".\nAdd(\n") 249 | if err := generateCode(ctx, out, c1); err != nil { 250 | return err 251 | } 252 | fmt.Fprint(out, ",\n)") 253 | } 254 | return nil 255 | } 256 | 257 | func generateAnyCode(ctx *genctx, out io.Writer, c *AnyConstraint) error { 258 | return generateComboCode(ctx, out, "Any", c.constraints) 259 | } 260 | 261 | func generateAllCode(ctx *genctx, out io.Writer, c *AllConstraint) error { 262 | return generateComboCode(ctx, out, "All", c.constraints) 263 | } 264 | 265 | func generateOneOfCode(ctx *genctx, out io.Writer, c *OneOfConstraint) error { 266 | return generateComboCode(ctx, out, "OneOf", c.constraints) 267 | } 268 | 269 | func generateIntegerCode(ctx *genctx, out io.Writer, c *IntegerConstraint) error { 270 | fmt.Fprintf(out, "%s.Integer()", ctx.pkgname) 271 | 272 | if c.applyMinimum { 273 | fmt.Fprintf(out, ".Minimum(%d)", int(c.minimum)) 274 | } 275 | 276 | if c.exclusiveMinimum { 277 | fmt.Fprintf(out, ".ExclusiveMinimum(true)") 278 | } 279 | 280 | if c.applyMaximum { 281 | fmt.Fprintf(out, ".Maximum(%d)", int(c.maximum)) 282 | } 283 | 284 | if c.exclusiveMaximum { 285 | fmt.Fprintf(out, ".ExclusiveMaximum(true)") 286 | } 287 | 288 | if c.HasDefault() { 289 | fmt.Fprintf(out, ".Default(%d)", int(c.DefaultValue().(float64))) 290 | } 291 | 292 | return nil 293 | } 294 | 295 | func generateNumberCode(ctx *genctx, out io.Writer, c *NumberConstraint) error { 296 | fmt.Fprintf(out, "%s.Number()", ctx.pkgname) 297 | 298 | if c.applyMinimum { 299 | fmt.Fprintf(out, ".Minimum(%f)", c.minimum) 300 | } 301 | 302 | if c.exclusiveMinimum { 303 | fmt.Fprintf(out, ".ExclusiveMinimum(true)") 304 | } 305 | 306 | if c.applyMaximum { 307 | fmt.Fprintf(out, ".Maximum(%f)", c.maximum) 308 | } 309 | 310 | if c.exclusiveMaximum { 311 | fmt.Fprintf(out, ".ExclusiveMaximum(true)") 312 | } 313 | 314 | if c.HasDefault() { 315 | fmt.Fprintf(out, ".Default(%f)", c.DefaultValue()) 316 | } 317 | 318 | return nil 319 | } 320 | 321 | func generateEnumCode(ctx *genctx, out io.Writer, c *EnumConstraint) error { 322 | fmt.Fprintf(out, "") 323 | l := len(c.enums) 324 | for i, v := range c.enums { 325 | rv := reflect.ValueOf(v) 326 | switch rv.Kind() { 327 | case reflect.String: 328 | fmt.Fprintf(out, "%s", strconv.Quote(rv.String())) 329 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 330 | fmt.Fprintf(out, "%d", rv.Int()) 331 | case reflect.Float32, reflect.Float64: 332 | fmt.Fprintf(out, "%f", rv.Float()) 333 | default: 334 | return fmt.Errorf("failed to stringify enum value %#v", rv.Interface()) 335 | } 336 | if i < l-1 { 337 | fmt.Fprintf(out, ", ") 338 | } 339 | } 340 | 341 | return nil 342 | } 343 | 344 | func generateStringCode(ctx *genctx, out io.Writer, c *StringConstraint) error { 345 | fmt.Fprintf(out, "%s.String()", ctx.pkgname) 346 | 347 | if c.maxLength > -1 { 348 | fmt.Fprintf(out, ".MaxLength(%d)", c.maxLength) 349 | } 350 | 351 | if c.minLength > 0 { 352 | fmt.Fprintf(out, ".MinLength(%d)", c.minLength) 353 | } 354 | 355 | if f := c.format; f != "" { 356 | fmt.Fprintf(out, ".Format(%s)", strconv.Quote(string(f))) 357 | } 358 | 359 | if rx := c.regexp; rx != nil { 360 | fmt.Fprintf(out, ".RegexpString(%s)", strconv.Quote(rx.String())) 361 | } 362 | 363 | if enum := c.enums; enum != nil { 364 | fmt.Fprintf(out, ".Enum(") 365 | if err := generateEnumCode(ctx, out, enum); err != nil { 366 | return err 367 | } 368 | fmt.Fprintf(out, ",)") 369 | } 370 | 371 | if c.HasDefault() { 372 | def := c.DefaultValue() 373 | switch def.(type) { 374 | case string: 375 | default: 376 | return errors.New("default value must be a string") 377 | } 378 | fmt.Fprintf(out, ".Default(%s)", strconv.Quote(def.(string))) 379 | } 380 | 381 | return nil 382 | } 383 | 384 | func generateObjectCode(ctx *genctx, out io.Writer, c *ObjectConstraint) error { 385 | fmt.Fprintf(out, "%s.Object()", ctx.pkgname) 386 | 387 | if c.HasDefault() { 388 | fmt.Fprintf(out, ".\nDefault(%s)", c.DefaultValue()) 389 | } 390 | 391 | if len(c.required) > 0 { 392 | fmt.Fprint(out, ".\nRequired(") 393 | l := len(c.required) 394 | pnames := make([]string, 0, l) 395 | for pname := range c.required { 396 | pnames = append(pnames, pname) 397 | } 398 | sort.Strings(pnames) 399 | for i, pname := range pnames { 400 | fmt.Fprint(out, strconv.Quote(pname)) 401 | if i < l-1 { 402 | fmt.Fprint(out, ", ") 403 | } 404 | } 405 | fmt.Fprint(out, ")") 406 | } 407 | 408 | if aprop := c.additionalProperties; aprop != nil { 409 | fmt.Fprintf(out, ".\nAdditionalProperties(\n") 410 | if err := generateCode(ctx, out, aprop); err != nil { 411 | return err 412 | } 413 | fmt.Fprintf(out, ",\n)") 414 | } 415 | 416 | pnames := make([]string, 0, len(c.properties)) 417 | for pname := range c.properties { 418 | pnames = append(pnames, pname) 419 | } 420 | sort.Strings(pnames) 421 | 422 | for _, pname := range pnames { 423 | pdef := c.properties[pname] 424 | 425 | fmt.Fprintf(out, ".\nAddProp(\n%s,\n", strconv.Quote(pname)) 426 | if err := generateCode(ctx, out, pdef); err != nil { 427 | return err 428 | } 429 | fmt.Fprint(out, ",\n)") 430 | } 431 | 432 | // patternProperties is a bit tricky, because the keys are 433 | // Regexp objects, and they don't have a sort.Regexp available 434 | // for us. It's easy to write one, but we'd have to stringify them 435 | // anyways, so we might as well just create a temporary container 436 | // with strings as keys 437 | ppmap := make(map[string]Constraint) 438 | ppnames := make([]string, 0, len(c.patternProperties)) 439 | for rx, ppc := range c.patternProperties { 440 | rxs := rx.String() 441 | ppmap[rxs] = ppc 442 | ppnames = append(ppnames, rxs) 443 | } 444 | sort.Strings(ppnames) 445 | for _, ppname := range ppnames { 446 | fmt.Fprintf(out, ".\nPatternPropertiesString(\n%s,\n", strconv.Quote(ppname)) 447 | if err := generateCode(ctx, out, ppmap[ppname]); err != nil { 448 | return err 449 | } 450 | fmt.Fprint(out, ",\n)") 451 | } 452 | 453 | if m := c.propdeps; len(m) > 0 { 454 | keys := make([]string, 0, len(m)) 455 | for from := range m { 456 | keys = append(keys, from) 457 | } 458 | sort.Strings(keys) 459 | 460 | for _, from := range keys { 461 | deplist := m[from] 462 | for _, to := range deplist { 463 | fmt.Fprintf(out, ".\nPropDependency(%s, %s)", strconv.Quote(from), strconv.Quote(to)) 464 | } 465 | } 466 | } 467 | 468 | return nil 469 | } 470 | 471 | func generateArrayCode(ctx *genctx, out io.Writer, c *ArrayConstraint) error { 472 | fmt.Fprintf(out, "%s.Array()", ctx.pkgname) 473 | 474 | if cc := c.items; cc != nil { 475 | fmt.Fprint(out, ".\nItems(\n") 476 | if err := generateCode(ctx, out, cc); err != nil { 477 | return err 478 | } 479 | fmt.Fprint(out, ",\n)") 480 | } 481 | 482 | if cc := c.additionalItems; cc != nil { 483 | fmt.Fprint(out, ".\nAdditionalItems(\n") 484 | if err := generateCode(ctx, out, cc); err != nil { 485 | return err 486 | } 487 | fmt.Fprintf(out, ",\n)") 488 | } 489 | 490 | if cc := c.positionalItems; len(cc) > 0 { 491 | fmt.Fprintf(out, ".\nPositionalItems([]%s.Constraint{\n", ctx.pkgname) 492 | for _, ccc := range cc { 493 | if err := generateCode(ctx, out, ccc); err != nil { 494 | } 495 | fmt.Fprintf(out, ",\n") 496 | } 497 | fmt.Fprint(out, "})") 498 | } 499 | if c.minItems > -1 { 500 | fmt.Fprintf(out, ".\nMinItems(%d)", c.minItems) 501 | } 502 | if c.maxItems > -1 { 503 | fmt.Fprintf(out, ".\nMaxItems(%d)", c.maxItems) 504 | } 505 | if c.uniqueItems { 506 | fmt.Fprint(out, ".\nUniqueItems(true)") 507 | } 508 | return nil 509 | } 510 | 511 | func generateBooleanCode(ctx *genctx, out io.Writer, c *BooleanConstraint) error { 512 | fmt.Fprintf(out, "%s.Boolean()", ctx.pkgname) 513 | if c.HasDefault() { 514 | fmt.Fprintf(out, ".Default(%t)", c.DefaultValue()) 515 | } 516 | return nil 517 | } 518 | 519 | func generateNotCode(ctx *genctx, out io.Writer, c *NotConstraint) error { 520 | fmt.Fprintf(out, "%s.Not(\n", ctx.pkgname) 521 | if err := generateCode(ctx, out, c.child); err != nil { 522 | return err 523 | } 524 | fmt.Fprint(out, "\n)") 525 | return nil 526 | } 527 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package jsval 2 | 3 | import ( 4 | "reflect" 5 | "regexp" 6 | "sync" 7 | 8 | "github.com/lestrrat-go/jsref" 9 | ) 10 | 11 | var zeroval = reflect.Value{} 12 | 13 | // JSVal is the main validator object. 14 | type JSVal struct { 15 | *ConstraintMap 16 | // Name is the name that will be used to generate code for this validator. 17 | // If unspecified, the generator will create variable names like `V0`, `V1`, 18 | // `V2`, etc. If you want to generate more meaningful names, you should 19 | // set this value manually. For example, if you are using jsval with a 20 | // scaffold generator, you might want to set this to a human-readable value 21 | Name string 22 | root Constraint 23 | resolver *jsref.Resolver 24 | } 25 | 26 | // JSValSlice is a list of JSVal validators. This exists in order to define 27 | // methods to satisfy the `sort.Interface` interface 28 | type JSValSlice []*JSVal 29 | 30 | // Constraint is an object that know how to validate 31 | // individual types of input 32 | type Constraint interface { 33 | DefaultValue() interface{} 34 | HasDefault() bool 35 | Validate(interface{}) error 36 | } 37 | 38 | type emptyConstraint struct{} 39 | 40 | // EmptyConstraint is a constraint that returns true for any value 41 | var EmptyConstraint = emptyConstraint{} 42 | 43 | type nullConstraint struct{} 44 | 45 | // NullConstraint is a constraint that only matches the JSON 46 | // "null" value, or "nil" in golang 47 | var NullConstraint = nullConstraint{} 48 | 49 | type defaultValue struct { 50 | initialized bool 51 | value interface{} 52 | } 53 | 54 | // BooleanConstraint implements a constraint to match against 55 | // a boolean. 56 | type BooleanConstraint struct { 57 | defaultValue 58 | } 59 | 60 | // StringConstraint implements a constraint to match against 61 | // a string 62 | type StringConstraint struct { 63 | defaultValue 64 | enums *EnumConstraint 65 | maxLength int 66 | minLength int 67 | regexp *regexp.Regexp 68 | format string 69 | } 70 | 71 | // NumberConstraint implements a constraint to match against 72 | // a number (i.e. float) type. 73 | type NumberConstraint struct { 74 | defaultValue 75 | applyMinimum bool 76 | applyMaximum bool 77 | applyMultipleOf bool 78 | minimum float64 79 | maximum float64 80 | multipleOf float64 81 | exclusiveMinimum bool 82 | exclusiveMaximum bool 83 | enums *EnumConstraint 84 | } 85 | 86 | // IntegerConstraint implements a constraint to match against 87 | // an integer type. Note that after checking if the value is 88 | // an int (i.e. floor(x) == x), the rest of the logic follows 89 | // exactly that of NumberConstraint 90 | type IntegerConstraint struct { 91 | NumberConstraint 92 | } 93 | 94 | // ArrayConstraint implements a constraint to match against 95 | // various aspects of a Array/Slice structure 96 | type ArrayConstraint struct { 97 | defaultValue 98 | items Constraint 99 | positionalItems []Constraint 100 | additionalItems Constraint 101 | minItems int 102 | maxItems int 103 | uniqueItems bool 104 | } 105 | 106 | // ObjectConstraint implements a constraint to match against 107 | // various aspects of a Map-like structure. 108 | type ObjectConstraint struct { 109 | defaultValue 110 | additionalProperties Constraint 111 | deplock sync.Mutex 112 | patternProperties map[*regexp.Regexp]Constraint 113 | proplock sync.Mutex 114 | properties map[string]Constraint 115 | propdeps map[string][]string 116 | reqlock sync.Mutex 117 | required map[string]struct{} 118 | maxProperties int 119 | minProperties int 120 | schemadeps map[string]Constraint 121 | 122 | // FieldNameFromName takes a struct wrapped in reflect.Value, and a 123 | // field name -- in JSON format (i.e. what you specified in your 124 | // JSON struct tags, or the actual field name). It returns the 125 | // struct field name to pass to `Value.FieldByName()`. If you do 126 | // not specify one, DefaultFieldNameFromName will be used. 127 | FieldNameFromName func(reflect.Value, string) string 128 | 129 | // FieldNamesFromStruct takes a struct wrapped in reflect.Value, and 130 | // returns the name of all public fields. Note that the returned 131 | // names will be JSON names, which may not necessarily be the same 132 | // as the field name. If you do not specify one, DefaultFieldNamesFromStruct 133 | // will be used 134 | FieldNamesFromStruct func(reflect.Value) []string 135 | } 136 | 137 | // EnumConstraint implements a constraint where the incoming 138 | // value must match one of the values enumerated in the constraint. 139 | // Note that due to various language specific reasons, you should 140 | // only use simple types where the "==" operator can distinguish 141 | // between different values 142 | type EnumConstraint struct { 143 | emptyConstraint 144 | enums []interface{} 145 | } 146 | 147 | type comboconstraint struct { 148 | emptyConstraint 149 | constraints []Constraint 150 | } 151 | 152 | // AnyConstraint implements a constraint where at least 1 153 | // child constraint must pass in order for this validation to pass. 154 | type AnyConstraint struct { 155 | comboconstraint 156 | } 157 | 158 | // AllConstraint implements a constraint where all of the 159 | // child constraints' validation must pass in order for this 160 | // validation to pass. 161 | type AllConstraint struct { 162 | comboconstraint 163 | } 164 | 165 | // OneOfConstraint implements a constraint where only exactly 166 | // one of the child constraints' validation can pass. If 167 | // none of the constraints passes, or more than 1 constraint 168 | // passes, the expressions deemed to have failed. 169 | type OneOfConstraint struct { 170 | comboconstraint 171 | } 172 | 173 | // NotConstraint implements a constraint where the result of 174 | // child constraint is negated -- that is, validation passes 175 | // only if the child constraint fails. 176 | type NotConstraint struct { 177 | child Constraint 178 | } 179 | -------------------------------------------------------------------------------- /internal/cmd/genmaybe/genmaybe.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/format" 7 | "log" 8 | "os" 9 | "sort" 10 | "strconv" 11 | ) 12 | 13 | const tPreamble = `package jsval_test 14 | 15 | import ( 16 | "testing" 17 | 18 | "github.com/lestrrat-go/jsval" 19 | ) 20 | 21 | ` 22 | 23 | const preamble = `package jsval 24 | 25 | import ( 26 | "bytes" 27 | "encoding/json" 28 | "reflect" 29 | "time" 30 | ) 31 | 32 | type ErrInvalidMaybeValue struct { 33 | Value interface{} 34 | } 35 | 36 | func (e ErrInvalidMaybeValue) Error() string { 37 | buf := bytes.Buffer{} 38 | buf.WriteString("invalid Maybe value: ") 39 | t := reflect.TypeOf(e.Value) 40 | switch t { 41 | case nil: 42 | buf.WriteString("(nil)") 43 | default: 44 | buf.WriteByte('(') 45 | buf.WriteString(t.String()) 46 | buf.WriteByte(')') 47 | } 48 | 49 | return buf.String() 50 | } 51 | 52 | // Maybe is an interface that can be used for struct fields which 53 | // want to differentiate between initialized and uninitialized state. 54 | // For example, a string field, if uninitialized, will contain the zero 55 | // value of "", but that empty string *could* be a valid value for 56 | // our validation purposes. 57 | // 58 | // To differentiate between an uninitialized string and an empty string, 59 | // you should wrap it with a wrapper that implements the Maybe interface 60 | // and JSVal will do its best to figure this out 61 | type Maybe interface { 62 | // Valid should return true if this value has been properly initialized. 63 | // If this returns false, JSVal will treat as if the field is has not been 64 | // provided at all. 65 | Valid() bool 66 | 67 | // Value should return whatever the underlying value is. 68 | Value() interface{} 69 | 70 | // Set sets a value to this Maybe value, and turns on the Valid flag. 71 | // An error may be returned if the value could not be set (e.g. 72 | // you provided a value with the wrong type) 73 | Set(interface{}) error 74 | 75 | // Reset clears the Maybe value, and sets the Valid flag to false. 76 | Reset() 77 | } 78 | 79 | type ValidFlag bool 80 | 81 | func (v *ValidFlag) Reset() { 82 | *v = false 83 | } 84 | 85 | func (v ValidFlag) Valid() bool { 86 | return bool(v) 87 | } 88 | 89 | ` 90 | 91 | func main() { 92 | types := map[string]string{ 93 | "String": "string", 94 | "Int": "int64", 95 | "Uint": "uint64", 96 | "Float": "float64", 97 | "Bool": "bool", 98 | "Time": "time.Time", 99 | } 100 | 101 | typenames := make([]string, 0, len(types)) 102 | for t := range types { 103 | typenames = append(typenames, t) 104 | } 105 | sort.Strings(typenames) 106 | 107 | genMaybe(types, typenames, "maybe.go") 108 | genMaybeTests(types, typenames, "maybe_gen_test.go") 109 | } 110 | 111 | func genMaybeTests(types map[string]string, typenames []string, fn string) { 112 | var buf bytes.Buffer 113 | buf.WriteString(tPreamble) 114 | buf.WriteString("\n" + `func TestSanity(t *testing.T) {`) 115 | for _, t := range typenames { 116 | fmt.Fprintf(&buf, "\n"+`t.Run(%s, func(t *testing.T) {`, 117 | strconv.Quote("Maybe"+t)) 118 | buf.WriteString("\nvar v jsval.Maybe") 119 | fmt.Fprintf(&buf, "\nv = &jsval.Maybe%s{}", t) 120 | buf.WriteString("\n_ = v") 121 | buf.WriteString("\n})") 122 | } 123 | buf.WriteString("\n}") 124 | 125 | writeFormatted(fn, buf.Bytes()) 126 | } 127 | 128 | func genMaybe(types map[string]string, typenames []string, fn string) { 129 | var buf bytes.Buffer 130 | buf.WriteString(preamble) 131 | for _, t := range typenames { 132 | bt := types[t] 133 | 134 | fmt.Fprintf(&buf, "\n\ntype Maybe%s struct{", t) 135 | buf.WriteString("\nValidFlag") 136 | fmt.Fprintf(&buf, "\n%s %s", t, bt) 137 | buf.WriteString("\n}") 138 | fmt.Fprintf(&buf, "\n\nfunc (v *Maybe%s) Set(x interface{}) error {", t) 139 | // Numeric types are special, because they can be converted. 140 | // float64 is included in the int/uint because JSON uses float64 141 | // to express numeric values, and we work with a lot of JSON 142 | switch t { 143 | case "Int": 144 | buf.WriteString("\nswitch x.(type) {") 145 | for _, ct := range []string{"int", "int8", "int16", "int32", "float64"} { 146 | fmt.Fprintf(&buf, "\ncase %s:", ct) 147 | fmt.Fprintf(&buf, "\nv.%s = int64(x.(%s))", t, ct) 148 | } 149 | buf.WriteString("\ncase int64:") 150 | fmt.Fprintf(&buf, "\nv.%s = x.(int64)", t) 151 | buf.WriteString("\ndefault:") 152 | buf.WriteString("\nreturn ErrInvalidMaybeValue{Value: x}") 153 | buf.WriteString("\n}") 154 | buf.WriteString("\nv.ValidFlag = true") 155 | buf.WriteString("\nreturn nil") 156 | buf.WriteString("\n}") 157 | case "Uint": 158 | buf.WriteString("\nswitch x.(type) {") 159 | for _, ct := range []string{"uint", "uint8", "uint16", "uint32", "float64"} { 160 | fmt.Fprintf(&buf, "\ncase %s:", ct) 161 | fmt.Fprintf(&buf, "\nv.%s = uint64(x.(%s))", t, ct) 162 | } 163 | buf.WriteString("\ncase uint64:") 164 | fmt.Fprintf(&buf, "\nv.%s = x.(uint64)", t) 165 | buf.WriteString("\ndefault:") 166 | buf.WriteString("\nreturn ErrInvalidMaybeValue{Value: x}") 167 | buf.WriteString("\n}") 168 | buf.WriteString("\nv.ValidFlag = true") 169 | buf.WriteString("\nreturn nil") 170 | buf.WriteString("\n}") 171 | case "Float": 172 | buf.WriteString("\nswitch x.(type) {") 173 | buf.WriteString("\ncase float32:") 174 | fmt.Fprintf(&buf, "\nv.%s = float64(x.(float32))", t) 175 | buf.WriteString("\ncase float64:") 176 | fmt.Fprintf(&buf, "\nv.%s = x.(float64)", t) 177 | buf.WriteString("\ndefault:") 178 | buf.WriteString("\nreturn ErrInvalidMaybeValue{Value: x}") 179 | buf.WriteString("\n}") 180 | buf.WriteString("\nv.ValidFlag = true") 181 | buf.WriteString("\nreturn nil") 182 | buf.WriteString("\n}") 183 | case "Time": 184 | buf.WriteString("\nswitch x.(type) {") 185 | buf.WriteString("\ncase string:") 186 | buf.WriteString("\ntv, err := time.Parse(time.RFC3339, x.(string))") 187 | buf.WriteString("\nif err != nil {") 188 | buf.WriteString("\nreturn err") 189 | buf.WriteString("\n}") 190 | buf.WriteString("\nv.ValidFlag = true") 191 | buf.WriteString("\nv.Time = tv") 192 | buf.WriteString("\ncase time.Time:") 193 | buf.WriteString("\nv.ValidFlag = true") 194 | buf.WriteString("\nv.Time = x.(time.Time)") 195 | buf.WriteString("\ndefault:") 196 | buf.WriteString("\nreturn ErrInvalidMaybeValue{Value: x}") 197 | buf.WriteString("\n}") 198 | buf.WriteString("\nreturn nil") 199 | buf.WriteString("\n}") 200 | default: 201 | fmt.Fprintf(&buf, "\ns, ok := x.(%s)", bt) 202 | buf.WriteString("\nif !ok {") 203 | buf.WriteString("\nreturn ErrInvalidMaybeValue{Value: x}") 204 | buf.WriteString("\n}") 205 | buf.WriteString("\nv.ValidFlag = true") 206 | fmt.Fprintf(&buf, "\nv.%s = s", t) 207 | buf.WriteString("\nreturn nil") 208 | buf.WriteString("\n}") 209 | } 210 | fmt.Fprintf(&buf, "\n\nfunc (v Maybe%s) Value() interface{} {", t) 211 | fmt.Fprintf(&buf, "\nreturn v.%s", t) 212 | buf.WriteString("\n}") 213 | 214 | if t == "Time" { 215 | // This has to be handled separately 216 | buf.WriteString("\n\nfunc (v MaybeTime) MarshalJSON() ([]byte, error) {") 217 | buf.WriteString("\nreturn json.Marshal(v.Time.Format(time.RFC3339))") 218 | buf.WriteString("\n}") 219 | buf.WriteString("\n\nfunc (v *MaybeTime) UnmarshalJSON(data []byte) error {") 220 | buf.WriteString("\nvar s string") 221 | buf.WriteString("\nif err := json.Unmarshal(data, &s); err != nil {") 222 | buf.WriteString("\n return err") 223 | buf.WriteString("\n}") 224 | buf.WriteString("\nt, err := time.Parse(time.RFC3339, s)") 225 | buf.WriteString("\nif err != nil {") 226 | buf.WriteString("\n return err") 227 | buf.WriteString("\n}") 228 | buf.WriteString("\nv.Time = t") 229 | buf.WriteString("\nreturn nil") 230 | buf.WriteString("\n}") 231 | } else { 232 | fmt.Fprintf(&buf, "\n\nfunc (v Maybe%s) MarshalJSON() ([]byte, error) {", t) 233 | fmt.Fprintf(&buf, "\nreturn json.Marshal(v.%s)", t) 234 | buf.WriteString("\n}") 235 | fmt.Fprintf(&buf, "\n\nfunc (v *Maybe%s) UnmarshalJSON(data []byte) error {", t) 236 | fmt.Fprintf(&buf, "\nvar in %s", bt) 237 | buf.WriteString("\nif err := json.Unmarshal(data, &in); err != nil {") 238 | buf.WriteString("\nreturn err") 239 | buf.WriteString("\n}") 240 | buf.WriteString("\nreturn v.Set(in)") 241 | buf.WriteString("\n}") 242 | } 243 | } 244 | 245 | writeFormatted(fn, buf.Bytes()) 246 | } 247 | 248 | func writeFormatted(fn string, code []byte) { 249 | fsrc, err := format.Source(code) 250 | if err != nil { 251 | log.Printf("Error formatting: %s", err) 252 | fsrc = code 253 | } 254 | 255 | fh, err := os.OpenFile(fn, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) 256 | if err != nil { 257 | log.Printf("Error opening file: %s", err) 258 | return 259 | } 260 | defer fh.Close() 261 | 262 | fh.Write(fsrc) 263 | } 264 | -------------------------------------------------------------------------------- /internal/cmd/gentest/gentest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "log" 7 | "os" 8 | 9 | schema "github.com/lestrrat-go/jsschema" 10 | jsval "github.com/lestrrat-go/jsval" 11 | "github.com/lestrrat-go/jsval/builder" 12 | ) 13 | 14 | func main() { 15 | os.Exit(_main()) 16 | } 17 | 18 | func _main() int { 19 | s, err := schema.ReadFile(os.Args[1]) 20 | if err != nil { 21 | log.Printf("%s", err) 22 | return 1 23 | } 24 | 25 | b := builder.New() 26 | v, err := b.Build(s) 27 | if err != nil { 28 | log.Printf("%s", err) 29 | return 1 30 | } 31 | 32 | var out bytes.Buffer 33 | out.WriteString("package jsval_test") 34 | out.WriteString("\n\nimport \"github.com/lestrrat-go/jsval\"") 35 | out.WriteString("\n") 36 | 37 | g := jsval.NewGenerator() 38 | if err := g.Process(&out, v); err != nil { 39 | log.Printf("%s", err) 40 | return 1 41 | } 42 | 43 | f, err := os.OpenFile(os.Args[2], os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) 44 | if err != nil { 45 | log.Printf("%s", err) 46 | return 1 47 | } 48 | defer f.Close() 49 | 50 | io.Copy(f, &out) 51 | return 0 52 | } 53 | -------------------------------------------------------------------------------- /jsval.go: -------------------------------------------------------------------------------- 1 | //go:generate go run internal/cmd/gentest/gentest.go schema.json generated_validator_test.go 2 | //go:generate go run internal/cmd/genmaybe/genmaybe.go 3 | 4 | // Package jsval implements an input validator, based on JSON Schema. 5 | // The main purpose is to validate JSON Schemas (see 6 | // https://github.com/lestrrat-go/jsschema), and to automatically 7 | // generate validators from schemas, but jsval can be used independently 8 | // of JSON Schema. 9 | package jsval 10 | 11 | import "github.com/pkg/errors" 12 | 13 | // New creates a new JSVal instance. 14 | func New() *JSVal { 15 | return &JSVal{ 16 | ConstraintMap: &ConstraintMap{}, 17 | } 18 | } 19 | 20 | // Validate validates the input, and return an error 21 | // if any of the validations fail 22 | func (v *JSVal) Validate(x interface{}) error { 23 | name := v.Name 24 | if len(name) == 0 { 25 | return errors.Wrapf(v.root.Validate(x), "validator %p failed", v) 26 | } 27 | return errors.Wrapf(v.root.Validate(x), "validator %s failed", name) 28 | } 29 | 30 | // SetName sets the name for the validator 31 | func (v *JSVal) SetName(s string) *JSVal { 32 | v.Name = s 33 | return v 34 | } 35 | 36 | // SetRoot sets the root Constraint object. 37 | func (v *JSVal) SetRoot(c Constraint) *JSVal { 38 | v.root = c 39 | return v 40 | } 41 | 42 | // Root returns the root Constraint object. 43 | func (v *JSVal) Root() Constraint { 44 | return v.root 45 | } 46 | 47 | // SetConstraintMap allows you to set the map that is referred to 48 | // when resolving JSON references within constraints. By setting 49 | // this to a common map, for example, you can share the same references 50 | // to save you some memory space and sanity. See an example in the 51 | // `generated_validator_test.go` file. 52 | func (v *JSVal) SetConstraintMap(cm *ConstraintMap) *JSVal { 53 | v.ConstraintMap = cm 54 | return v 55 | } 56 | 57 | func (p JSValSlice) Len() int { return len(p) } 58 | func (p JSValSlice) Less(i, j int) bool { return p[i].Name < p[j].Name } 59 | func (p JSValSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 60 | -------------------------------------------------------------------------------- /maybe.go: -------------------------------------------------------------------------------- 1 | package jsval 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "reflect" 7 | "time" 8 | ) 9 | 10 | type ErrInvalidMaybeValue struct { 11 | Value interface{} 12 | } 13 | 14 | func (e ErrInvalidMaybeValue) Error() string { 15 | buf := bytes.Buffer{} 16 | buf.WriteString("invalid Maybe value: ") 17 | t := reflect.TypeOf(e.Value) 18 | switch t { 19 | case nil: 20 | buf.WriteString("(nil)") 21 | default: 22 | buf.WriteByte('(') 23 | buf.WriteString(t.String()) 24 | buf.WriteByte(')') 25 | } 26 | 27 | return buf.String() 28 | } 29 | 30 | // Maybe is an interface that can be used for struct fields which 31 | // want to differentiate between initialized and uninitialized state. 32 | // For example, a string field, if uninitialized, will contain the zero 33 | // value of "", but that empty string *could* be a valid value for 34 | // our validation purposes. 35 | // 36 | // To differentiate between an uninitialized string and an empty string, 37 | // you should wrap it with a wrapper that implements the Maybe interface 38 | // and JSVal will do its best to figure this out 39 | type Maybe interface { 40 | // Valid should return true if this value has been properly initialized. 41 | // If this returns false, JSVal will treat as if the field is has not been 42 | // provided at all. 43 | Valid() bool 44 | 45 | // Value should return whatever the underlying value is. 46 | Value() interface{} 47 | 48 | // Set sets a value to this Maybe value, and turns on the Valid flag. 49 | // An error may be returned if the value could not be set (e.g. 50 | // you provided a value with the wrong type) 51 | Set(interface{}) error 52 | 53 | // Reset clears the Maybe value, and sets the Valid flag to false. 54 | Reset() 55 | } 56 | 57 | type ValidFlag bool 58 | 59 | func (v *ValidFlag) Reset() { 60 | *v = false 61 | } 62 | 63 | func (v ValidFlag) Valid() bool { 64 | return bool(v) 65 | } 66 | 67 | type MaybeBool struct { 68 | ValidFlag 69 | Bool bool 70 | } 71 | 72 | func (v *MaybeBool) Set(x interface{}) error { 73 | s, ok := x.(bool) 74 | if !ok { 75 | return ErrInvalidMaybeValue{Value: x} 76 | } 77 | v.ValidFlag = true 78 | v.Bool = s 79 | return nil 80 | } 81 | 82 | func (v MaybeBool) Value() interface{} { 83 | return v.Bool 84 | } 85 | 86 | func (v MaybeBool) MarshalJSON() ([]byte, error) { 87 | return json.Marshal(v.Bool) 88 | } 89 | 90 | func (v *MaybeBool) UnmarshalJSON(data []byte) error { 91 | var in bool 92 | if err := json.Unmarshal(data, &in); err != nil { 93 | return err 94 | } 95 | return v.Set(in) 96 | } 97 | 98 | type MaybeFloat struct { 99 | ValidFlag 100 | Float float64 101 | } 102 | 103 | func (v *MaybeFloat) Set(x interface{}) error { 104 | switch x.(type) { 105 | case float32: 106 | v.Float = float64(x.(float32)) 107 | case float64: 108 | v.Float = x.(float64) 109 | default: 110 | return ErrInvalidMaybeValue{Value: x} 111 | } 112 | v.ValidFlag = true 113 | return nil 114 | } 115 | 116 | func (v MaybeFloat) Value() interface{} { 117 | return v.Float 118 | } 119 | 120 | func (v MaybeFloat) MarshalJSON() ([]byte, error) { 121 | return json.Marshal(v.Float) 122 | } 123 | 124 | func (v *MaybeFloat) UnmarshalJSON(data []byte) error { 125 | var in float64 126 | if err := json.Unmarshal(data, &in); err != nil { 127 | return err 128 | } 129 | return v.Set(in) 130 | } 131 | 132 | type MaybeInt struct { 133 | ValidFlag 134 | Int int64 135 | } 136 | 137 | func (v *MaybeInt) Set(x interface{}) error { 138 | switch x.(type) { 139 | case int: 140 | v.Int = int64(x.(int)) 141 | case int8: 142 | v.Int = int64(x.(int8)) 143 | case int16: 144 | v.Int = int64(x.(int16)) 145 | case int32: 146 | v.Int = int64(x.(int32)) 147 | case float64: 148 | v.Int = int64(x.(float64)) 149 | case int64: 150 | v.Int = x.(int64) 151 | default: 152 | return ErrInvalidMaybeValue{Value: x} 153 | } 154 | v.ValidFlag = true 155 | return nil 156 | } 157 | 158 | func (v MaybeInt) Value() interface{} { 159 | return v.Int 160 | } 161 | 162 | func (v MaybeInt) MarshalJSON() ([]byte, error) { 163 | return json.Marshal(v.Int) 164 | } 165 | 166 | func (v *MaybeInt) UnmarshalJSON(data []byte) error { 167 | var in int64 168 | if err := json.Unmarshal(data, &in); err != nil { 169 | return err 170 | } 171 | return v.Set(in) 172 | } 173 | 174 | type MaybeString struct { 175 | ValidFlag 176 | String string 177 | } 178 | 179 | func (v *MaybeString) Set(x interface{}) error { 180 | s, ok := x.(string) 181 | if !ok { 182 | return ErrInvalidMaybeValue{Value: x} 183 | } 184 | v.ValidFlag = true 185 | v.String = s 186 | return nil 187 | } 188 | 189 | func (v MaybeString) Value() interface{} { 190 | return v.String 191 | } 192 | 193 | func (v MaybeString) MarshalJSON() ([]byte, error) { 194 | return json.Marshal(v.String) 195 | } 196 | 197 | func (v *MaybeString) UnmarshalJSON(data []byte) error { 198 | var in string 199 | if err := json.Unmarshal(data, &in); err != nil { 200 | return err 201 | } 202 | return v.Set(in) 203 | } 204 | 205 | type MaybeTime struct { 206 | ValidFlag 207 | Time time.Time 208 | } 209 | 210 | func (v *MaybeTime) Set(x interface{}) error { 211 | switch x.(type) { 212 | case string: 213 | tv, err := time.Parse(time.RFC3339, x.(string)) 214 | if err != nil { 215 | return err 216 | } 217 | v.ValidFlag = true 218 | v.Time = tv 219 | case time.Time: 220 | v.ValidFlag = true 221 | v.Time = x.(time.Time) 222 | default: 223 | return ErrInvalidMaybeValue{Value: x} 224 | } 225 | return nil 226 | } 227 | 228 | func (v MaybeTime) Value() interface{} { 229 | return v.Time 230 | } 231 | 232 | func (v MaybeTime) MarshalJSON() ([]byte, error) { 233 | return json.Marshal(v.Time.Format(time.RFC3339)) 234 | } 235 | 236 | func (v *MaybeTime) UnmarshalJSON(data []byte) error { 237 | var s string 238 | if err := json.Unmarshal(data, &s); err != nil { 239 | return err 240 | } 241 | t, err := time.Parse(time.RFC3339, s) 242 | if err != nil { 243 | return err 244 | } 245 | v.Time = t 246 | return nil 247 | } 248 | 249 | type MaybeUint struct { 250 | ValidFlag 251 | Uint uint64 252 | } 253 | 254 | func (v *MaybeUint) Set(x interface{}) error { 255 | switch x.(type) { 256 | case uint: 257 | v.Uint = uint64(x.(uint)) 258 | case uint8: 259 | v.Uint = uint64(x.(uint8)) 260 | case uint16: 261 | v.Uint = uint64(x.(uint16)) 262 | case uint32: 263 | v.Uint = uint64(x.(uint32)) 264 | case float64: 265 | v.Uint = uint64(x.(float64)) 266 | case uint64: 267 | v.Uint = x.(uint64) 268 | default: 269 | return ErrInvalidMaybeValue{Value: x} 270 | } 271 | v.ValidFlag = true 272 | return nil 273 | } 274 | 275 | func (v MaybeUint) Value() interface{} { 276 | return v.Uint 277 | } 278 | 279 | func (v MaybeUint) MarshalJSON() ([]byte, error) { 280 | return json.Marshal(v.Uint) 281 | } 282 | 283 | func (v *MaybeUint) UnmarshalJSON(data []byte) error { 284 | var in uint64 285 | if err := json.Unmarshal(data, &in); err != nil { 286 | return err 287 | } 288 | return v.Set(in) 289 | } 290 | -------------------------------------------------------------------------------- /maybe_gen_test.go: -------------------------------------------------------------------------------- 1 | package jsval_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/lestrrat-go/jsval" 7 | ) 8 | 9 | func TestSanity(t *testing.T) { 10 | t.Run("MaybeBool", func(t *testing.T) { 11 | var v jsval.Maybe 12 | v = &jsval.MaybeBool{} 13 | _ = v 14 | }) 15 | t.Run("MaybeFloat", func(t *testing.T) { 16 | var v jsval.Maybe 17 | v = &jsval.MaybeFloat{} 18 | _ = v 19 | }) 20 | t.Run("MaybeInt", func(t *testing.T) { 21 | var v jsval.Maybe 22 | v = &jsval.MaybeInt{} 23 | _ = v 24 | }) 25 | t.Run("MaybeString", func(t *testing.T) { 26 | var v jsval.Maybe 27 | v = &jsval.MaybeString{} 28 | _ = v 29 | }) 30 | t.Run("MaybeTime", func(t *testing.T) { 31 | var v jsval.Maybe 32 | v = &jsval.MaybeTime{} 33 | _ = v 34 | }) 35 | t.Run("MaybeUint", func(t *testing.T) { 36 | var v jsval.Maybe 37 | v = &jsval.MaybeUint{} 38 | _ = v 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /maybe_test.go: -------------------------------------------------------------------------------- 1 | package jsval_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/lestrrat-go/jsval" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | type TestMaybeStruct struct { 16 | Name jsval.MaybeString `json:"name"` 17 | Age int `json:"age"` 18 | } 19 | 20 | func TestMaybeString_Empty(t *testing.T) { 21 | const src = `{"age": 10}` 22 | 23 | var s TestMaybeStruct 24 | if !assert.NoError(t, json.NewDecoder(strings.NewReader(src)).Decode(&s), "Decode works") { 25 | return 26 | } 27 | 28 | v := jsval.New().SetRoot( 29 | jsval.Object(). 30 | AddProp("name", jsval.String()). 31 | AddProp("age", jsval.Integer()), 32 | ) 33 | if !assert.NoError(t, v.Validate(&s), "Validate succeeds") { 34 | return 35 | } 36 | } 37 | 38 | func TestMaybeString_Populated(t *testing.T) { 39 | const src = `{"age": 10, "name": "John Doe"}` 40 | 41 | var s TestMaybeStruct 42 | if !assert.NoError(t, json.NewDecoder(strings.NewReader(src)).Decode(&s), "Decode works") { 43 | return 44 | } 45 | 46 | v := jsval.New().SetRoot( 47 | jsval.Object(). 48 | AddProp("name", jsval.String()). 49 | AddProp("age", jsval.Integer()), 50 | ) 51 | if !assert.NoError(t, v.Validate(&s), "Validate succeeds") { 52 | return 53 | } 54 | } 55 | 56 | func TestMaybeString_EmptyDefault(t *testing.T) { 57 | const src = `{"age": 10}` 58 | 59 | var s TestMaybeStruct 60 | if !assert.NoError(t, json.NewDecoder(strings.NewReader(src)).Decode(&s), "Decode works") { 61 | return 62 | } 63 | 64 | v := jsval.New().SetRoot( 65 | jsval.Object(). 66 | AddProp("name", jsval.String().Default("John Doe")). 67 | AddProp("age", jsval.Integer()), 68 | ) 69 | if !assert.NoError(t, v.Validate(&s), "Validate succeeds") { 70 | return 71 | } 72 | 73 | if !assert.Equal(t, s.Name.Value().(string), "John Doe", "Should have default value") { 74 | return 75 | } 76 | } 77 | 78 | func TestMaybeInt(t *testing.T) { 79 | var i jsval.MaybeInt 80 | 81 | if !assert.NoError(t, i.Set(10), "const 10 can be set to MaybeInt (coersion takes place)") { 82 | return 83 | } 84 | 85 | if !assert.NoError(t, i.Set(10.0), "const 10.0 can be set to MaybeInt (coersion takes place)") { 86 | return 87 | } 88 | } 89 | 90 | func TestMaybeTime(t *testing.T) { 91 | var v jsval.MaybeTime 92 | 93 | x := time.Now().Truncate(time.Second) 94 | if !assert.NoError(t, v.Set(x), "set v to now") { 95 | return 96 | } 97 | 98 | var buf bytes.Buffer 99 | if !assert.NoError(t, json.NewEncoder(&buf).Encode(v), "json encoding works") { 100 | return 101 | } 102 | 103 | if !assert.Equal(t, strconv.Quote(x.Format(time.RFC3339))+"\n", buf.String()) { 104 | return 105 | } 106 | 107 | var d time.Time 108 | if !assert.NoError(t, json.NewDecoder(&buf).Decode(&d)) { 109 | return 110 | } 111 | 112 | // Use epoch time for more unambiguous comparison 113 | if !assert.Equal(t, x.Unix(), d.Unix()) { 114 | return 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /number.go: -------------------------------------------------------------------------------- 1 | package jsval 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | "reflect" 7 | 8 | "github.com/lestrrat-go/pdebug" 9 | ) 10 | 11 | // Enum specifies the values that this constraint can have 12 | func (nc *NumberConstraint) Enum(l ...interface{}) *NumberConstraint { 13 | if nc.enums == nil { 14 | nc.enums = Enum() 15 | } 16 | nc.enums.Enum(l...) 17 | return nc 18 | } 19 | 20 | // Default specifies the default value for the given value 21 | func (nc *NumberConstraint) Default(v interface{}) *NumberConstraint { 22 | nc.defaultValue.initialized = true 23 | nc.defaultValue.value = v 24 | return nc 25 | } 26 | 27 | // Maximum sepcifies the maximum value that the constraint can allow 28 | func (nc *NumberConstraint) Maximum(n float64) *NumberConstraint { 29 | nc.applyMaximum = true 30 | nc.maximum = n 31 | return nc 32 | } 33 | 34 | // Minimum sepcifies the minimum value that the constraint can allow 35 | func (nc *NumberConstraint) Minimum(n float64) *NumberConstraint { 36 | nc.applyMinimum = true 37 | nc.minimum = n 38 | return nc 39 | } 40 | 41 | // MultipleOf specifies the number that the given value must be 42 | // divisible by. That is, the constraint will return an error unless 43 | // the given value satisfies `math.Mod(v, n) == 0` 44 | func (nc *NumberConstraint) MultipleOf(n float64) *NumberConstraint { 45 | nc.applyMultipleOf = true 46 | nc.multipleOf = n 47 | return nc 48 | } 49 | 50 | // ExclusiveMinimum specifies if the minimum value should be considered 51 | // a valid value 52 | func (nc *NumberConstraint) ExclusiveMinimum(b bool) *NumberConstraint { 53 | nc.exclusiveMinimum = b 54 | return nc 55 | } 56 | 57 | // ExclusiveMaximum specifies if the maximum value should be considered 58 | // a valid value 59 | func (nc *NumberConstraint) ExclusiveMaximum(b bool) *NumberConstraint { 60 | nc.exclusiveMaximum = b 61 | return nc 62 | } 63 | 64 | // Validate validates the value against this constraint 65 | func (nc *NumberConstraint) Validate(v interface{}) (err error) { 66 | if pdebug.Enabled { 67 | g := pdebug.IPrintf("START NumberConstraint.Validate") 68 | defer func() { 69 | if err == nil { 70 | g.IRelease("END NumberConstraint.Validate (PASS)") 71 | } else { 72 | g.IRelease("END NumberConstraint.Validate (FAIL): %s", err) 73 | } 74 | }() 75 | } 76 | 77 | rv := reflect.ValueOf(v) 78 | switch rv.Kind() { 79 | case reflect.Ptr, reflect.Interface: 80 | rv = rv.Elem() 81 | } 82 | 83 | switch rv.Kind() { 84 | case reflect.Float32, reflect.Float64: 85 | default: 86 | return errors.New("value is not a float") 87 | } 88 | 89 | f := rv.Float() 90 | if nc.applyMinimum { 91 | if pdebug.Enabled { 92 | pdebug.Printf("Checking Minimum (%f)", nc.minimum) 93 | } 94 | 95 | if nc.exclusiveMinimum { 96 | if nc.minimum >= f { 97 | return errors.New("numeric value is less than the minimum (exclusive minimum)") 98 | } 99 | } else { 100 | if nc.minimum > f { 101 | return errors.New("numeric value is less than the minimum") 102 | } 103 | } 104 | } 105 | 106 | if nc.applyMaximum { 107 | if pdebug.Enabled { 108 | pdebug.Printf("Checking Maximum (%f)", nc.maximum) 109 | } 110 | if nc.exclusiveMaximum { 111 | if nc.maximum <= f { 112 | return errors.New("numeric value is greater than maximum (exclusive maximum)") 113 | } 114 | } else { 115 | if nc.maximum < f { 116 | return errors.New("numeric value is greater than maximum") 117 | } 118 | } 119 | } 120 | 121 | if nc.applyMultipleOf { 122 | if pdebug.Enabled { 123 | pdebug.Printf("Checking MultipleOf (%f)", nc.multipleOf) 124 | } 125 | 126 | if nc.multipleOf != 0 { 127 | if math.Mod(f, nc.multipleOf) != 0 { 128 | return errors.New("numeric value is fails multipleOf validation") 129 | } 130 | } 131 | } 132 | 133 | if enum := nc.enums; enum != nil { 134 | if err := enum.Validate(f); err != nil { 135 | return err 136 | } 137 | } 138 | 139 | return nil 140 | } 141 | 142 | // Number creates a new NumberConstraint 143 | func Number() *NumberConstraint { 144 | return &NumberConstraint{ 145 | applyMinimum: false, 146 | applyMaximum: false, 147 | } 148 | } 149 | 150 | // Integer creates a new IntegerrConstraint 151 | func Integer() *IntegerConstraint { 152 | c := &IntegerConstraint{} 153 | c.applyMinimum = false 154 | c.applyMaximum = false 155 | return c 156 | } 157 | 158 | // Default specifies the default value for the given value 159 | func (ic *IntegerConstraint) Default(v interface{}) *IntegerConstraint { 160 | ic.NumberConstraint.Default(v) 161 | return ic 162 | } 163 | 164 | // Maximum sepcifies the maximum value that the constraint can allow 165 | // Maximum sepcifies the maximum value that the constraint can allow 166 | func (ic *IntegerConstraint) Maximum(n float64) *IntegerConstraint { 167 | ic.applyMaximum = true 168 | ic.maximum = n 169 | return ic 170 | } 171 | 172 | // Minimum sepcifies the minimum value that the constraint can allow 173 | func (ic *IntegerConstraint) Minimum(n float64) *IntegerConstraint { 174 | ic.applyMinimum = true 175 | ic.minimum = n 176 | return ic 177 | } 178 | 179 | // ExclusiveMinimum specifies if the minimum value should be considered 180 | // a valid value 181 | func (ic *IntegerConstraint) ExclusiveMinimum(b bool) *IntegerConstraint { 182 | ic.NumberConstraint.ExclusiveMinimum(b) 183 | return ic 184 | } 185 | 186 | // ExclusiveMaximum specifies if the maximum value should be considered 187 | // a valid value 188 | func (ic *IntegerConstraint) ExclusiveMaximum(b bool) *IntegerConstraint { 189 | ic.NumberConstraint.ExclusiveMaximum(b) 190 | return ic 191 | } 192 | 193 | // Validate validates the value against integer validation rules. 194 | // Note that because when Go decodes JSON it FORCES float64 on numbers, 195 | // this method will return true even if the *type* of the value is 196 | // float32/64. We just check that `math.Floor(v) == v` 197 | func (ic *IntegerConstraint) Validate(v interface{}) (err error) { 198 | if pdebug.Enabled { 199 | g := pdebug.IPrintf("START IntegerConstraint.Validate") 200 | defer func() { 201 | if err == nil { 202 | g.IRelease("END IntegerConstraint.Validate (PASS)") 203 | } else { 204 | g.IRelease("END IntegerConstraint.Validate (FAIL): %s", err) 205 | } 206 | }() 207 | } 208 | 209 | rv := reflect.ValueOf(v) 210 | switch rv.Kind() { 211 | case reflect.Interface, reflect.Ptr: 212 | rv = rv.Elem() 213 | } 214 | 215 | switch rv.Kind() { 216 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 217 | return ic.NumberConstraint.Validate(float64(rv.Int())) 218 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 219 | return ic.NumberConstraint.Validate(float64(rv.Uint())) 220 | case reflect.Float32, reflect.Float64: 221 | fv := rv.Float() 222 | if math.Floor(fv) != fv { 223 | return errors.New("value is not an int/uint") 224 | } 225 | return ic.NumberConstraint.Validate(fv) 226 | default: 227 | return errors.New("value is not numeric") 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /number_test.go: -------------------------------------------------------------------------------- 1 | package jsval_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/lestrrat-go/jsschema" 8 | "github.com/lestrrat-go/jsval" 9 | "github.com/lestrrat-go/jsval/builder" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestNumberFromSchema(t *testing.T) { 14 | const src = `{ 15 | "type": "number", 16 | "minimum": 5, 17 | "maximum": 15, 18 | "default": 10 19 | }` 20 | 21 | s, err := schema.Read(strings.NewReader(src)) 22 | if !assert.NoError(t, err, "schema.Read should succeed") { 23 | return 24 | } 25 | 26 | b := builder.New() 27 | v, err := b.Build(s) 28 | if !assert.NoError(t, err, "Builder.Build should succeed") { 29 | return 30 | } 31 | 32 | c2 := jsval.Number() 33 | c2.Default(float64(10)).Maximum(15).Minimum(5) 34 | if !assert.Equal(t, c2, v.Root(), "constraints are equal") { 35 | return 36 | } 37 | } 38 | 39 | func TestNumber(t *testing.T) { 40 | c := jsval.Number() 41 | c.Default(float64(10)).Maximum(15) 42 | 43 | if !assert.True(t, c.HasDefault(), "HasDefault is true") { 44 | return 45 | } 46 | 47 | if !assert.Equal(t, c.DefaultValue(), float64(10), "DefaultValue returns expected value") { 48 | return 49 | } 50 | 51 | var s float64 52 | if !assert.NoError(t, c.Validate(s), "validate should succeed") { 53 | return 54 | } 55 | 56 | c.Minimum(5) 57 | if !assert.Error(t, c.Validate(s), "validate should fail") { 58 | return 59 | } 60 | 61 | s = 10 62 | if !assert.NoError(t, c.Validate(s), "validate should succeed") { 63 | return 64 | } 65 | } -------------------------------------------------------------------------------- /object.go: -------------------------------------------------------------------------------- 1 | package jsval 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "regexp" 7 | 8 | "github.com/lestrrat-go/pdebug" 9 | ) 10 | 11 | type getPropValuer interface { 12 | // Given a JSON property name, return the actual field value 13 | GetPropValue(string) (interface{}, error) 14 | } 15 | 16 | type getPropNameser interface { 17 | // Given a lst of JSON property names 18 | GetPropNames() ([]string, error) 19 | } 20 | 21 | // Object creates a new ObjectConstraint 22 | func Object() *ObjectConstraint { 23 | return &ObjectConstraint{ 24 | additionalProperties: nil, 25 | minProperties: -1, 26 | maxProperties: -1, 27 | patternProperties: make(map[*regexp.Regexp]Constraint), 28 | properties: make(map[string]Constraint), 29 | propdeps: make(map[string][]string), 30 | required: make(map[string]struct{}), 31 | schemadeps: make(map[string]Constraint), 32 | } 33 | } 34 | 35 | // Required specifies required property names 36 | func (o *ObjectConstraint) Required(l ...string) *ObjectConstraint { 37 | o.reqlock.Lock() 38 | defer o.reqlock.Unlock() 39 | 40 | for _, pname := range l { 41 | o.required[pname] = struct{}{} 42 | } 43 | return o 44 | } 45 | 46 | // IsPropRequired returns true if the given name is listed under 47 | // the required properties 48 | func (o *ObjectConstraint) IsPropRequired(s string) bool { 49 | o.reqlock.Lock() 50 | defer o.reqlock.Unlock() 51 | 52 | _, ok := o.required[s] 53 | return ok 54 | } 55 | 56 | // MinProperties specifies the minimum number of properties this 57 | // constraint can allow. If unspecified, it is not checked. 58 | func (o *ObjectConstraint) MinProperties(n int) *ObjectConstraint { 59 | o.minProperties = n 60 | return o 61 | } 62 | 63 | // MaxProperties specifies the maximum number of properties this 64 | // constraint can allow. If unspecified, it is not checked. 65 | func (o *ObjectConstraint) MaxProperties(n int) *ObjectConstraint { 66 | o.maxProperties = n 67 | return o 68 | } 69 | 70 | // AdditionalProperties specifies the constraint that additional 71 | // properties should be validated against. 72 | func (o *ObjectConstraint) AdditionalProperties(c Constraint) *ObjectConstraint { 73 | o.additionalProperties = c 74 | return o 75 | } 76 | 77 | // AddProp adds constraints for a named property. 78 | func (o *ObjectConstraint) AddProp(name string, c Constraint) *ObjectConstraint { 79 | o.proplock.Lock() 80 | defer o.proplock.Unlock() 81 | 82 | o.properties[name] = c 83 | return o 84 | } 85 | 86 | // PatternProperties specifies constraints that properties matching 87 | // this pattern must be validated against. Note that properties listed 88 | // using `AddProp` takes precedence. 89 | func (o *ObjectConstraint) PatternProperties(key *regexp.Regexp, c Constraint) *ObjectConstraint { 90 | o.proplock.Lock() 91 | defer o.proplock.Unlock() 92 | 93 | o.patternProperties[key] = c 94 | return o 95 | } 96 | 97 | // PatternPropertiesString is the same as PatternProperties, but takes 98 | // a string representing a regular expression. If the regular expression 99 | // cannot be compiled, a panic occurs. 100 | func (o *ObjectConstraint) PatternPropertiesString(key string, c Constraint) *ObjectConstraint { 101 | rx := regexp.MustCompile(key) 102 | return o.PatternProperties(rx, c) 103 | } 104 | 105 | // PropDependency specifies properties that must be present when 106 | // `from` is present. 107 | func (o *ObjectConstraint) PropDependency(from string, to ...string) *ObjectConstraint { 108 | o.deplock.Lock() 109 | defer o.deplock.Unlock() 110 | 111 | l := o.propdeps[from] 112 | l = append(l, to...) 113 | o.propdeps[from] = l 114 | return o 115 | } 116 | 117 | // SchemaDependency specifies a schema that the value being validated 118 | // must also satisfy. Note that the "object" is the target that needs to 119 | // be additionally validated, not the value of the `from` property 120 | func (o *ObjectConstraint) SchemaDependency(from string, c Constraint) *ObjectConstraint { 121 | o.deplock.Lock() 122 | defer o.deplock.Unlock() 123 | 124 | o.schemadeps[from] = c 125 | return o 126 | } 127 | 128 | // GetPropDependencies returns the list of property names that must 129 | // be present for given property name `from` 130 | func (o *ObjectConstraint) GetPropDependencies(from string) []string { 131 | o.deplock.Lock() 132 | defer o.deplock.Unlock() 133 | 134 | l, ok := o.propdeps[from] 135 | if !ok { 136 | return nil 137 | } 138 | 139 | return l 140 | } 141 | 142 | // GetSchemaDependency returns the Constraint that must be used when 143 | // the property `from` is present. 144 | func (o *ObjectConstraint) GetSchemaDependency(from string) Constraint { 145 | o.deplock.Lock() 146 | defer o.deplock.Unlock() 147 | 148 | c, ok := o.schemadeps[from] 149 | if !ok { 150 | return nil 151 | } 152 | 153 | return c 154 | } 155 | 156 | var structInfoRegistry = StructInfoRegistry{ 157 | registry: make(map[reflect.Type]StructInfo), 158 | } 159 | 160 | // getProps return all of the property names for this object. 161 | // XXX Map keys can be something other than strings, but 162 | // we can't really allow it? 163 | func (o *ObjectConstraint) getPropNames(rv reflect.Value) ([]string, error) { 164 | switch rv.Kind() { 165 | case reflect.Ptr, reflect.Interface: 166 | rv = rv.Elem() 167 | } 168 | 169 | var keys []string 170 | switch rv.Kind() { 171 | case reflect.Map: 172 | vk := rv.MapKeys() 173 | keys = make([]string, len(vk)) 174 | for i, v := range vk { 175 | if v.Kind() != reflect.String { 176 | return nil, errors.New("panic: can only handle maps with string keys") 177 | } 178 | keys[i] = v.String() 179 | } 180 | case reflect.Struct: 181 | if gpv, ok := rv.Interface().(getPropNameser); ok { 182 | pv, err := gpv.GetPropNames() 183 | if err == nil { 184 | return pv, nil 185 | } 186 | } 187 | 188 | si, ok := structInfoRegistry.Lookup(rv.Type()) 189 | if !ok { 190 | si = structInfoRegistry.Register(rv.Type()) 191 | } 192 | 193 | return si.PropNames(rv), nil 194 | default: 195 | return nil, errors.New("cannot get property names from this value (Kind: " + rv.Kind().String() + ")") 196 | } 197 | 198 | return keys, nil 199 | } 200 | 201 | var ( 202 | maybefloatT = reflect.TypeOf(MaybeFloat{}) 203 | maybeintT = reflect.TypeOf(MaybeInt{}) 204 | intT = reflect.TypeOf(int64(0)) 205 | floatT = reflect.TypeOf(float64(0)) 206 | ) 207 | 208 | func coerceValue(v interface{}, t reflect.Type) reflect.Value { 209 | vv := reflect.ValueOf(v) 210 | switch vv.Kind() { 211 | case reflect.Interface: 212 | vv = vv.Elem() 213 | } 214 | 215 | // For known Maybe types, we should do our best, too 216 | switch { 217 | case t == maybefloatT: 218 | t = floatT 219 | case t == maybeintT: 220 | t = intT 221 | } 222 | 223 | if vv.Type().ConvertibleTo(t) { 224 | return vv.Convert(t) 225 | } 226 | return vv 227 | } 228 | 229 | type setPropValuer interface { 230 | SetPropValue(string, interface{}) error 231 | } 232 | 233 | var spvType = reflect.TypeOf((*setPropValuer)(nil)).Elem() 234 | 235 | func (o *ObjectConstraint) setProp(rv reflect.Value, pname string, val interface{}) error { 236 | switch rv.Kind() { 237 | case reflect.Ptr, reflect.Interface: 238 | rv = rv.Elem() 239 | } 240 | 241 | switch rv.Kind() { 242 | case reflect.Map: 243 | rv.SetMapIndex(reflect.ValueOf(pname), reflect.ValueOf(val)) 244 | return nil 245 | case reflect.Struct: 246 | spvm := rv.MethodByName("SetPropValue") 247 | if spvm != zeroval { 248 | out := spvm.Call([]reflect.Value{reflect.ValueOf(pname), reflect.ValueOf(val)}) 249 | if !out[0].IsNil() { 250 | return out[0].Interface().(error) 251 | } 252 | return nil 253 | } 254 | 255 | f := o.getProp(rv, pname) 256 | if f == zeroval { 257 | return errors.New("setProp: could not find field '" + pname + "'") 258 | } 259 | 260 | // Usability: If you specify `Default(10)` on an int64 value, 261 | // it doesn't work. But these values are compatible. We should 262 | // do our best to align them 263 | dv := coerceValue(val, f.Type()) 264 | 265 | // Is this a Maybe value? If so, we should use its Set() method 266 | var mv reflect.Value 267 | switch { 268 | case f.Type().Implements(maybeif): 269 | mv = f.MethodByName("Set") 270 | case f.CanAddr() && f.Addr().Type().Implements(maybeif): 271 | mv = f.Addr().MethodByName("Set") 272 | default: 273 | f.Set(dv) 274 | return nil 275 | } 276 | 277 | if mv == zeroval { 278 | return errors.New("setProp: could not get 'Set' method for value") 279 | } 280 | 281 | out := mv.Call([]reflect.Value{dv}) 282 | if !out[0].IsNil() { 283 | return out[0].Interface().(error) 284 | } 285 | return nil 286 | default: 287 | return errors.New("setProp: don't know what to do with '" + rv.Kind().String() + "'") 288 | } 289 | } 290 | 291 | func (o *ObjectConstraint) getProp(rv reflect.Value, pname string) reflect.Value { 292 | switch rv.Kind() { 293 | case reflect.Map: 294 | pv := reflect.ValueOf(pname) 295 | return rv.MapIndex(pv) 296 | case reflect.Struct: 297 | // This guy knows how to grab the value, given a name. Use that 298 | if gpv, ok := rv.Interface().(getPropValuer); ok { 299 | pv, err := gpv.GetPropValue(pname) 300 | if err == nil { 301 | return reflect.ValueOf(pv) 302 | } 303 | } 304 | 305 | si, ok := structInfoRegistry.Lookup(rv.Type()) 306 | if !ok { 307 | si = structInfoRegistry.Register(rv.Type()) 308 | } 309 | 310 | fn, ok := si.FieldName(pname) 311 | if !ok { 312 | if pdebug.Enabled { 313 | pdebug.Printf("Could not resolve name '%s'", pname) 314 | } 315 | return zeroval 316 | } 317 | return rv.FieldByName(fn) 318 | default: 319 | return zeroval 320 | } 321 | } 322 | 323 | // Validate validates the given value against this ObjectConstraint 324 | func (o *ObjectConstraint) Validate(v interface{}) (err error) { 325 | if pdebug.Enabled { 326 | g := pdebug.IPrintf("START ObjectConstraint.Validate") 327 | defer func() { 328 | if err == nil { 329 | g.IRelease("END ObjectConstraint.Validate (PASS)") 330 | } else { 331 | g.IRelease("END ObjectConstraint.Validate (FAIL): %s", err) 332 | } 333 | }() 334 | } 335 | 336 | rv := reflect.ValueOf(v) 337 | switch rv.Kind() { 338 | case reflect.Ptr, reflect.Interface: 339 | rv = rv.Elem() 340 | } 341 | 342 | fields, err := o.getPropNames(rv) 343 | if err != nil { 344 | return err 345 | } 346 | 347 | lf := len(fields) 348 | if o.minProperties > -1 && lf < o.minProperties { 349 | return errors.New("fewer properties than minProperties") 350 | } 351 | if o.maxProperties > -1 && lf > o.maxProperties { 352 | return errors.New("more properties than maxProperties") 353 | } 354 | 355 | // Find the list of field names that were passed to us 356 | // "premain" shows extra props, if any. 357 | // "pseen" shows props that we have already seen 358 | premain := map[string]struct{}{} 359 | pseen := map[string]struct{}{} 360 | for _, k := range fields { 361 | premain[k] = struct{}{} 362 | } 363 | 364 | // Now, for all known constraints, validate the prop 365 | // create a copy of properties so that we don't have to keep the lock 366 | propdefs := make(map[string]Constraint) 367 | o.proplock.Lock() 368 | for pname, c := range o.properties { 369 | propdefs[pname] = c 370 | } 371 | o.proplock.Unlock() 372 | 373 | for pname, c := range propdefs { 374 | if pdebug.Enabled { 375 | pdebug.Printf("Validating property '%s'", pname) 376 | } 377 | 378 | pval := o.getProp(rv, pname) 379 | propExists := false 380 | 381 | switch { 382 | case pval == zeroval: 383 | // If we got a zeroval, we're done for. 384 | case pval.Type().Implements(maybeif) || reflect.PtrTo(pval.Type()).Implements(maybeif): 385 | // If we have a Maybe value, we check the Valid() flag 386 | mv := pval.MethodByName("Valid") 387 | out := mv.Call(nil) 388 | if out[0].Bool() { 389 | propExists = true 390 | // Swap out pval to be the value pointed to by the Maybe value 391 | mv = pval.MethodByName("Value") 392 | out = mv.Call(nil) 393 | pval = out[0] 394 | } 395 | default: 396 | // Everything else, we have *something* 397 | propExists = true 398 | } 399 | 400 | if !propExists { 401 | if pdebug.Enabled { 402 | pdebug.Printf("Property '%s' does not exist", pname) 403 | } 404 | 405 | if o.IsPropRequired(pname) { // required, and not present. 406 | return errors.New("object property '" + pname + "' is required") 407 | } 408 | 409 | // At this point we know that the property was not present 410 | // and that this field was indeed not required. 411 | if c.HasDefault() { 412 | if pdebug.Enabled { 413 | pdebug.Printf("object property '" + pname + "' has default") 414 | } 415 | // We have default 416 | dv := c.DefaultValue() 417 | 418 | if err := o.setProp(rv, pname, dv); err != nil { 419 | return errors.New("failed to set default value for property '" + pname + "': " + err.Error()) 420 | } 421 | pval = reflect.ValueOf(dv) 422 | } 423 | 424 | continue 425 | } 426 | 427 | // delete from remaining props 428 | delete(premain, pname) 429 | // ...and add to props that we have seen 430 | pseen[pname] = struct{}{} 431 | 432 | if err := c.Validate(pval.Interface()); err != nil { 433 | return errors.New("object property '" + pname + "' validation failed: " + err.Error()) 434 | } 435 | } 436 | 437 | for pat, c := range o.patternProperties { 438 | if pdebug.Enabled { 439 | pdebug.Printf("Checking patternProperty '%s'", pat.String()) 440 | } 441 | for pname := range premain { 442 | if !pat.MatchString(pname) { 443 | if pdebug.Enabled { 444 | pdebug.Printf("Property '%s' does not match pattern...", pname) 445 | } 446 | continue 447 | } 448 | if pdebug.Enabled { 449 | pdebug.Printf("Property '%s' matches!", pname) 450 | } 451 | // No need to check if this pname exists, as we're taking 452 | // this from "premain" 453 | pval := o.getProp(rv, pname) 454 | 455 | delete(premain, pname) 456 | pseen[pname] = struct{}{} 457 | if err := c.Validate(pval.Interface()); err != nil { 458 | return errors.New("object property '" + pname + "' validation failed: " + err.Error()) 459 | } 460 | } 461 | } 462 | 463 | if len(premain) > 0 { 464 | c := o.additionalProperties 465 | if c == nil { 466 | return errors.New("additional properties are not allowed") 467 | } 468 | 469 | for pname := range premain { 470 | pval := o.getProp(rv, pname) 471 | if err := c.Validate(pval.Interface()); err != nil { 472 | return errors.New("object property for '" + pname + "' validation failed: " + err.Error()) 473 | } 474 | } 475 | } 476 | 477 | for pname := range pseen { 478 | if deps := o.GetPropDependencies(pname); len(deps) > 0 { 479 | if pdebug.Enabled { 480 | pdebug.Printf("Property '%s' has dependencies", pname) 481 | } 482 | for _, dep := range deps { 483 | if _, ok := pseen[dep]; !ok { 484 | return errors.New("required dependency '" + dep + "' is mising") 485 | } 486 | } 487 | 488 | // can't, and shouldn't do object validation after checking prop deps 489 | continue 490 | } 491 | 492 | if depc := o.GetSchemaDependency(pname); depc != nil { 493 | if err := depc.Validate(v); err != nil { 494 | return err 495 | } 496 | } 497 | } 498 | 499 | return nil 500 | } 501 | -------------------------------------------------------------------------------- /object_test.go: -------------------------------------------------------------------------------- 1 | package jsval_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/lestrrat-go/jsschema" 8 | "github.com/lestrrat-go/jsval/builder" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestObject(t *testing.T) { 13 | const src = `{ 14 | "type": "object", 15 | "additionalProperties": false, 16 | "properties": { 17 | "name": { 18 | "type": "string", 19 | "maxLength": 20, 20 | "pattern": "^[a-z ]+$" 21 | }, 22 | "age": { 23 | "type": "integer", 24 | "minimum": 0 25 | }, 26 | "tags": { 27 | "type": "array", 28 | "items": { 29 | "type": "string" 30 | } 31 | } 32 | }, 33 | "patternProperties": { 34 | "name#[a-z]+": { 35 | "type": "string" 36 | } 37 | } 38 | }` 39 | 40 | s, err := schema.Read(strings.NewReader(src)) 41 | if !assert.NoError(t, err, "reading schema should succeed") { 42 | return 43 | } 44 | 45 | b := builder.New() 46 | v, err := b.Build(s) 47 | if !assert.NoError(t, err, "Builder.Build should succeed") { 48 | return 49 | } 50 | 51 | data := []interface{}{ 52 | map[string]interface{}{"Name": "World"}, 53 | map[string]interface{}{"name": "World"}, 54 | map[string]interface{}{"name": "wooooooooooooooooooooooooooooooorld"}, 55 | map[string]interface{}{ 56 | "tags": []interface{}{ 1, "foo", false }, 57 | }, 58 | map[string]interface{}{"name": "ハロー、ワールド"}, 59 | map[string]interface{}{"foo#ja": "フー!"}, 60 | } 61 | for _, input := range data { 62 | t.Logf("Testing %#v (should FAIL)", input) 63 | if !assert.Error(t, v.Validate(input), "validation fails") { 64 | return 65 | } 66 | } 67 | 68 | data = []interface{}{ 69 | map[string]interface{}{"name": "world"}, 70 | map[string]interface{}{"tags": []interface{}{"foo", "bar", "baz"}}, 71 | map[string]interface{}{"name#ja": "ハロー、ワールド"}, 72 | } 73 | for _, input := range data { 74 | t.Logf("Testing %#v (should PASS)", input) 75 | if !assert.NoError(t, v.Validate(input), "validation passes") { 76 | return 77 | } 78 | } 79 | } 80 | 81 | func TestObjectDependency(t *testing.T) { 82 | const src = `{ 83 | "type": "object", 84 | "additionalProperties": false, 85 | "properties": { 86 | "foo": { "type": "string" }, 87 | "bar": { "type": "string" } 88 | }, 89 | "dependencies": { 90 | "foo": ["bar"] 91 | } 92 | }` 93 | 94 | s, err := schema.Read(strings.NewReader(src)) 95 | if !assert.NoError(t, err, "reading schema should succeed") { 96 | return 97 | } 98 | 99 | b := builder.New() 100 | v, err := b.Build(s) 101 | if !assert.NoError(t, err, "Builder.Build should succeed") { 102 | return 103 | } 104 | 105 | data := []interface{}{ 106 | map[string]interface{}{"foo": "foo"}, 107 | } 108 | for _, input := range data { 109 | t.Logf("Testing %#v (should FAIL)", input) 110 | if !assert.Error(t, v.Validate(input), "validation fails") { 111 | return 112 | } 113 | } 114 | 115 | data = []interface{}{ 116 | map[string]interface{}{"foo": "foo", "bar": "bar"}, 117 | } 118 | for _, input := range data { 119 | t.Logf("Testing %#v (should PASS)", input) 120 | if !assert.NoError(t, v.Validate(input), "validation passes") { 121 | return 122 | } 123 | } 124 | } 125 | 126 | func TestObjectSchemaDependency(t *testing.T) { 127 | const src =`{ 128 | "type": "object", 129 | 130 | "properties": { 131 | "name": { "type": "string" }, 132 | "credit_card": { "type": "string", "pattern": "^[0-9]+$" } 133 | }, 134 | 135 | "required": ["name"], 136 | 137 | "dependencies": { 138 | "credit_card": { 139 | "properties": { 140 | "billing_address": { "type": "string" } 141 | }, 142 | "required": ["billing_address"] 143 | } 144 | } 145 | }` 146 | 147 | s, err := schema.Read(strings.NewReader(src)) 148 | if !assert.NoError(t, err, "reading schema should succeed") { 149 | return 150 | } 151 | 152 | b := builder.New() 153 | v, err := b.Build(s) 154 | if !assert.NoError(t, err, "Builder.Build should succeed") { 155 | return 156 | } 157 | 158 | data := []interface{}{ 159 | map[string]interface{}{ 160 | "name": "John Doe", 161 | "credit_card": "5555555555555555", 162 | }, 163 | } 164 | for _, input := range data { 165 | t.Logf("Testing %#v (should FAIL)", input) 166 | if !assert.Error(t, v.Validate(input), "validation fails") { 167 | return 168 | } 169 | } 170 | 171 | data = []interface{}{ 172 | map[string]interface{}{ 173 | "name": "John Doe", 174 | "credit_card": "5555555555555555", 175 | "billing_address": "555 Debtor's Lane", 176 | }, 177 | map[string]interface{}{ 178 | "name": "John Doe", 179 | "billing_address": "555 Debtor's Lane", 180 | }, 181 | } 182 | for _, input := range data { 183 | t.Logf("Testing %#v (should PASS)", input) 184 | if !assert.NoError(t, v.Validate(input), "validation passes") { 185 | return 186 | } 187 | } 188 | } 189 | 190 | -------------------------------------------------------------------------------- /reference.go: -------------------------------------------------------------------------------- 1 | package jsval 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | 7 | "github.com/lestrrat-go/pdebug" 8 | ) 9 | 10 | // RefResolver is a mandatory object that you must pass to a 11 | // ReferenceConstraint upon its creation. This is responsible 12 | // for resolving the reference to an actual constraint. 13 | type RefResolver interface { 14 | GetReference(string) (Constraint, error) 15 | } 16 | 17 | // ConstraintMap is an implementation of RefResolver 18 | type ConstraintMap struct { 19 | lock sync.Mutex 20 | refs map[string]Constraint 21 | } 22 | 23 | // Len returns the number of references stored in this ConstraintMap 24 | func (cm ConstraintMap) Len() int { 25 | return len(cm.refmap()) 26 | } 27 | 28 | func (cm *ConstraintMap) refmap() map[string]Constraint { 29 | if cm.refs == nil { 30 | cm.refs = make(map[string]Constraint) 31 | } 32 | return cm.refs 33 | } 34 | 35 | // SetReference registeres a new Constraint to a name 36 | func (cm *ConstraintMap) SetReference(name string, c Constraint) { 37 | cm.lock.Lock() 38 | defer cm.lock.Unlock() 39 | 40 | refs := cm.refmap() 41 | refs[name] = c 42 | } 43 | 44 | // GetReference fetches the Constraint associated with the given name 45 | func (cm *ConstraintMap) GetReference(name string) (Constraint, error) { 46 | cm.lock.Lock() 47 | defer cm.lock.Unlock() 48 | 49 | refs := cm.refmap() 50 | c, ok := refs[name] 51 | if !ok { 52 | return nil, errors.New("reference '" + name + "' not found") 53 | } 54 | return c, nil 55 | } 56 | 57 | // ReferenceConstraint is a constraint where its actual definition 58 | // is stored elsewhere. 59 | type ReferenceConstraint struct { 60 | resolver RefResolver 61 | lock sync.Mutex 62 | resolved Constraint 63 | reference string 64 | } 65 | 66 | // Reference creates a new ReferenceConstraint object 67 | func Reference(resolver RefResolver) *ReferenceConstraint { 68 | return &ReferenceConstraint{ 69 | resolver: resolver, 70 | } 71 | } 72 | 73 | // Resolved returns the Constraint obtained by resolving 74 | // the reference. 75 | func (r *ReferenceConstraint) Resolved() (c Constraint, err error) { 76 | if pdebug.Enabled { 77 | g := pdebug.IPrintf("START ReferenceConstraint.Resolved '%s'", r.reference) 78 | defer func() { 79 | if err == nil { 80 | g.IRelease("END ReferenceConstraint.Resolved '%s' (OK)", r.reference) 81 | } else { 82 | g.IRelease("END ReferenceConstraint.Resolved '%s' (FAIL): %s", r.reference, err) 83 | } 84 | }() 85 | } 86 | r.lock.Lock() 87 | defer r.lock.Unlock() 88 | 89 | if r.resolved != nil { 90 | if pdebug.Enabled { 91 | pdebug.Printf("Reference is already resolved") 92 | } 93 | return r.resolved, nil 94 | } 95 | 96 | c, err = r.resolver.GetReference(r.reference) 97 | if err != nil { 98 | return nil, err 99 | } 100 | r.resolved = c 101 | return c, nil 102 | } 103 | 104 | // RefersTo specifies the reference string that this constraint points to 105 | func (r *ReferenceConstraint) RefersTo(s string) *ReferenceConstraint { 106 | r.reference = s 107 | return r 108 | } 109 | 110 | // Default is a no op for this type 111 | func (r *ReferenceConstraint) Default(_ interface{}) { 112 | } 113 | 114 | // DefaultValue returns the default value from the constraint pointed 115 | // by the reference 116 | func (r *ReferenceConstraint) DefaultValue() interface{} { 117 | c, err := r.Resolved() 118 | if err != nil { 119 | return nil 120 | } 121 | return c.DefaultValue() 122 | } 123 | 124 | // HasDefault returns true if the constraint pointed by the reference 125 | // has defaults 126 | func (r *ReferenceConstraint) HasDefault() bool { 127 | c, err := r.Resolved() 128 | if err != nil { 129 | return false 130 | } 131 | return c.HasDefault() 132 | } 133 | 134 | // Validate validates the value against the constraint pointed to 135 | // by the reference. 136 | func (r *ReferenceConstraint) Validate(v interface{}) (err error) { 137 | if pdebug.Enabled { 138 | g := pdebug.IPrintf("START ReferenceConstraint.Validate") 139 | defer func() { 140 | if err == nil { 141 | g.IRelease("END ReferenceConstraint.Validate (PASS)") 142 | } else { 143 | g.IRelease("END ReferenceConstraint.Validate (FAIL): %s", err) 144 | } 145 | }() 146 | } 147 | 148 | c, err := r.Resolved() 149 | if err != nil { 150 | return err 151 | } 152 | return c.Validate(v) 153 | } -------------------------------------------------------------------------------- /schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://json-schema.org/draft-04/schema#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "description": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "positiveInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "positiveIntegerDefault0": { 16 | "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] 17 | }, 18 | "simpleTypes": { 19 | "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] 20 | }, 21 | "stringArray": { 22 | "type": "array", 23 | "items": { "type": "string" }, 24 | "minItems": 1, 25 | "uniqueItems": true 26 | } 27 | }, 28 | "type": "object", 29 | "properties": { 30 | "id": { 31 | "type": "string", 32 | "format": "uri" 33 | }, 34 | "$schema": { 35 | "type": "string", 36 | "format": "uri" 37 | }, 38 | "title": { 39 | "type": "string" 40 | }, 41 | "description": { 42 | "type": "string" 43 | }, 44 | "default": {}, 45 | "multipleOf": { 46 | "type": "number", 47 | "minimum": 0, 48 | "exclusiveMinimum": true 49 | }, 50 | "maximum": { 51 | "type": "number" 52 | }, 53 | "exclusiveMaximum": { 54 | "type": "boolean", 55 | "default": false 56 | }, 57 | "minimum": { 58 | "type": "number" 59 | }, 60 | "exclusiveMinimum": { 61 | "type": "boolean", 62 | "default": false 63 | }, 64 | "maxLength": { "$ref": "#/definitions/positiveInteger" }, 65 | "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, 66 | "pattern": { 67 | "type": "string", 68 | "format": "regex" 69 | }, 70 | "additionalItems": { 71 | "anyOf": [ 72 | { "type": "boolean" }, 73 | { "$ref": "#" } 74 | ], 75 | "default": {} 76 | }, 77 | "items": { 78 | "anyOf": [ 79 | { "$ref": "#" }, 80 | { "$ref": "#/definitions/schemaArray" } 81 | ], 82 | "default": {} 83 | }, 84 | "maxItems": { "$ref": "#/definitions/positiveInteger" }, 85 | "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, 86 | "uniqueItems": { 87 | "type": "boolean", 88 | "default": false 89 | }, 90 | "maxProperties": { "$ref": "#/definitions/positiveInteger" }, 91 | "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, 92 | "required": { "$ref": "#/definitions/stringArray" }, 93 | "additionalProperties": { 94 | "anyOf": [ 95 | { "type": "boolean" }, 96 | { "$ref": "#" } 97 | ], 98 | "default": {} 99 | }, 100 | "definitions": { 101 | "type": "object", 102 | "additionalProperties": { "$ref": "#" }, 103 | "default": {} 104 | }, 105 | "properties": { 106 | "type": "object", 107 | "additionalProperties": { "$ref": "#" }, 108 | "default": {} 109 | }, 110 | "patternProperties": { 111 | "type": "object", 112 | "additionalProperties": { "$ref": "#" }, 113 | "default": {} 114 | }, 115 | "dependencies": { 116 | "type": "object", 117 | "additionalProperties": { 118 | "anyOf": [ 119 | { "$ref": "#" }, 120 | { "$ref": "#/definitions/stringArray" } 121 | ] 122 | } 123 | }, 124 | "enum": { 125 | "type": "array", 126 | "minItems": 1, 127 | "uniqueItems": true 128 | }, 129 | "type": { 130 | "anyOf": [ 131 | { "$ref": "#/definitions/simpleTypes" }, 132 | { 133 | "type": "array", 134 | "items": { "$ref": "#/definitions/simpleTypes" }, 135 | "minItems": 1, 136 | "uniqueItems": true 137 | } 138 | ] 139 | }, 140 | "allOf": { "$ref": "#/definitions/schemaArray" }, 141 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 142 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 143 | "not": { "$ref": "#" } 144 | }, 145 | "dependencies": { 146 | "exclusiveMaximum": [ "maximum" ], 147 | "exclusiveMinimum": [ "minimum" ] 148 | }, 149 | "default": {} 150 | } 151 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "html/template" 7 | "io" 8 | "net/http" 9 | "sync" 10 | 11 | "github.com/lestrrat-go/jsschema" 12 | "github.com/lestrrat-go/jsval" 13 | "github.com/lestrrat-go/jsval/builder" 14 | "github.com/pkg/errors" 15 | "golang.org/x/net/context" 16 | ) 17 | 18 | const indexTmpl = ` 19 | 20 | 21 | JSVal Playground 22 | 44 | 45 | 46 |