From 0f7831489f16f033ff990c88bcbbf511d2a3e7d9 Mon Sep 17 00:00:00 2001 From: jakezhu9 Date: Tue, 1 Aug 2023 20:48:11 +0800 Subject: [PATCH] feat: generate kcl schema from json schema --- go.mod | 6 + go.sum | 13 + pkg/3rdparty/jsonschema/README.md | 35 + .../jsonschema/draft2019_09_keywords.go | 93 +++ pkg/3rdparty/jsonschema/keyword.go | 157 ++++ pkg/3rdparty/jsonschema/keywords_array.go | 444 +++++++++++ pkg/3rdparty/jsonschema/keywords_boolean.go | 319 ++++++++ .../jsonschema/keywords_conditional.go | 206 +++++ pkg/3rdparty/jsonschema/keywords_core.go | 693 +++++++++++++++++ pkg/3rdparty/jsonschema/keywords_numeric.go | 172 +++++ pkg/3rdparty/jsonschema/keywords_object.go | 723 ++++++++++++++++++ pkg/3rdparty/jsonschema/keywords_optional.go | 325 ++++++++ pkg/3rdparty/jsonschema/keywords_standard.go | 287 +++++++ pkg/3rdparty/jsonschema/keywords_string.go | 113 +++ pkg/3rdparty/jsonschema/schema.go | 360 +++++++++ pkg/3rdparty/jsonschema/schema_registry.go | 90 +++ pkg/3rdparty/jsonschema/traversal.go | 35 + pkg/3rdparty/jsonschema/util.go | 95 +++ pkg/3rdparty/jsonschema/validation_state.go | 194 +++++ pkg/tools/gen/genkcl.go | 46 +- pkg/tools/gen/genkcl_jsonschema.go | 194 +++++ pkg/tools/gen/genkcl_test.go | 48 ++ pkg/tools/gen/template.go | 58 ++ pkg/tools/gen/templates/header.gotmpl | 10 + pkg/tools/gen/templates/schema.gotmpl | 8 + .../gen/testdata/jsonschema/basic/expect.k | 12 + .../gen/testdata/jsonschema/basic/input.json | 33 + .../gen/testdata/jsonschema/nested/expect.k | 13 + .../gen/testdata/jsonschema/nested/input.json | 21 + pkg/tools/gen/types.go | 79 ++ 30 files changed, 4881 insertions(+), 1 deletion(-) create mode 100644 pkg/3rdparty/jsonschema/README.md create mode 100644 pkg/3rdparty/jsonschema/draft2019_09_keywords.go create mode 100644 pkg/3rdparty/jsonschema/keyword.go create mode 100644 pkg/3rdparty/jsonschema/keywords_array.go create mode 100644 pkg/3rdparty/jsonschema/keywords_boolean.go create mode 100644 pkg/3rdparty/jsonschema/keywords_conditional.go create mode 100644 pkg/3rdparty/jsonschema/keywords_core.go create mode 100644 pkg/3rdparty/jsonschema/keywords_numeric.go create mode 100644 pkg/3rdparty/jsonschema/keywords_object.go create mode 100644 pkg/3rdparty/jsonschema/keywords_optional.go create mode 100644 pkg/3rdparty/jsonschema/keywords_standard.go create mode 100644 pkg/3rdparty/jsonschema/keywords_string.go create mode 100644 pkg/3rdparty/jsonschema/schema.go create mode 100644 pkg/3rdparty/jsonschema/schema_registry.go create mode 100644 pkg/3rdparty/jsonschema/traversal.go create mode 100644 pkg/3rdparty/jsonschema/util.go create mode 100644 pkg/3rdparty/jsonschema/validation_state.go create mode 100644 pkg/tools/gen/genkcl_jsonschema.go create mode 100644 pkg/tools/gen/template.go create mode 100644 pkg/tools/gen/templates/header.gotmpl create mode 100644 pkg/tools/gen/templates/schema.gotmpl create mode 100644 pkg/tools/gen/testdata/jsonschema/basic/expect.k create mode 100644 pkg/tools/gen/testdata/jsonschema/basic/input.json create mode 100644 pkg/tools/gen/testdata/jsonschema/nested/expect.k create mode 100644 pkg/tools/gen/testdata/jsonschema/nested/input.json diff --git a/go.mod b/go.mod index 79b5613d..6368ec99 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,14 @@ require ( github.com/gofrs/flock v0.8.1 github.com/golang/protobuf v1.5.3 github.com/google/go-cmp v0.5.9 + github.com/iancoleman/strcase v0.3.0 github.com/julienschmidt/httprouter v1.3.0 github.com/mitchellh/mapstructure v1.5.0 github.com/powerman/rpc-codec v1.2.2 + github.com/qri-io/jsonpointer v0.1.1 github.com/stretchr/testify v1.8.2 github.com/urfave/cli/v2 v2.6.0 + github.com/wk8/go-ordered-map/v2 v2.1.8 google.golang.org/grpc v1.53.0 google.golang.org/protobuf v1.28.1 gopkg.in/yaml.v3 v3.0.1 @@ -20,9 +23,12 @@ require ( ) require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/snappy v0.0.3 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect golang.org/x/net v0.9.0 // indirect diff --git a/go.sum b/go.sum index 28213524..80364d6b 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/chai2010/jsonv v1.1.3 h1:gBIHXn/5mdEPTuWZfjC54fn/yUSRR8OGobXobcc6now= github.com/chai2010/jsonv v1.1.3/go.mod h1:mEoT1dQ9qVF4oP9peVTl0UymTmJwXoTDOh+sNA6+XII= github.com/chai2010/protorpc v1.1.4 h1:CTtFUhzXRoeuR7FtgQ2b2vdT/KgWVpCM+sIus8zJjHs= @@ -18,14 +22,21 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/powerman/rpc-codec v1.2.2 h1:BK0JScZivljhwW/vLLhZLtUgqSxc/CD3sHEs8LiwwKw= github.com/powerman/rpc-codec v1.2.2/go.mod h1:3Qr/y/+u3CwcSww9tfJMRn/95lB2qUdUeIQe7BYlLDo= +github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA= +github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -37,6 +48,8 @@ github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/urfave/cli/v2 v2.6.0 h1:yj2Drkflh8X/zUrkWlWlUjZYHyWN7WMmpVxyxXIUyv8= github.com/urfave/cli/v2 v2.6.0/go.mod h1:oDzoM7pVwz6wHn5ogWgFUU1s4VJayeQS+aEZDqXIEJs= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= diff --git a/pkg/3rdparty/jsonschema/README.md b/pkg/3rdparty/jsonschema/README.md new file mode 100644 index 00000000..585a1f1a --- /dev/null +++ b/pkg/3rdparty/jsonschema/README.md @@ -0,0 +1,35 @@ +# jsonschema + +This package is a fork of the [jsonschema](https://github.com/qri-io/jsonschema) +package, which is a Go implementation of the JSON Schema specification. + +Thanks to the original authors for their great work, we are able to use this package to +parse JSON Schema and support keywords in different versions of the specification easily. +We also make some modifications to support our needs, such as make some field public, +use orderedmap in properties keyword to keep the order, etc. + +## License + +``` +The MIT License (MIT) + +Copyright (c) 2017 Brendan O'Brien + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +``` diff --git a/pkg/3rdparty/jsonschema/draft2019_09_keywords.go b/pkg/3rdparty/jsonschema/draft2019_09_keywords.go new file mode 100644 index 00000000..1858b711 --- /dev/null +++ b/pkg/3rdparty/jsonschema/draft2019_09_keywords.go @@ -0,0 +1,93 @@ +package jsonschema + +// LoadDraft2019_09 loads the Keywords for schema validation +// based on draft2019_09 +// this is also the default keyword set loaded automatically +// if no other is loaded +func LoadDraft2019_09() { + // core Keywords + RegisterKeyword("$schema", NewSchemaURI) + RegisterKeyword("$id", NewID) + RegisterKeyword("description", NewDescription) + RegisterKeyword("title", NewTitle) + RegisterKeyword("$comment", NewComment) + RegisterKeyword("examples", NewExamples) + RegisterKeyword("readOnly", NewReadOnly) + RegisterKeyword("writeOnly", NewWriteOnly) + RegisterKeyword("$ref", NewRef) + RegisterKeyword("$recursiveRef", NewRecursiveRef) + RegisterKeyword("$anchor", NewAnchor) + RegisterKeyword("$recursiveAnchor", NewRecursiveAnchor) + RegisterKeyword("$defs", NewDefs) + RegisterKeyword("definitions", NewDefs) + RegisterKeyword("default", NewDefault) + + SetKeywordOrder("$ref", 0) + SetKeywordOrder("$recursiveRef", 0) + + // standard Keywords + RegisterKeyword("type", NewType) + RegisterKeyword("enum", NewEnum) + RegisterKeyword("const", NewConst) + + // numeric Keywords + RegisterKeyword("multipleOf", NewMultipleOf) + RegisterKeyword("maximum", NewMaximum) + RegisterKeyword("exclusiveMaximum", NewExclusiveMaximum) + RegisterKeyword("minimum", NewMinimum) + RegisterKeyword("exclusiveMinimum", NewExclusiveMinimum) + + // string Keywords + RegisterKeyword("maxLength", NewMaxLength) + RegisterKeyword("minLength", NewMinLength) + RegisterKeyword("pattern", NewPattern) + + // boolean Keywords + RegisterKeyword("allOf", NewAllOf) + RegisterKeyword("anyOf", NewAnyOf) + RegisterKeyword("oneOf", NewOneOf) + RegisterKeyword("not", NewNot) + + // object Keywords + RegisterKeyword("properties", NewProperties) + RegisterKeyword("patternProperties", NewPatternProperties) + RegisterKeyword("additionalProperties", NewAdditionalProperties) + RegisterKeyword("required", NewRequired) + RegisterKeyword("propertyNames", NewPropertyNames) + RegisterKeyword("maxProperties", NewMaxProperties) + RegisterKeyword("minProperties", NewMinProperties) + RegisterKeyword("dependentSchemas", NewDependentSchemas) + RegisterKeyword("dependentRequired", NewDependentRequired) + RegisterKeyword("unevaluatedProperties", NewUnevaluatedProperties) + + SetKeywordOrder("properties", 2) + SetKeywordOrder("additionalProperties", 3) + SetKeywordOrder("unevaluatedProperties", 4) + + // array Keywords + RegisterKeyword("items", NewItems) + RegisterKeyword("additionalItems", NewAdditionalItems) + RegisterKeyword("maxItems", NewMaxItems) + RegisterKeyword("minItems", NewMinItems) + RegisterKeyword("uniqueItems", NewUniqueItems) + RegisterKeyword("contains", NewContains) + RegisterKeyword("maxContains", NewMaxContains) + RegisterKeyword("minContains", NewMinContains) + RegisterKeyword("unevaluatedItems", NewUnevaluatedItems) + + SetKeywordOrder("maxContains", 2) + SetKeywordOrder("minContains", 2) + SetKeywordOrder("additionalItems", 3) + SetKeywordOrder("unevaluatedItems", 4) + + // conditional Keywords + RegisterKeyword("if", NewIf) + RegisterKeyword("then", NewThen) + RegisterKeyword("else", NewElse) + + SetKeywordOrder("then", 2) + SetKeywordOrder("else", 2) + + //optional formats + RegisterKeyword("format", NewFormat) +} diff --git a/pkg/3rdparty/jsonschema/keyword.go b/pkg/3rdparty/jsonschema/keyword.go new file mode 100644 index 00000000..72026235 --- /dev/null +++ b/pkg/3rdparty/jsonschema/keyword.go @@ -0,0 +1,157 @@ +package jsonschema + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + + jptr "github.com/qri-io/jsonpointer" +) + +var notSupported = map[string]bool{ + // core + "$vocabulary": true, + + // other + "contentEncoding": true, + "contentMediaType": true, + "contentSchema": true, + "deprecated": true, + + // backward compatibility with draft7 + "definitions": true, + "dependencies": true, +} + +var ( + keywordRegistry = map[string]KeyMaker{} + keywordOrder = map[string]int{} + keywordInsertOrder = map[string]int{} +) + +// IsRegisteredKeyword validates if a given prop string is a registered keyword +func IsRegisteredKeyword(prop string) bool { + _, ok := keywordRegistry[prop] + return ok +} + +// GetKeyword returns a new instance of the keyword +func GetKeyword(prop string) Keyword { + if !IsRegisteredKeyword(prop) { + return NewVoid() + } + return keywordRegistry[prop]() +} + +// GetKeywordOrder returns the order index of +// the given keyword or defaults to 1 +func GetKeywordOrder(prop string) int { + if order, ok := keywordOrder[prop]; ok { + return order + } + return 1 +} + +// GetKeywordInsertOrder returns the insert index of +// the given keyword +func GetKeywordInsertOrder(prop string) int { + if order, ok := keywordInsertOrder[prop]; ok { + return order + } + // TODO(arqu): this is an arbitrary max + return 1000 +} + +// SetKeywordOrder assignes a given order to a keyword +func SetKeywordOrder(prop string, order int) { + keywordOrder[prop] = order +} + +// IsNotSupportedKeyword is a utility function to clarify when +// a given keyword, while expected is not supported +func IsNotSupportedKeyword(prop string) bool { + _, ok := notSupported[prop] + return ok +} + +// IsRegistryLoaded checks if any Keywords are present +func IsRegistryLoaded() bool { + return keywordRegistry != nil && len(keywordRegistry) > 0 +} + +// RegisterKeyword registers a keyword with the registry +func RegisterKeyword(prop string, maker KeyMaker) { + keywordRegistry[prop] = maker + keywordInsertOrder[prop] = len(keywordInsertOrder) +} + +// MaxKeywordErrStringLen sets how long a value can be before it's length is truncated +// when printing error strings +// a special value of -1 disables output trimming +var MaxKeywordErrStringLen = 20 + +// Keyword is an interface for anything that can validate. +// JSON-Schema Keywords are all examples of Keyword +type Keyword interface { + // ValidateKeyword checks decoded JSON data and writes + // validation errors (if any) to an outparam slice of KeyErrors + ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) + + // Register builds up the schema tree by evaluating the current key + // and the current location pointer which is later used with resolve to + // navigate the schema tree and substitute the propper schema for a given + // reference. + Register(uri string, registry *SchemaRegistry) + // Resolve unraps a pointer to the destination schema + // It usually starts with a $ref validation call which + // uses the pointer token by token to navigate the + // schema tree to get to the last schema in the chain. + // Since every keyword can have it's specifics around resolving + // each keyword need to implement it's own version of Resolve. + // Terminal Keywords should respond with nil as it's not a schema + // Keywords that wrap a schema should return the appropriate schema. + // In case of a non-existing location it will fail to resolve, return nil + // on ref resolution and error out. + Resolve(pointer jptr.Pointer, uri string) *Schema +} + +// KeyMaker is a function that generates instances of a Keyword. +// Calls to KeyMaker will be passed directly to json.Marshal, +// so the returned value should be a pointer +type KeyMaker func() Keyword + +// KeyError represents a Single error in an instance of a schema +// The only absolutely-required property is Message. +type KeyError struct { + // PropertyPath is a string path that leads to the + // property that produced the error + PropertyPath string `json:"propertyPath,omitempty"` + // InvalidValue is the value that returned the error + InvalidValue interface{} `json:"invalidValue,omitempty"` + // Message is a human-readable description of the error + Message string `json:"message"` +} + +// Error implements the error interface for KeyError +func (v KeyError) Error() string { + if v.PropertyPath != "" && v.InvalidValue != nil { + return fmt.Sprintf("%s: %s %s", v.PropertyPath, InvalidValueString(v.InvalidValue), v.Message) + } else if v.PropertyPath != "" { + return fmt.Sprintf("%s: %s", v.PropertyPath, v.Message) + } + return v.Message +} + +// InvalidValueString returns the errored value as a string +func InvalidValueString(data interface{}) string { + bt, err := json.Marshal(data) + if err != nil { + return "" + } + bt = bytes.Replace(bt, []byte{'\n', '\r'}, []byte{' '}, -1) + if MaxKeywordErrStringLen != -1 && len(bt) > MaxKeywordErrStringLen { + bt = append(bt[:MaxKeywordErrStringLen], []byte("...")...) + } + return string(bt) +} diff --git a/pkg/3rdparty/jsonschema/keywords_array.go b/pkg/3rdparty/jsonschema/keywords_array.go new file mode 100644 index 00000000..f741dd9e --- /dev/null +++ b/pkg/3rdparty/jsonschema/keywords_array.go @@ -0,0 +1,444 @@ +package jsonschema + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "strconv" + + jptr "github.com/qri-io/jsonpointer" +) + +// Items defines the items JSON Schema keyword +type Items struct { + Single bool + Schemas []*Schema +} + +// NewItems allocates a new Items keyword +func NewItems() Keyword { + return &Items{} +} + +// Register implements the Keyword interface for Items +func (it *Items) Register(uri string, registry *SchemaRegistry) { + for _, v := range it.Schemas { + v.Register(uri, registry) + } +} + +// Resolve implements the Keyword interface for Items +func (it *Items) Resolve(pointer jptr.Pointer, uri string) *Schema { + if pointer == nil { + return nil + } + current := pointer.Head() + if current == nil { + return nil + } + + pos, err := strconv.Atoi(*current) + if err != nil { + return nil + } + + if pos < 0 || pos >= len(it.Schemas) { + return nil + } + + return it.Schemas[pos].Resolve(pointer.Tail(), uri) + + return nil +} + +// ValidateKeyword implements the Keyword interface for Items +func (it Items) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Items] Validating") + if arr, ok := data.([]interface{}); ok { + if it.Single { + subState := currentState.NewSubState() + subState.DescendBase("items") + subState.DescendRelative("items") + for i, elem := range arr { + subState.ClearState() + subState.DescendInstanceFromState(currentState, strconv.Itoa(i)) + it.Schemas[0].ValidateKeyword(ctx, subState, elem) + subState.SetEvaluatedIndex(i) + // TODO(arqu): this might clash with additional/unevaluated + // Properties/Items, should separate out + currentState.UpdateEvaluatedPropsAndItems(subState) + } + } else { + subState := currentState.NewSubState() + subState.DescendBase("items") + for i, vs := range it.Schemas { + if i < len(arr) { + subState.ClearState() + subState.DescendRelativeFromState(currentState, "items", strconv.Itoa(i)) + subState.DescendInstanceFromState(currentState, strconv.Itoa(i)) + + vs.ValidateKeyword(ctx, subState, arr[i]) + subState.SetEvaluatedIndex(i) + currentState.UpdateEvaluatedPropsAndItems(subState) + } + } + } + } +} + +// JSONProp implements the JSONPather for Items +func (it Items) JSONProp(name string) interface{} { + idx, err := strconv.Atoi(name) + if err != nil { + return nil + } + if idx > len(it.Schemas) || idx < 0 { + return nil + } + return it.Schemas[idx] +} + +// JSONChildren implements the JSONContainer interface for Items +func (it Items) JSONChildren() (res map[string]JSONPather) { + res = map[string]JSONPather{} + for i, sch := range it.Schemas { + res[strconv.Itoa(i)] = sch + } + return +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Items +func (it *Items) UnmarshalJSON(data []byte) error { + s := &Schema{} + if err := json.Unmarshal(data, s); err == nil { + *it = Items{Single: true, Schemas: []*Schema{s}} + return nil + } + ss := []*Schema{} + if err := json.Unmarshal(data, &ss); err != nil { + return err + } + *it = Items{Schemas: ss} + return nil +} + +// MarshalJSON implements the json.Marshaler interface for Items +func (it Items) MarshalJSON() ([]byte, error) { + if it.Single { + return json.Marshal(it.Schemas[0]) + } + return json.Marshal([]*Schema(it.Schemas)) +} + +// MaxItems defines the maxItems JSON Schema keyword +type MaxItems int + +// NewMaxItems allocates a new MaxItems keyword +func NewMaxItems() Keyword { + return new(MaxItems) +} + +// Register implements the Keyword interface for MaxItems +func (m *MaxItems) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for MaxItems +func (m *MaxItems) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for MaxItems +func (m MaxItems) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[MaxItems] Validating") + if arr, ok := data.([]interface{}); ok { + if len(arr) > int(m) { + currentState.AddError(data, fmt.Sprintf("array length %d exceeds %d max", len(arr), m)) + return + } + } +} + +// MinItems defines the minItems JSON Schema keyword +type MinItems int + +// NewMinItems allocates a new MinItems keyword +func NewMinItems() Keyword { + return new(MinItems) +} + +// Register implements the Keyword interface for MinItems +func (m *MinItems) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for MinItems +func (m *MinItems) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for MinItems +func (m MinItems) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[MinItems] Validating") + if arr, ok := data.([]interface{}); ok { + if len(arr) < int(m) { + currentState.AddError(data, fmt.Sprintf("array length %d below %d minimum items", len(arr), m)) + return + } + } +} + +// UniqueItems defines the uniqueItems JSON Schema keyword +type UniqueItems bool + +// NewUniqueItems allocates a new UniqueItems keyword +func NewUniqueItems() Keyword { + return new(UniqueItems) +} + +// Register implements the Keyword interface for UniqueItems +func (u *UniqueItems) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for UniqueItems +func (u *UniqueItems) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for UniqueItems +func (u UniqueItems) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[UniqueItems] Validating") + if arr, ok := data.([]interface{}); ok { + found := []interface{}{} + for _, elem := range arr { + for _, f := range found { + if reflect.DeepEqual(f, elem) { + currentState.AddError(data, fmt.Sprintf("array items must be unique. duplicated entry: %v", elem)) + return + } + } + found = append(found, elem) + } + } +} + +// Contains defines the contains JSON Schema keyword +type Contains Schema + +// NewContains allocates a new Contains keyword +func NewContains() Keyword { + return &Contains{} +} + +// Register implements the Keyword interface for Contains +func (c *Contains) Register(uri string, registry *SchemaRegistry) { + (*Schema)(c).Register(uri, registry) +} + +// Resolve implements the Keyword interface for Contains +func (c *Contains) Resolve(pointer jptr.Pointer, uri string) *Schema { + return (*Schema)(c).Resolve(pointer, uri) +} + +// ValidateKeyword implements the Keyword interface for Contains +func (c *Contains) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Contains] Validating") + v := Schema(*c) + if arr, ok := data.([]interface{}); ok { + valid := false + matchCount := 0 + subState := currentState.NewSubState() + subState.ClearState() + subState.DescendBase("contains") + subState.DescendRelative("contains") + for i, elem := range arr { + subState.ClearState() + subState.DescendInstanceFromState(currentState, strconv.Itoa(i)) + subState.Errs = &[]KeyError{} + v.ValidateKeyword(ctx, subState, elem) + if subState.IsValid() { + valid = true + matchCount++ + } + } + if valid { + currentState.Misc["containsCount"] = matchCount + } else { + currentState.AddError(data, fmt.Sprintf("must contain at least one of: %v", c)) + } + } +} + +// JSONProp implements the JSONPather for Contains +func (c Contains) JSONProp(name string) interface{} { + return Schema(c).JSONProp(name) +} + +// JSONChildren implements the JSONContainer interface for Contains +func (c Contains) JSONChildren() (res map[string]JSONPather) { + return Schema(c).JSONChildren() +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Contains +func (c *Contains) UnmarshalJSON(data []byte) error { + var sch Schema + if err := json.Unmarshal(data, &sch); err != nil { + return err + } + *c = Contains(sch) + return nil +} + +// MaxContains defines the maxContains JSON Schema keyword +type MaxContains int + +// NewMaxContains allocates a new MaxContains keyword +func NewMaxContains() Keyword { + return new(MaxContains) +} + +// Register implements the Keyword interface for MaxContains +func (m *MaxContains) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for MaxContains +func (m *MaxContains) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for MaxContains +func (m MaxContains) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[MaxContains] Validating") + if arr, ok := data.([]interface{}); ok { + if containsCount, ok := currentState.Misc["containsCount"]; ok { + if containsCount.(int) > int(m) { + currentState.AddError(data, fmt.Sprintf("contained items %d exceeds %d max", len(arr), m)) + } + } + } +} + +// MinContains defines the minContains JSON Schema keyword +type MinContains int + +// NewMinContains allocates a new MinContains keyword +func NewMinContains() Keyword { + return new(MinContains) +} + +// Register implements the Keyword interface for MinContains +func (m *MinContains) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for MinContains +func (m *MinContains) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for MinContains +func (m MinContains) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[MinContains] Validating") + if arr, ok := data.([]interface{}); ok { + if containsCount, ok := currentState.Misc["containsCount"]; ok { + if containsCount.(int) < int(m) { + currentState.AddError(data, fmt.Sprintf("contained items %d bellow %d min", len(arr), m)) + } + } + } +} + +// AdditionalItems defines the additionalItems JSON Schema keyword +type AdditionalItems Schema + +// NewAdditionalItems allocates a new AdditionalItems keyword +func NewAdditionalItems() Keyword { + return &AdditionalItems{} +} + +// Register implements the Keyword interface for AdditionalItems +func (ai *AdditionalItems) Register(uri string, registry *SchemaRegistry) { + (*Schema)(ai).Register(uri, registry) +} + +// Resolve implements the Keyword interface for AdditionalItems +func (ai *AdditionalItems) Resolve(pointer jptr.Pointer, uri string) *Schema { + return (*Schema)(ai).Resolve(pointer, uri) +} + +// ValidateKeyword implements the Keyword interface for AdditionalItems +func (ai *AdditionalItems) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[AdditionalItems] Validating") + if arr, ok := data.([]interface{}); ok { + if currentState.LastEvaluatedIndex > -1 && currentState.LastEvaluatedIndex < len(arr) { + for i := currentState.LastEvaluatedIndex + 1; i < len(arr); i++ { + if ai.SchemaType == SchemaTypeFalse { + currentState.AddError(data, "additional items are not allowed") + return + } + subState := currentState.NewSubState() + subState.ClearState() + subState.SetEvaluatedIndex(i) + subState.DescendBase("additionalItems") + subState.DescendRelative("additionalItems") + subState.DescendInstance(strconv.Itoa(i)) + + (*Schema)(ai).ValidateKeyword(ctx, subState, arr[i]) + currentState.UpdateEvaluatedPropsAndItems(subState) + } + } + } +} + +// UnmarshalJSON implements the json.Unmarshaler interface for AdditionalItems +func (ai *AdditionalItems) UnmarshalJSON(data []byte) error { + sch := &Schema{} + if err := json.Unmarshal(data, sch); err != nil { + return err + } + *ai = (AdditionalItems)(*sch) + return nil +} + +// UnevaluatedItems defines the unevaluatedItems JSON Schema keyword +type UnevaluatedItems Schema + +// NewUnevaluatedItems allocates a new UnevaluatedItems keyword +func NewUnevaluatedItems() Keyword { + return &UnevaluatedItems{} +} + +// Register implements the Keyword interface for UnevaluatedItems +func (ui *UnevaluatedItems) Register(uri string, registry *SchemaRegistry) { + (*Schema)(ui).Register(uri, registry) +} + +// Resolve implements the Keyword interface for UnevaluatedItems +func (ui *UnevaluatedItems) Resolve(pointer jptr.Pointer, uri string) *Schema { + return (*Schema)(ui).Resolve(pointer, uri) +} + +// ValidateKeyword implements the Keyword interface for UnevaluatedItems +func (ui *UnevaluatedItems) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[UnevaluatedItems] Validating") + if arr, ok := data.([]interface{}); ok { + if currentState.LastEvaluatedIndex < len(arr) { + for i := currentState.LastEvaluatedIndex + 1; i < len(arr); i++ { + if ui.SchemaType == SchemaTypeFalse { + currentState.AddError(data, "unevaluated items are not allowed") + return + } + subState := currentState.NewSubState() + subState.ClearState() + subState.DescendBase("unevaluatedItems") + subState.DescendRelative("unevaluatedItems") + subState.DescendInstance(strconv.Itoa(i)) + + (*Schema)(ui).ValidateKeyword(ctx, subState, arr[i]) + } + } + } +} + +// UnmarshalJSON implements the json.Unmarshaler interface for UnevaluatedItems +func (ui *UnevaluatedItems) UnmarshalJSON(data []byte) error { + sch := &Schema{} + if err := json.Unmarshal(data, sch); err != nil { + return err + } + *ui = (UnevaluatedItems)(*sch) + return nil +} diff --git a/pkg/3rdparty/jsonschema/keywords_boolean.go b/pkg/3rdparty/jsonschema/keywords_boolean.go new file mode 100644 index 00000000..9e320402 --- /dev/null +++ b/pkg/3rdparty/jsonschema/keywords_boolean.go @@ -0,0 +1,319 @@ +package jsonschema + +import ( + "context" + "encoding/json" + "strconv" + + jptr "github.com/qri-io/jsonpointer" +) + +// AllOf defines the allOf JSON Schema keyword +type AllOf []*Schema + +// NewAllOf allocates a new AllOf keyword +func NewAllOf() Keyword { + return &AllOf{} +} + +// Register implements the Keyword interface for AllOf +func (a *AllOf) Register(uri string, registry *SchemaRegistry) { + for _, sch := range *a { + sch.Register(uri, registry) + } +} + +// Resolve implements the Keyword interface for AllOf +func (a *AllOf) Resolve(pointer jptr.Pointer, uri string) *Schema { + if pointer == nil { + return nil + } + current := pointer.Head() + if current == nil { + return nil + } + + pos, err := strconv.Atoi(*current) + if err != nil { + return nil + } + + if pos < 0 || pos >= len(*a) { + return nil + } + + return (*a)[pos].Resolve(pointer.Tail(), uri) + + return nil +} + +// ValidateKeyword implements the Keyword interface for AllOf +func (a *AllOf) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[AllOf] Validating") + stateCopy := currentState.NewSubState() + stateCopy.ClearState() + invalid := false + for i, sch := range *a { + subState := currentState.NewSubState() + subState.ClearState() + subState.DescendBase("allOf", strconv.Itoa(i)) + subState.DescendRelative("allOf", strconv.Itoa(i)) + subState.Errs = &[]KeyError{} + sch.ValidateKeyword(ctx, subState, data) + currentState.AddSubErrors(*subState.Errs...) + stateCopy.UpdateEvaluatedPropsAndItems(subState) + if !subState.IsValid() { + invalid = true + } + } + if !invalid { + currentState.UpdateEvaluatedPropsAndItems(stateCopy) + } +} + +// JSONProp implements the JSONPather for AllOf +func (a AllOf) JSONProp(name string) interface{} { + idx, err := strconv.Atoi(name) + if err != nil { + return nil + } + if idx > len(a) || idx < 0 { + return nil + } + return a[idx] +} + +// JSONChildren implements the JSONContainer interface for AllOf +func (a AllOf) JSONChildren() (res map[string]JSONPather) { + res = map[string]JSONPather{} + for i, sch := range a { + res[strconv.Itoa(i)] = sch + } + return +} + +// AnyOf defines the anyOf JSON Schema keyword +type AnyOf []*Schema + +// NewAnyOf allocates a new AnyOf keyword +func NewAnyOf() Keyword { + return &AnyOf{} +} + +// Register implements the Keyword interface for AnyOf +func (a *AnyOf) Register(uri string, registry *SchemaRegistry) { + for _, sch := range *a { + sch.Register(uri, registry) + } +} + +// Resolve implements the Keyword interface for AnyOf +func (a *AnyOf) Resolve(pointer jptr.Pointer, uri string) *Schema { + if pointer == nil { + return nil + } + current := pointer.Head() + if current == nil { + return nil + } + + pos, err := strconv.Atoi(*current) + if err != nil { + return nil + } + + if pos < 0 || pos >= len(*a) { + return nil + } + + return (*a)[pos].Resolve(pointer.Tail(), uri) + + return nil +} + +// ValidateKeyword implements the Keyword interface for AnyOf +func (a *AnyOf) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[AnyOf] Validating") + for i, sch := range *a { + subState := currentState.NewSubState() + subState.ClearState() + subState.DescendBase("anyOf", strconv.Itoa(i)) + subState.DescendRelative("anyOf", strconv.Itoa(i)) + subState.Errs = &[]KeyError{} + sch.ValidateKeyword(ctx, subState, data) + if subState.IsValid() { + currentState.UpdateEvaluatedPropsAndItems(subState) + return + } + } + + currentState.AddError(data, "did Not match any specified AnyOf schemas") +} + +// JSONProp implements the JSONPather for AnyOf +func (a AnyOf) JSONProp(name string) interface{} { + idx, err := strconv.Atoi(name) + if err != nil { + return nil + } + if idx > len(a) || idx < 0 { + return nil + } + return a[idx] +} + +// JSONChildren implements the JSONContainer interface for AnyOf +func (a AnyOf) JSONChildren() (res map[string]JSONPather) { + res = map[string]JSONPather{} + for i, sch := range a { + res[strconv.Itoa(i)] = sch + } + return +} + +// OneOf defines the oneOf JSON Schema keyword +type OneOf []*Schema + +// NewOneOf allocates a new OneOf keyword +func NewOneOf() Keyword { + return &OneOf{} +} + +// Register implements the Keyword interface for OneOf +func (o *OneOf) Register(uri string, registry *SchemaRegistry) { + for _, sch := range *o { + sch.Register(uri, registry) + } +} + +// Resolve implements the Keyword interface for OneOf +func (o *OneOf) Resolve(pointer jptr.Pointer, uri string) *Schema { + if pointer == nil { + return nil + } + current := pointer.Head() + if current == nil { + return nil + } + + pos, err := strconv.Atoi(*current) + if err != nil { + return nil + } + + if pos < 0 || pos >= len(*o) { + return nil + } + + return (*o)[pos].Resolve(pointer.Tail(), uri) + + return nil +} + +// ValidateKeyword implements the Keyword interface for OneOf +func (o *OneOf) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[OneOf] Validating") + matched := false + stateCopy := currentState.NewSubState() + stateCopy.ClearState() + for i, sch := range *o { + subState := currentState.NewSubState() + subState.ClearState() + subState.DescendBase("oneOf", strconv.Itoa(i)) + subState.DescendRelative("oneOf", strconv.Itoa(i)) + subState.Errs = &[]KeyError{} + sch.ValidateKeyword(ctx, subState, data) + stateCopy.UpdateEvaluatedPropsAndItems(subState) + if subState.IsValid() { + if matched { + currentState.AddError(data, "matched more than one specified OneOf schemas") + return + } + matched = true + } + } + if !matched { + currentState.AddError(data, "did not match any of the specified OneOf schemas") + } else { + currentState.UpdateEvaluatedPropsAndItems(stateCopy) + } +} + +// JSONProp implements the JSONPather for OneOf +func (o OneOf) JSONProp(name string) interface{} { + idx, err := strconv.Atoi(name) + if err != nil { + return nil + } + if idx > len(o) || idx < 0 { + return nil + } + return o[idx] +} + +// JSONChildren implements the JSONContainer interface for OneOf +func (o OneOf) JSONChildren() (res map[string]JSONPather) { + res = map[string]JSONPather{} + for i, sch := range o { + res[strconv.Itoa(i)] = sch + } + return +} + +// Not defines the not JSON Schema keyword +type Not Schema + +// NewNot allocates a new Not keyword +func NewNot() Keyword { + return &Not{} +} + +// Register implements the Keyword interface for Not +func (n *Not) Register(uri string, registry *SchemaRegistry) { + (*Schema)(n).Register(uri, registry) +} + +// Resolve implements the Keyword interface for Not +func (n *Not) Resolve(pointer jptr.Pointer, uri string) *Schema { + return (*Schema)(n).Resolve(pointer, uri) +} + +// ValidateKeyword implements the Keyword interface for Not +func (n *Not) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Not] Validating") + subState := currentState.NewSubState() + subState.DescendBase("not") + subState.DescendRelative("not") + + subState.Errs = &[]KeyError{} + sch := Schema(*n) + sch.ValidateKeyword(ctx, subState, data) + if subState.IsValid() { + currentState.AddError(data, "result was valid, ('not') expected invalid") + } +} + +// JSONProp implements the JSONPather for Not +func (n Not) JSONProp(name string) interface{} { + return Schema(n).JSONProp(name) +} + +// JSONChildren implements the JSONContainer interface for Not +func (n Not) JSONChildren() (res map[string]JSONPather) { + return Schema(n).JSONChildren() +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Not +func (n *Not) UnmarshalJSON(data []byte) error { + var sch Schema + if err := json.Unmarshal(data, &sch); err != nil { + return err + } + *n = Not(sch) + return nil +} + +// MarshalJSON implements the json.Marshaler interface for Not +func (n Not) MarshalJSON() ([]byte, error) { + return json.Marshal(Schema(n)) +} diff --git a/pkg/3rdparty/jsonschema/keywords_conditional.go b/pkg/3rdparty/jsonschema/keywords_conditional.go new file mode 100644 index 00000000..81809978 --- /dev/null +++ b/pkg/3rdparty/jsonschema/keywords_conditional.go @@ -0,0 +1,206 @@ +package jsonschema + +import ( + "context" + "encoding/json" + + jptr "github.com/qri-io/jsonpointer" +) + +// If defines the if JSON Schema keyword +type If Schema + +// NewIf allocates a new If keyword +func NewIf() Keyword { + return &If{} +} + +// Register implements the Keyword interface for If +func (f *If) Register(uri string, registry *SchemaRegistry) { + (*Schema)(f).Register(uri, registry) +} + +// Resolve implements the Keyword interface for If +func (f *If) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for If +func (f *If) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[If] Validating") + thenKW := currentState.Local.Keywords["then"] + elseKW := currentState.Local.Keywords["else"] + + if thenKW == nil && elseKW == nil { + // no then or else for if, aborting validation + schemaDebug("[If] Aborting validation as no then or else is present") + return + } + + subState := currentState.NewSubState() + subState.ClearState() + subState.DescendBase("if") + subState.DescendRelative("if") + + subState.Errs = &[]KeyError{} + sch := Schema(*f) + sch.ValidateKeyword(ctx, subState, data) + + currentState.Misc["ifResult"] = subState.IsValid() +} + +// JSONProp implements the JSONPather for If +func (f If) JSONProp(name string) interface{} { + return Schema(f).JSONProp(name) +} + +// JSONChildren implements the JSONContainer interface for If +func (f If) JSONChildren() (res map[string]JSONPather) { + return Schema(f).JSONChildren() +} + +// UnmarshalJSON implements the json.Unmarshaler interface for If +func (f *If) UnmarshalJSON(data []byte) error { + var sch Schema + if err := json.Unmarshal(data, &sch); err != nil { + return err + } + *f = If(sch) + return nil +} + +// MarshalJSON implements the json.Marshaler interface for If +func (f If) MarshalJSON() ([]byte, error) { + return json.Marshal(Schema(f)) +} + +// Then defines the then JSON Schema keyword +type Then Schema + +// NewThen allocates a new Then keyword +func NewThen() Keyword { + return &Then{} +} + +// Register implements the Keyword interface for Then +func (t *Then) Register(uri string, registry *SchemaRegistry) { + (*Schema)(t).Register(uri, registry) +} + +// Resolve implements the Keyword interface for Then +func (t *Then) Resolve(pointer jptr.Pointer, uri string) *Schema { + return (*Schema)(t).Resolve(pointer, uri) +} + +// ValidateKeyword implements the Keyword interface for Then +func (t *Then) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Then] Validating") + ifResult, okIf := currentState.Misc["ifResult"] + if !okIf { + schemaDebug("[Then] If result not found, skipping") + // if not found + return + } + if !(ifResult.(bool)) { + schemaDebug("[Then] If result is false, skipping") + // if was false + return + } + + subState := currentState.NewSubState() + subState.DescendBase("then") + subState.DescendRelative("then") + + sch := Schema(*t) + sch.ValidateKeyword(ctx, subState, data) + currentState.UpdateEvaluatedPropsAndItems(subState) +} + +// JSONProp implements the JSONPather for Then +func (t Then) JSONProp(name string) interface{} { + return Schema(t).JSONProp(name) +} + +// JSONChildren implements the JSONContainer interface for Then +func (t Then) JSONChildren() (res map[string]JSONPather) { + return Schema(t).JSONChildren() +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Then +func (t *Then) UnmarshalJSON(data []byte) error { + var sch Schema + if err := json.Unmarshal(data, &sch); err != nil { + return err + } + *t = Then(sch) + return nil +} + +// MarshalJSON implements the json.Marshaler interface for Then +func (t Then) MarshalJSON() ([]byte, error) { + return json.Marshal(Schema(t)) +} + +// Else defines the else JSON Schema keyword +type Else Schema + +// NewElse allocates a new Else keyword +func NewElse() Keyword { + return &Else{} +} + +// Register implements the Keyword interface for Else +func (e *Else) Register(uri string, registry *SchemaRegistry) { + (*Schema)(e).Register(uri, registry) +} + +// Resolve implements the Keyword interface for Else +func (e *Else) Resolve(pointer jptr.Pointer, uri string) *Schema { + return (*Schema)(e).Resolve(pointer, uri) +} + +// ValidateKeyword implements the Keyword interface for Else +func (e *Else) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Else] Validating") + ifResult, okIf := currentState.Misc["ifResult"] + if !okIf { + // if not found + return + } + if ifResult.(bool) { + // if was true + return + } + + subState := currentState.NewSubState() + subState.DescendBase("else") + subState.DescendRelative("else") + + sch := Schema(*e) + sch.ValidateKeyword(ctx, subState, data) +} + +// JSONProp implements the JSONPather for Else +func (e Else) JSONProp(name string) interface{} { + return Schema(e).JSONProp(name) +} + +// JSONChildren implements the JSONContainer interface for Else +func (e Else) JSONChildren() (res map[string]JSONPather) { + return Schema(e).JSONChildren() +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Else +func (e *Else) UnmarshalJSON(data []byte) error { + var sch Schema + if err := json.Unmarshal(data, &sch); err != nil { + return err + } + *e = Else(sch) + return nil +} + +// MarshalJSON implements the json.Marshaler interface for Else +func (e Else) MarshalJSON() ([]byte, error) { + return json.Marshal(Schema(e)) +} diff --git a/pkg/3rdparty/jsonschema/keywords_core.go b/pkg/3rdparty/jsonschema/keywords_core.go new file mode 100644 index 00000000..bb5d04ac --- /dev/null +++ b/pkg/3rdparty/jsonschema/keywords_core.go @@ -0,0 +1,693 @@ +package jsonschema + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strings" + + jptr "github.com/qri-io/jsonpointer" +) + +// SchemaURI defines the $schema JSON Schema keyword +type SchemaURI string + +// NewSchemaURI allocates a new SchemaURI keyword +func NewSchemaURI() Keyword { + return new(SchemaURI) +} + +// ValidateKeyword implements the Keyword interface for SchemaURI +func (s *SchemaURI) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[SchemaURI] Validating") +} + +// Register implements the Keyword interface for SchemaURI +func (s *SchemaURI) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for SchemaURI +func (s *SchemaURI) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ID defines the $Id JSON Schema keyword +type ID string + +// NewID allocates a new Id keyword +func NewID() Keyword { + return new(ID) +} + +// ValidateKeyword implements the Keyword interface for ID +func (i *ID) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Id] Validating") + // TODO(arqu): make sure ID is valid URI for draft2019 +} + +// Register implements the Keyword interface for ID +func (i *ID) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for ID +func (i *ID) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// Description defines the description JSON Schema keyword +type Description string + +// NewDescription allocates a new Description keyword +func NewDescription() Keyword { + return new(Description) +} + +// ValidateKeyword implements the Keyword interface for Description +func (d *Description) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Description] Validating") +} + +// Register implements the Keyword interface for Description +func (d *Description) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Description +func (d *Description) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// Title defines the title JSON Schema keyword +type Title string + +// NewTitle allocates a new Title keyword +func NewTitle() Keyword { + return new(Title) +} + +// ValidateKeyword implements the Keyword interface for Title +func (t *Title) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Title] Validating") +} + +// Register implements the Keyword interface for Title +func (t *Title) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Title +func (t *Title) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// Comment defines the comment JSON Schema keyword +type Comment string + +// NewComment allocates a new Comment keyword +func NewComment() Keyword { + return new(Comment) +} + +// ValidateKeyword implements the Keyword interface for Comment +func (c *Comment) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Comment] Validating") +} + +// Register implements the Keyword interface for Comment +func (c *Comment) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Comment +func (c *Comment) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// Default defines the default JSON Schema keyword +type Default struct { + Data interface{} +} + +// NewDefault allocates a new Default keyword +func NewDefault() Keyword { + return &Default{} +} + +// ValidateKeyword implements the Keyword interface for Default +func (d *Default) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Default] Validating") +} + +// Register implements the Keyword interface for Default +func (d *Default) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Default +func (d *Default) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Default +func (d *Default) UnmarshalJSON(data []byte) error { + var defaultData interface{} + if err := json.Unmarshal(data, &defaultData); err != nil { + return err + } + *d = Default{ + Data: defaultData, + } + return nil +} + +// Examples defines the examples JSON Schema keyword +type Examples []interface{} + +// NewExamples allocates a new Examples keyword +func NewExamples() Keyword { + return new(Examples) +} + +// ValidateKeyword implements the Keyword interface for Examples +func (e *Examples) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Examples] Validating") +} + +// Register implements the Keyword interface for Examples +func (e *Examples) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Examples +func (e *Examples) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ReadOnly defines the readOnly JSON Schema keyword +type ReadOnly bool + +// NewReadOnly allocates a new ReadOnly keyword +func NewReadOnly() Keyword { + return new(ReadOnly) +} + +// ValidateKeyword implements the Keyword interface for ReadOnly +func (r *ReadOnly) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[ReadOnly] Validating") +} + +// Register implements the Keyword interface for ReadOnly +func (r *ReadOnly) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for ReadOnly +func (r *ReadOnly) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// WriteOnly defines the writeOnly JSON Schema keyword +type WriteOnly bool + +// NewWriteOnly allocates a new WriteOnly keyword +func NewWriteOnly() Keyword { + return new(WriteOnly) +} + +// ValidateKeyword implements the Keyword interface for WriteOnly +func (w *WriteOnly) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[WriteOnly] Validating") +} + +// Register implements the Keyword interface for WriteOnly +func (w *WriteOnly) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for WriteOnly +func (w *WriteOnly) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// Ref defines the $ref JSON Schema keyword +type Ref struct { + Reference string + resolved *Schema + resolvedRoot *Schema + resolvedFragment *jptr.Pointer + fragmentLocalized bool +} + +// NewRef allocates a new Ref keyword +func NewRef() Keyword { + return new(Ref) +} + +// ValidateKeyword implements the Keyword interface for Ref +func (r *Ref) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Ref] Validating") + if r.resolved == nil { + r._resolveRef(ctx, currentState) + if r.resolved == nil { + currentState.AddError(data, fmt.Sprintf("failed to resolve schema for ref %s", r.Reference)) + } + } + + subState := currentState.NewSubState() + subState.ClearState() + if r.resolvedRoot != nil { + subState.BaseURI = r.resolvedRoot.DocPath + subState.Root = r.resolvedRoot + } + if r.resolvedFragment != nil && !r.resolvedFragment.IsEmpty() { + subState.BaseRelativeLocation = r.resolvedFragment + } + subState.DescendRelative("$ref") + + r.resolved.ValidateKeyword(ctx, subState, data) + + currentState.UpdateEvaluatedPropsAndItems(subState) +} + +// _resolveRef attempts to resolve the reference from the top-level context +func (r *Ref) _resolveRef(ctx context.Context, currentState *ValidationState) { + if IsLocalSchemaID(r.Reference) { + r.resolved = currentState.LocalRegistry.GetLocal(r.Reference) + if r.resolved != nil { + return + } + } + + docPath := currentState.BaseURI + refParts := strings.Split(r.Reference, "#") + address := "" + if refParts != nil && len(strings.TrimSpace(refParts[0])) > 0 { + address = refParts[0] + } else if docPath != "" { + docPathParts := strings.Split(docPath, "#") + address = docPathParts[0] + } + if len(refParts) > 1 { + frag := refParts[1] + if len(frag) > 0 && frag[0] != '/' { + frag = "/" + frag + r.fragmentLocalized = true + } + fragPointer, err := jptr.Parse(frag) + if err != nil { + r.resolvedFragment = &jptr.Pointer{} + } else { + r.resolvedFragment = &fragPointer + } + } else { + r.resolvedFragment = &jptr.Pointer{} + } + + if address != "" { + if u, err := url.Parse(address); err == nil { + if !u.IsAbs() { + address = currentState.Local.Id + address + if docPath != "" { + uriFolder := "" + if docPath[len(docPath)-1] == '/' { + uriFolder = docPath + } else { + corePath := strings.Split(docPath, "#")[0] + pathComponents := strings.Split(corePath, "/") + pathComponents = pathComponents[:len(pathComponents)-1] + uriFolder = strings.Join(pathComponents, "/") + "/" + } + address, _ = SafeResolveURL(uriFolder, address) + } + } + } + r.resolvedRoot = GetSchemaRegistry().Get(ctx, address) + } else { + r.resolvedRoot = currentState.Root + } + + if r.resolvedRoot == nil { + return + } + + knownSchema := GetSchemaRegistry().GetKnown(r.Reference) + if knownSchema != nil { + r.resolved = knownSchema + return + } + + localURI := currentState.BaseURI + if r.resolvedRoot != nil && r.resolvedRoot.DocPath != "" { + localURI = r.resolvedRoot.DocPath + if r.fragmentLocalized && !r.resolvedFragment.IsEmpty() { + current := r.resolvedFragment.Head() + sch := currentState.LocalRegistry.GetLocal("#" + *current) + if sch != nil { + r.resolved = sch + return + } + } + } + r._resolveLocalRef(localURI) +} + +// _resolveLocalRef attempts to resolve the reference from a local context +func (r *Ref) _resolveLocalRef(uri string) { + if r.resolvedFragment.IsEmpty() { + r.resolved = r.resolvedRoot + return + } + + if r.resolvedRoot != nil { + r.resolved = r.resolvedRoot.Resolve(*r.resolvedFragment, uri) + } +} + +// Register implements the Keyword interface for Ref +func (r *Ref) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Ref +func (r *Ref) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Ref +func (r *Ref) UnmarshalJSON(data []byte) error { + var ref string + if err := json.Unmarshal(data, &ref); err != nil { + return err + } + normalizedRef, _ := url.QueryUnescape(ref) + *r = Ref{ + Reference: normalizedRef, + } + return nil +} + +// MarshalJSON implements the json.Marshaler interface for Ref +func (r Ref) MarshalJSON() ([]byte, error) { + return json.Marshal(r.Reference) +} + +// RecursiveRef defines the $recursiveRef JSON Schema keyword +type RecursiveRef struct { + Reference string + resolved *Schema + resolvedRoot *Schema + resolvedFragment *jptr.Pointer + + validatingLocations map[string]bool +} + +// NewRecursiveRef allocates a new RecursiveRef keyword +func NewRecursiveRef() Keyword { + return new(RecursiveRef) +} + +// ValidateKeyword implements the Keyword interface for RecursiveRef +func (r *RecursiveRef) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[RecursiveRef] Validating") + if r.isLocationVisited(currentState.InstanceLocation.String()) { + // recursion detected aborting further descent + return + } + + if r.resolved == nil { + r._resolveRef(ctx, currentState) + if r.resolved == nil { + currentState.AddError(data, fmt.Sprintf("failed to resolve schema for ref %s", r.Reference)) + } + } + + subState := currentState.NewSubState() + subState.ClearState() + if r.resolvedRoot != nil { + subState.BaseURI = r.resolvedRoot.DocPath + subState.Root = r.resolvedRoot + } + if r.resolvedFragment != nil && !r.resolvedFragment.IsEmpty() { + subState.BaseRelativeLocation = r.resolvedFragment + } + subState.DescendRelative("$recursiveRef") + + if r.validatingLocations == nil { + r.validatingLocations = map[string]bool{} + } + + r.validatingLocations[currentState.InstanceLocation.String()] = true + r.resolved.ValidateKeyword(ctx, subState, data) + r.validatingLocations[currentState.InstanceLocation.String()] = false + + currentState.UpdateEvaluatedPropsAndItems(subState) +} + +func (r *RecursiveRef) isLocationVisited(location string) bool { + if r.validatingLocations == nil { + return false + } + v, ok := r.validatingLocations[location] + if !ok { + return false + } + return v +} + +// _resolveRef attempts to resolve the reference from the top-level context +func (r *RecursiveRef) _resolveRef(ctx context.Context, currentState *ValidationState) { + if currentState.RecursiveAnchor != nil { + if currentState.BaseURI == "" { + currentState.AddError(nil, fmt.Sprintf("base uri not set")) + return + } + baseSchema := GetSchemaRegistry().Get(ctx, currentState.BaseURI) + if baseSchema != nil && baseSchema.HasKeyword("$recursiveAnchor") { + r.resolvedRoot = currentState.RecursiveAnchor + } + } + + if IsLocalSchemaID(r.Reference) { + r.resolved = currentState.LocalRegistry.GetLocal(r.Reference) + if r.resolved != nil { + return + } + } + + docPath := currentState.BaseURI + if r.resolvedRoot != nil && r.resolvedRoot.DocPath != "" { + docPath = r.resolvedRoot.DocPath + } + + refParts := strings.Split(r.Reference, "#") + address := "" + if refParts != nil && len(strings.TrimSpace(refParts[0])) > 0 { + address = refParts[0] + } else { + address = docPath + } + + if len(refParts) > 1 { + + fragPointer, err := jptr.Parse(refParts[1]) + if err != nil { + r.resolvedFragment = &jptr.Pointer{} + } else { + r.resolvedFragment = &fragPointer + } + } else { + r.resolvedFragment = &jptr.Pointer{} + } + + if r.resolvedRoot == nil { + if address != "" { + if u, err := url.Parse(address); err == nil { + if !u.IsAbs() { + address = currentState.Local.Id + address + if docPath != "" { + uriFolder := "" + if docPath[len(docPath)-1] == '/' { + uriFolder = docPath + } else { + corePath := strings.Split(docPath, "#")[0] + pathComponents := strings.Split(corePath, "/") + pathComponents = pathComponents[:len(pathComponents)-1] + uriFolder = strings.Join(pathComponents, "/") + } + address, _ = SafeResolveURL(uriFolder, address) + } + } + } + r.resolvedRoot = GetSchemaRegistry().Get(ctx, address) + } else { + r.resolvedRoot = currentState.Root + } + } + + if r.resolvedRoot == nil { + return + } + + knownSchema := GetSchemaRegistry().GetKnown(r.Reference) + if knownSchema != nil { + r.resolved = knownSchema + return + } + + localURI := currentState.BaseURI + if r.resolvedRoot != nil && r.resolvedRoot.DocPath != "" { + localURI = r.resolvedRoot.DocPath + } + r._resolveLocalRef(localURI) +} + +// _resolveLocalRef attempts to resolve the reference from a local context +func (r *RecursiveRef) _resolveLocalRef(uri string) { + if r.resolvedFragment.IsEmpty() { + r.resolved = r.resolvedRoot + return + } + + if r.resolvedRoot != nil { + r.resolved = r.resolvedRoot.Resolve(*r.resolvedFragment, uri) + } +} + +// Register implements the Keyword interface for RecursiveRef +func (r *RecursiveRef) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for RecursiveRef +func (r *RecursiveRef) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface for RecursiveRef +func (r *RecursiveRef) UnmarshalJSON(data []byte) error { + var ref string + if err := json.Unmarshal(data, &ref); err != nil { + return err + } + *r = RecursiveRef{ + Reference: ref, + } + return nil +} + +// MarshalJSON implements the json.Marshaler interface for RecursiveRef +func (r RecursiveRef) MarshalJSON() ([]byte, error) { + return json.Marshal(r.Reference) +} + +// Anchor defines the $anchor JSON Schema keyword +type Anchor string + +// NewAnchor allocates a new Anchor keyword +func NewAnchor() Keyword { + return new(Anchor) +} + +// ValidateKeyword implements the Keyword interface for Anchor +func (a *Anchor) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Anchor] Validating") +} + +// Register implements the Keyword interface for Anchor +func (a *Anchor) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Anchor +func (a *Anchor) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// RecursiveAnchor defines the $recursiveAnchor JSON Schema keyword +type RecursiveAnchor Schema + +// NewRecursiveAnchor allocates a new RecursiveAnchor keyword +func NewRecursiveAnchor() Keyword { + return &RecursiveAnchor{} +} + +// Register implements the Keyword interface for RecursiveAnchor +func (r *RecursiveAnchor) Register(uri string, registry *SchemaRegistry) { + (*Schema)(r).Register(uri, registry) +} + +// Resolve implements the Keyword interface for RecursiveAnchor +func (r *RecursiveAnchor) Resolve(pointer jptr.Pointer, uri string) *Schema { + return (*Schema)(r).Resolve(pointer, uri) +} + +// ValidateKeyword implements the Keyword interface for RecursiveAnchor +func (r *RecursiveAnchor) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[RecursiveAnchor] Validating") + if currentState.RecursiveAnchor == nil { + currentState.RecursiveAnchor = currentState.Local + } +} + +// UnmarshalJSON implements the json.Unmarshaler interface for RecursiveAnchor +func (r *RecursiveAnchor) UnmarshalJSON(data []byte) error { + sch := &Schema{} + if err := json.Unmarshal(data, sch); err != nil { + return err + } + *r = (RecursiveAnchor)(*sch) + return nil +} + +// Defs defines the $defs JSON Schema keyword +type Defs map[string]*Schema + +// NewDefs allocates a new Defs keyword +func NewDefs() Keyword { + return &Defs{} +} + +// Register implements the Keyword interface for Defs +func (d *Defs) Register(uri string, registry *SchemaRegistry) { + for _, v := range *d { + v.Register(uri, registry) + } +} + +// Resolve implements the Keyword interface for Defs +func (d *Defs) Resolve(pointer jptr.Pointer, uri string) *Schema { + if pointer == nil { + return nil + } + current := pointer.Head() + if current == nil { + return nil + } + + if schema, ok := (*d)[*current]; ok { + return schema.Resolve(pointer.Tail(), uri) + } + + return nil +} + +// ValidateKeyword implements the Keyword interface for Defs +func (d Defs) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Defs] Validating") +} + +// JSONProp implements the JSONPather for Defs +func (d Defs) JSONProp(name string) interface{} { + return d[name] +} + +// JSONChildren implements the JSONContainer interface for Defs +func (d Defs) JSONChildren() (res map[string]JSONPather) { + res = map[string]JSONPather{} + for key, sch := range d { + res[key] = sch + } + return +} + +// Void is a placeholder definition for a keyword +type Void struct{} + +// NewVoid allocates a new Void keyword +func NewVoid() Keyword { + return &Void{} +} + +// Register implements the Keyword interface for Void +func (vo *Void) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Void +func (vo *Void) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for Void +func (vo *Void) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Void] Validating") + schemaDebug("[Void] WARNING this is a placeholder and should not be used") + schemaDebug("[Void] Void is always true") +} diff --git a/pkg/3rdparty/jsonschema/keywords_numeric.go b/pkg/3rdparty/jsonschema/keywords_numeric.go new file mode 100644 index 00000000..e4b379f7 --- /dev/null +++ b/pkg/3rdparty/jsonschema/keywords_numeric.go @@ -0,0 +1,172 @@ +package jsonschema + +import ( + "context" + "fmt" + + jptr "github.com/qri-io/jsonpointer" +) + +// MultipleOf defines the multipleOf JSON Schema keyword +type MultipleOf float64 + +// NewMultipleOf allocates a new MultipleOf keyword +func NewMultipleOf() Keyword { + return new(MultipleOf) +} + +// Register implements the Keyword interface for MultipleOf +func (m *MultipleOf) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for MultipleOf +func (m *MultipleOf) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for MultipleOf +func (m MultipleOf) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[MultipleOf] Validating") + if num, ok := convertNumberToFloat(data); ok { + div := num / float64(m) + if float64(int(div)) != div { + currentState.AddError(data, fmt.Sprintf("must be a multiple of %v", m)) + } + } +} + +// Maximum defines the maximum JSON Schema keyword +type Maximum float64 + +// NewMaximum allocates a new Maximum keyword +func NewMaximum() Keyword { + return new(Maximum) +} + +// Register implements the Keyword interface for Maximum +func (m *Maximum) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Maximum +func (m *Maximum) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for Maximum +func (m Maximum) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Maximum] Validating") + if num, ok := convertNumberToFloat(data); ok { + if num > float64(m) { + currentState.AddError(data, fmt.Sprintf("must be less than or equal to %v", m)) + } + } +} + +// ExclusiveMaximum defines the exclusiveMaximum JSON Schema keyword +type ExclusiveMaximum float64 + +// NewExclusiveMaximum allocates a new ExclusiveMaximum keyword +func NewExclusiveMaximum() Keyword { + return new(ExclusiveMaximum) +} + +// Register implements the Keyword interface for ExclusiveMaximum +func (m *ExclusiveMaximum) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for ExclusiveMaximum +func (m *ExclusiveMaximum) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for ExclusiveMaximum +func (m ExclusiveMaximum) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[ExclusiveMaximum] Validating") + if num, ok := convertNumberToFloat(data); ok { + if num >= float64(m) { + currentState.AddError(data, fmt.Sprintf("%v must be less than %v", num, m)) + } + } +} + +// Minimum defines the minimum JSON Schema keyword +type Minimum float64 + +// NewMinimum allocates a new Minimum keyword +func NewMinimum() Keyword { + return new(Minimum) +} + +// Register implements the Keyword interface for Minimum +func (m *Minimum) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Minimum +func (m *Minimum) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for Minimum +func (m Minimum) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Minimum] Validating") + if num, ok := convertNumberToFloat(data); ok { + if num < float64(m) { + currentState.AddError(data, fmt.Sprintf("must be greater than or equal to %v", m)) + } + } +} + +// ExclusiveMinimum defines the exclusiveMinimum JSON Schema keyword +type ExclusiveMinimum float64 + +// NewExclusiveMinimum allocates a new ExclusiveMinimum keyword +func NewExclusiveMinimum() Keyword { + return new(ExclusiveMinimum) +} + +// Register implements the Keyword interface for ExclusiveMinimum +func (m *ExclusiveMinimum) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for ExclusiveMinimum +func (m *ExclusiveMinimum) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for ExclusiveMinimum +func (m ExclusiveMinimum) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[ExclusiveMinimum] Validating") + if num, ok := convertNumberToFloat(data); ok { + if num <= float64(m) { + currentState.AddError(data, fmt.Sprintf("%v must be greater than %v", num, m)) + } + } +} + +func convertNumberToFloat(data interface{}) (float64, bool) { + switch v := data.(type) { + case uint: + return float64(v), true + case uint8: + return float64(v), true + case uint16: + return float64(v), true + case uint32: + return float64(v), true + case uint64: + return float64(v), true + case int: + return float64(v), true + case int8: + return float64(v), true + case int16: + return float64(v), true + case int32: + return float64(v), true + case int64: + return float64(v), true + case float32: + return float64(v), true + case float64: + return float64(v), true + case uintptr: + return float64(v), true + } + + return 0, false +} diff --git a/pkg/3rdparty/jsonschema/keywords_object.go b/pkg/3rdparty/jsonschema/keywords_object.go new file mode 100644 index 00000000..d38907a9 --- /dev/null +++ b/pkg/3rdparty/jsonschema/keywords_object.go @@ -0,0 +1,723 @@ +package jsonschema + +import ( + "context" + "encoding/json" + "fmt" + orderedmap "github.com/wk8/go-ordered-map/v2" + "regexp" + "strconv" + + jptr "github.com/qri-io/jsonpointer" +) + +// Properties defines the properties JSON Schema keyword +type Properties []Property + +type Property struct { + Key string + Value *Schema +} + +func (p *Properties) UnmarshalJSON(data []byte) error { + om := orderedmap.New[string, *Schema]() + err := json.Unmarshal(data, &om) + if err != nil { + return err + } + + for pair := om.Oldest(); pair != nil; pair = pair.Next() { + key := pair.Key + value := pair.Value + *p = append(*p, Property{key, value}) + } + + return nil +} + +func (p *Properties) Get(key string) (*Schema, bool) { + for _, v := range *p { + if v.Key == key { + return v.Value, true + } + } + return nil, false +} + +// NewProperties allocates a new Properties keyword +func NewProperties() Keyword { + return &Properties{} +} + +// Register implements the Keyword interface for Properties +func (p *Properties) Register(uri string, registry *SchemaRegistry) { + for _, v := range *p { + v.Value.Register(uri, registry) + } +} + +// Resolve implements the Keyword interface for Properties +func (p *Properties) Resolve(pointer jptr.Pointer, uri string) *Schema { + if pointer == nil { + return nil + } + current := pointer.Head() + if current == nil { + return nil + } + + if schema, ok := p.Get(*current); ok { + return schema.Resolve(pointer.Tail(), uri) + } + + return nil +} + +// ValidateKeyword implements the Keyword interface for Properties +func (p Properties) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Properties] Validating") + if obj, ok := data.(map[string]interface{}); ok { + subState := currentState.NewSubState() + for _, v := range p { + key := v.Key + if _, ok := obj[key]; ok { + currentState.SetEvaluatedKey(key) + subState.ClearState() + subState.DescendBaseFromState(currentState, "properties", key) + subState.DescendRelativeFromState(currentState, "properties", key) + subState.DescendInstanceFromState(currentState, key) + + subState.Errs = &[]KeyError{} + v.Value.ValidateKeyword(ctx, subState, obj[key]) + currentState.AddSubErrors(*subState.Errs...) + if subState.IsValid() { + currentState.UpdateEvaluatedPropsAndItems(subState) + } + } + } + } +} + +// JSONProp implements the JSONPather for Properties +func (p Properties) JSONProp(name string) interface{} { + res, _ := p.Get(name) + return res +} + +// JSONChildren implements the JSONContainer interface for Properties +func (p Properties) JSONChildren() (res map[string]JSONPather) { + res = map[string]JSONPather{} + for _, v := range p { + res[v.Key] = v.Value + } + return +} + +// Required defines the required JSON Schema keyword +type Required []string + +// NewRequired allocates a new Required keyword +func NewRequired() Keyword { + return &Required{} +} + +// Register implements the Keyword interface for Required +func (r *Required) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Required +func (r *Required) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for Required +func (r Required) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Required] Validating") + if obj, ok := data.(map[string]interface{}); ok { + for _, key := range r { + if _, ok := obj[key]; !ok { + currentState.AddError(data, fmt.Sprintf(`"%s" value is required`, key)) + } + } + } +} + +// JSONProp implements the JSONPather for Required +func (r Required) JSONProp(name string) interface{} { + idx, err := strconv.Atoi(name) + if err != nil { + return nil + } + if idx > len(r) || idx < 0 { + return nil + } + return r[idx] +} + +// MaxProperties defines the maxProperties JSON Schema keyword +type MaxProperties int + +// NewMaxProperties allocates a new MaxProperties keyword +func NewMaxProperties() Keyword { + return new(MaxProperties) +} + +// Register implements the Keyword interface for MaxProperties +func (m *MaxProperties) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for MaxProperties +func (m *MaxProperties) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for MaxProperties +func (m MaxProperties) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[MaxProperties] Validating") + if obj, ok := data.(map[string]interface{}); ok { + if len(obj) > int(m) { + currentState.AddError(data, fmt.Sprintf("%d object Properties exceed %d maximum", len(obj), m)) + } + } +} + +// MinProperties defines the minProperties JSON Schema keyword +type MinProperties int + +// NewMinProperties allocates a new MinProperties keyword +func NewMinProperties() Keyword { + return new(MinProperties) +} + +// Register implements the Keyword interface for MinProperties +func (m *MinProperties) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for MinProperties +func (m *MinProperties) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for MinProperties +func (m MinProperties) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[MinProperties] Validating") + if obj, ok := data.(map[string]interface{}); ok { + if len(obj) < int(m) { + currentState.AddError(data, fmt.Sprintf("%d object Properties below %d minimum", len(obj), m)) + } + } +} + +// PatternProperties defines the patternProperties JSON Schema keyword +type PatternProperties []patternSchema + +// NewPatternProperties allocates a new PatternProperties keyword +func NewPatternProperties() Keyword { + return &PatternProperties{} +} + +type patternSchema struct { + key string + re *regexp.Regexp + schema *Schema +} + +// Register implements the Keyword interface for PatternProperties +func (p *PatternProperties) Register(uri string, registry *SchemaRegistry) { + for _, v := range *p { + v.schema.Register(uri, registry) + } +} + +// Resolve implements the Keyword interface for PatternProperties +func (p *PatternProperties) Resolve(pointer jptr.Pointer, uri string) *Schema { + if pointer == nil { + return nil + } + current := pointer.Head() + if current == nil { + return nil + } + + patProp := &patternSchema{} + + for _, v := range *p { + if v.key == *current { + patProp = &v + break + } + } + + return patProp.schema.Resolve(pointer.Tail(), uri) +} + +// ValidateKeyword implements the Keyword interface for PatternProperties +func (p PatternProperties) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[PatternProperties] Validating") + if obj, ok := data.(map[string]interface{}); ok { + for key, val := range obj { + for _, ptn := range p { + if ptn.re.Match([]byte(key)) { + currentState.SetEvaluatedKey(key) + subState := currentState.NewSubState() + subState.DescendBase("patternProperties", key) + subState.DescendRelative("patternProperties", key) + subState.DescendInstance(key) + + subState.Errs = &[]KeyError{} + ptn.schema.ValidateKeyword(ctx, subState, val) + currentState.AddSubErrors(*subState.Errs...) + + if subState.IsValid() { + currentState.UpdateEvaluatedPropsAndItems(subState) + } + } + } + } + } +} + +// JSONProp implements the JSONPather for PatternProperties +func (p PatternProperties) JSONProp(name string) interface{} { + for _, pp := range p { + if pp.key == name { + return pp.schema + } + } + return nil +} + +// JSONChildren implements the JSONContainer interface for PatternProperties +func (p PatternProperties) JSONChildren() (res map[string]JSONPather) { + res = map[string]JSONPather{} + for i, pp := range p { + res[strconv.Itoa(i)] = pp.schema + } + return +} + +// UnmarshalJSON implements the json.Unmarshaler interface for PatternProperties +func (p *PatternProperties) UnmarshalJSON(data []byte) error { + var props map[string]*Schema + if err := json.Unmarshal(data, &props); err != nil { + return err + } + + ptn := make(PatternProperties, len(props)) + i := 0 + for key, sch := range props { + re, err := regexp.Compile(key) + if err != nil { + return fmt.Errorf("invalid pattern: %s: %s", key, err.Error()) + } + ptn[i] = patternSchema{ + key: key, + re: re, + schema: sch, + } + i++ + } + + *p = ptn + return nil +} + +// MarshalJSON implements the json.Marshaler interface for PatternProperties +func (p PatternProperties) MarshalJSON() ([]byte, error) { + obj := map[string]interface{}{} + for _, prop := range p { + obj[prop.key] = prop.schema + } + return json.Marshal(obj) +} + +// AdditionalProperties defines the additionalProperties JSON Schema keyword +type AdditionalProperties Schema + +// NewAdditionalProperties allocates a new AdditionalProperties keyword +func NewAdditionalProperties() Keyword { + return &AdditionalProperties{} +} + +// Register implements the Keyword interface for AdditionalProperties +func (ap *AdditionalProperties) Register(uri string, registry *SchemaRegistry) { + (*Schema)(ap).Register(uri, registry) +} + +// Resolve implements the Keyword interface for AdditionalProperties +func (ap *AdditionalProperties) Resolve(pointer jptr.Pointer, uri string) *Schema { + return (*Schema)(ap).Resolve(pointer, uri) +} + +// ValidateKeyword implements the Keyword interface for AdditionalProperties +func (ap *AdditionalProperties) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[AdditionalProperties] Validating") + if obj, ok := data.(map[string]interface{}); ok { + subState := currentState.NewSubState() + subState.ClearState() + subState.DescendBase("additionalProperties") + subState.DescendRelative("additionalProperties") + for key := range obj { + if currentState.IsLocallyEvaluatedKey(key) { + continue + } + if ap.SchemaType == SchemaTypeFalse { + currentState.AddError(data, "additional properties are not allowed") + return + } + currentState.SetEvaluatedKey(key) + subState.ClearState() + subState.DescendInstanceFromState(currentState, key) + + (*Schema)(ap).ValidateKeyword(ctx, subState, obj[key]) + currentState.UpdateEvaluatedPropsAndItems(subState) + } + } +} + +// UnmarshalJSON implements the json.Unmarshaler interface for AdditionalProperties +func (ap *AdditionalProperties) UnmarshalJSON(data []byte) error { + sch := &Schema{} + if err := json.Unmarshal(data, sch); err != nil { + return err + } + *ap = (AdditionalProperties)(*sch) + return nil +} + +// PropertyNames defines the propertyNames JSON Schema keyword +type PropertyNames Schema + +// NewPropertyNames allocates a new PropertyNames keyword +func NewPropertyNames() Keyword { + return &PropertyNames{} +} + +// Register implements the Keyword interface for PropertyNames +func (p *PropertyNames) Register(uri string, registry *SchemaRegistry) { + (*Schema)(p).Register(uri, registry) +} + +// Resolve implements the Keyword interface for PropertyNames +func (p *PropertyNames) Resolve(pointer jptr.Pointer, uri string) *Schema { + return (*Schema)(p).Resolve(pointer, uri) +} + +// ValidateKeyword implements the Keyword interface for PropertyNames +func (p *PropertyNames) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[PropertyNames] Validating") + if obj, ok := data.(map[string]interface{}); ok { + for key := range obj { + subState := currentState.NewSubState() + subState.DescendBase("propertyNames") + subState.DescendRelative("propertyNames") + subState.DescendInstance(key) + (*Schema)(p).ValidateKeyword(ctx, subState, key) + } + } +} + +// JSONProp implements the JSONPather for PropertyNames +func (p PropertyNames) JSONProp(name string) interface{} { + return Schema(p).JSONProp(name) +} + +// JSONChildren implements the JSONContainer interface for PropertyNames +func (p PropertyNames) JSONChildren() (res map[string]JSONPather) { + return Schema(p).JSONChildren() +} + +// UnmarshalJSON implements the json.Unmarshaler interface for PropertyNames +func (p *PropertyNames) UnmarshalJSON(data []byte) error { + var sch Schema + if err := json.Unmarshal(data, &sch); err != nil { + return err + } + *p = PropertyNames(sch) + return nil +} + +// MarshalJSON implements the json.Marshaler interface for PropertyNames +func (p PropertyNames) MarshalJSON() ([]byte, error) { + return json.Marshal(Schema(p)) +} + +// DependentSchemas defines the dependentSchemas JSON Schema keyword +type DependentSchemas map[string]SchemaDependency + +// NewDependentSchemas allocates a new DependentSchemas keyword +func NewDependentSchemas() Keyword { + return &DependentSchemas{} +} + +// Register implements the Keyword interface for DependentSchemas +func (d *DependentSchemas) Register(uri string, registry *SchemaRegistry) { + for _, v := range *d { + v.schema.Register(uri, registry) + } +} + +// Resolve implements the Keyword interface for DependentSchemas +func (d *DependentSchemas) Resolve(pointer jptr.Pointer, uri string) *Schema { + if pointer == nil { + return nil + } + current := pointer.Head() + if current == nil { + return nil + } + + if schema, ok := (*d)[*current]; ok { + return schema.Resolve(pointer.Tail(), uri) + } + + return nil +} + +// ValidateKeyword implements the Keyword interface for DependentSchemas +func (d *DependentSchemas) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[DependentSchemas] Validating") + for _, v := range *d { + subState := currentState.NewSubState() + subState.DescendBase("dependentSchemas") + subState.DescendRelative("dependentSchemas") + subState.Misc["dependencyParent"] = "dependentSchemas" + v.ValidateKeyword(ctx, subState, data) + } +} + +type _dependentSchemas map[string]Schema + +// UnmarshalJSON implements the json.Unmarshaler interface for DependentSchemas +func (d *DependentSchemas) UnmarshalJSON(data []byte) error { + _d := _dependentSchemas{} + if err := json.Unmarshal(data, &_d); err != nil { + return err + } + ds := DependentSchemas{} + for k, v := range _d { + sch := Schema(v) + ds[k] = SchemaDependency{ + schema: &sch, + prop: k, + } + } + *d = ds + return nil +} + +// JSONProp implements the JSONPather for DependentSchemas +func (d DependentSchemas) JSONProp(name string) interface{} { + return d[name] +} + +// JSONChildren implements the JSONContainer interface for DependentSchemas +func (d DependentSchemas) JSONChildren() (r map[string]JSONPather) { + r = map[string]JSONPather{} + for key, val := range d { + r[key] = val + } + return +} + +// SchemaDependency is the internal representation of a dependent schema +type SchemaDependency struct { + schema *Schema + prop string +} + +// Register implements the Keyword interface for SchemaDependency +func (d *SchemaDependency) Register(uri string, registry *SchemaRegistry) { + d.schema.Register(uri, registry) +} + +// Resolve implements the Keyword interface for SchemaDependency +func (d *SchemaDependency) Resolve(pointer jptr.Pointer, uri string) *Schema { + return d.schema.Resolve(pointer, uri) +} + +// ValidateKeyword implements the Keyword interface for SchemaDependency +func (d *SchemaDependency) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[SchemaDependency] Validating") + depsData := map[string]interface{}{} + ok := false + if depsData, ok = data.(map[string]interface{}); !ok { + return + } + if _, okProp := depsData[d.prop]; !okProp { + return + } + subState := currentState.NewSubState() + subState.DescendBase(d.prop) + subState.DescendRelative(d.prop) + d.schema.ValidateKeyword(ctx, subState, data) +} + +// MarshalJSON implements the json.Marshaler interface for SchemaDependency +func (d SchemaDependency) MarshalJSON() ([]byte, error) { + return json.Marshal(d.schema) +} + +// JSONProp implements the JSONPather for SchemaDependency +func (d SchemaDependency) JSONProp(name string) interface{} { + return d.schema.JSONProp(name) +} + +// DependentRequired defines the dependentRequired JSON Schema keyword +type DependentRequired map[string]PropertyDependency + +// NewDependentRequired allocates a new DependentRequired keyword +func NewDependentRequired() Keyword { + return &DependentRequired{} +} + +// Register implements the Keyword interface for DependentRequired +func (d *DependentRequired) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for DependentRequired +func (d *DependentRequired) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for DependentRequired +func (d *DependentRequired) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[DependentRequired] Validating") + for _, prop := range *d { + subState := currentState.NewSubState() + subState.DescendBase("dependentRequired") + subState.DescendRelative("dependentRequired") + subState.Misc["dependencyParent"] = "dependentRequired" + prop.ValidateKeyword(ctx, subState, data) + } +} + +type _dependentRequired map[string][]string + +// UnmarshalJSON implements the json.Unmarshaler interface for DependentRequired +func (d *DependentRequired) UnmarshalJSON(data []byte) error { + _d := _dependentRequired{} + if err := json.Unmarshal(data, &_d); err != nil { + return err + } + dr := DependentRequired{} + for k, v := range _d { + dr[k] = PropertyDependency{ + dependencies: v, + prop: k, + } + } + *d = dr + return nil +} + +// MarshalJSON implements the json.Marshaler interface for DependentRequired +func (d DependentRequired) MarshalJSON() ([]byte, error) { + obj := map[string]interface{}{} + for key, prop := range d { + obj[key] = prop.dependencies + } + return json.Marshal(obj) +} + +// JSONProp implements the JSONPather for DependentRequired +func (d DependentRequired) JSONProp(name string) interface{} { + return d[name] +} + +// JSONChildren implements the JSONContainer interface for DependentRequired +func (d DependentRequired) JSONChildren() (r map[string]JSONPather) { + r = map[string]JSONPather{} + for key, val := range d { + r[key] = val + } + return +} + +// PropertyDependency is the internal representation of a dependent property +type PropertyDependency struct { + dependencies []string + prop string +} + +// Register implements the Keyword interface for PropertyDependency +func (p *PropertyDependency) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for PropertyDependency +func (p *PropertyDependency) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for PropertyDependency +func (p *PropertyDependency) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[PropertyDependency] Validating") + if obj, ok := data.(map[string]interface{}); ok { + if obj[p.prop] == nil { + return + } + for _, dep := range p.dependencies { + if obj[dep] == nil { + currentState.AddError(data, fmt.Sprintf(`"%s" property is required`, dep)) + } + } + } +} + +// JSONProp implements the JSONPather for PropertyDependency +func (p PropertyDependency) JSONProp(name string) interface{} { + idx, err := strconv.Atoi(name) + if err != nil { + return nil + } + if idx > len(p.dependencies) || idx < 0 { + return nil + } + return p.dependencies[idx] +} + +// UnevaluatedProperties defines the unevaluatedProperties JSON Schema keyword +type UnevaluatedProperties Schema + +// NewUnevaluatedProperties allocates a new UnevaluatedProperties keyword +func NewUnevaluatedProperties() Keyword { + return &UnevaluatedProperties{} +} + +// Register implements the Keyword interface for UnevaluatedProperties +func (up *UnevaluatedProperties) Register(uri string, registry *SchemaRegistry) { + (*Schema)(up).Register(uri, registry) +} + +// Resolve implements the Keyword interface for UnevaluatedProperties +func (up *UnevaluatedProperties) Resolve(pointer jptr.Pointer, uri string) *Schema { + return (*Schema)(up).Resolve(pointer, uri) +} + +// ValidateKeyword implements the Keyword interface for UnevaluatedProperties +func (up *UnevaluatedProperties) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[UnevaluatedProperties] Validating") + if obj, ok := data.(map[string]interface{}); ok { + subState := currentState.NewSubState() + subState.ClearState() + subState.DescendBase("unevaluatedProperties") + subState.DescendRelative("unevaluatedProperties") + for key := range obj { + if currentState.IsEvaluatedKey(key) { + continue + } + if up.SchemaType == SchemaTypeFalse { + currentState.AddError(data, "unevaluated properties are not allowed") + return + } + subState.DescendInstanceFromState(currentState, key) + + (*Schema)(up).ValidateKeyword(ctx, subState, obj[key]) + } + } +} + +// UnmarshalJSON implements the json.Unmarshaler interface for UnevaluatedProperties +func (up *UnevaluatedProperties) UnmarshalJSON(data []byte) error { + sch := &Schema{} + if err := json.Unmarshal(data, sch); err != nil { + return err + } + *up = (UnevaluatedProperties)(*sch) + return nil +} diff --git a/pkg/3rdparty/jsonschema/keywords_optional.go b/pkg/3rdparty/jsonschema/keywords_optional.go new file mode 100644 index 00000000..76ba5d03 --- /dev/null +++ b/pkg/3rdparty/jsonschema/keywords_optional.go @@ -0,0 +1,325 @@ +package jsonschema + +import ( + "context" + "fmt" + "net" + "net/mail" + "net/url" + "regexp" + "strconv" + "strings" + "time" + + jptr "github.com/qri-io/jsonpointer" +) + +const ( + hostname string = `^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$` + unescapedTilda = `\~[^01]` + endingTilda = `\~$` + schemePrefix = `^[^\:]+\:` + uriTemplate = `\{[^\{\}\\]*\}` +) + +var ( + // emailPattern = regexp.MustCompile(email) + hostnamePattern = regexp.MustCompile(hostname) + unescaptedTildaPattern = regexp.MustCompile(unescapedTilda) + endingTildaPattern = regexp.MustCompile(endingTilda) + schemePrefixPattern = regexp.MustCompile(schemePrefix) + uriTemplatePattern = regexp.MustCompile(uriTemplate) + + disallowedIdnChars = map[string]bool{"\u0020": true, "\u002D": true, "\u00A2": true, "\u00A3": true, "\u00A4": true, "\u00A5": true, "\u034F": true, "\u0640": true, "\u07FA": true, "\u180B": true, "\u180C": true, "\u180D": true, "\u200B": true, "\u2060": true, "\u2104": true, "\u2108": true, "\u2114": true, "\u2117": true, "\u2118": true, "\u211E": true, "\u211F": true, "\u2123": true, "\u2125": true, "\u2282": true, "\u2283": true, "\u2284": true, "\u2285": true, "\u2286": true, "\u2287": true, "\u2288": true, "\u2616": true, "\u2617": true, "\u2619": true, "\u262F": true, "\u2638": true, "\u266C": true, "\u266D": true, "\u266F": true, "\u2752": true, "\u2756": true, "\u2758": true, "\u275E": true, "\u2761": true, "\u2775": true, "\u2794": true, "\u2798": true, "\u27AF": true, "\u27B1": true, "\u27BE": true, "\u3004": true, "\u3012": true, "\u3013": true, "\u3020": true, "\u302E": true, "\u302F": true, "\u3031": true, "\u3032": true, "\u3035": true, "\u303B": true, "\u3164": true, "\uFFA0": true} +) + +// Format defines the format JSON Schema keyword +type Format string + +// NewFormat allocates a new Format keyword +func NewFormat() Keyword { + return new(Format) +} + +// Register implements the Keyword interface for Format +func (f *Format) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Format +func (f *Format) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for Format +func (f Format) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Format] Validating") + var err error + if str, ok := data.(string); ok { + switch f { + case "date-time": + err = isValidDateTime(str) + case "date": + err = isValidDate(str) + case "email": + err = isValidEmail(str) + case "hostname": + err = isValidHostname(str) + case "idn-email": + err = isValidIDNEmail(str) + case "idn-hostname": + err = isValidIDNHostname(str) + case "ipv4": + err = isValidIPv4(str) + case "ipv6": + err = isValidIPv6(str) + case "iri-reference": + err = isValidIriRef(str) + case "iri": + err = isValidIri(str) + case "json-pointer": + err = isValidJSONPointer(str) + case "regex": + err = isValidRegex(str) + case "relative-json-pointer": + err = isValidRelJSONPointer(str) + case "time": + err = isValidTime(str) + case "uri-reference": + err = isValidURIRef(str) + case "uri-template": + err = isValidURITemplate(str) + case "uri": + err = isValidURI(str) + default: + err = nil + } + if err != nil { + currentState.AddError(data, fmt.Sprintf("invalid %s: %s", f, err.Error())) + } + } +} + +// A string instance is valid against "date-time" if it is a valid +// representation according to the "date-time" production derived +// from RFC 3339, section 5.6 [RFC3339] +// https://tools.ietf.org/html/rfc3339#section-5.6 +func isValidDateTime(dateTime string) error { + if _, err := time.Parse(time.RFC3339, strings.ToUpper(dateTime)); err != nil { + return fmt.Errorf("date-time incorrectly Formatted: %s", err.Error()) + } + return nil +} + +// A string instance is valid against "date" if it is a valid +// representation according to the "full-date" production derived +// from RFC 3339, section 5.6 [RFC3339] +// https://tools.ietf.org/html/rfc3339#section-5.6 +func isValidDate(date string) error { + arbitraryTime := "T08:30:06.283185Z" + dateTime := fmt.Sprintf("%s%s", date, arbitraryTime) + return isValidDateTime(dateTime) +} + +// A string instance is valid against "email" if it is a valid +// representation as defined by RFC 5322, section 3.4.1 [RFC5322]. +// https://tools.ietf.org/html/rfc5322#section-3.4.1 +func isValidEmail(email string) error { + // if !emailPattern.MatchString(email) { + // return fmt.Errorf("invalid email Format") + // } + if _, err := mail.ParseAddress(email); err != nil { + return fmt.Errorf("email address incorrectly Formatted: %s", err.Error()) + } + return nil +} + +// A string instance is valid against "hostname" if it is a valid +// representation as defined by RFC 1034, section 3.1 [RFC1034], +// including host names produced using the Punycode algorithm +// specified in RFC 5891, section 4.4 [RFC5891]. +// https://tools.ietf.org/html/rfc1034#section-3.1 +// https://tools.ietf.org/html/rfc5891#section-4.4 +func isValidHostname(hostname string) error { + if !hostnamePattern.MatchString(hostname) || len(hostname) > 255 { + return fmt.Errorf("invalid hostname string") + } + return nil +} + +// A string instance is valid against "idn-email" if it is a valid +// representation as defined by RFC 6531 [RFC6531] +// https://tools.ietf.org/html/rfc6531 +func isValidIDNEmail(idnEmail string) error { + if _, err := mail.ParseAddress(idnEmail); err != nil { + return fmt.Errorf("email address incorrectly Formatted: %s", err.Error()) + } + return nil +} + +// A string instance is valid against "hostname" if it is a valid +// representation as defined by either RFC 1034 as for hostname, or +// an internationalized hostname as defined by RFC 5890, section +// 2.3.2.3 [RFC5890]. +// https://tools.ietf.org/html/rfc1034 +// https://tools.ietf.org/html/rfc5890#section-2.3.2.3 +// https://pdfs.semanticscholar.org/9275/6bcecb29d3dc407e23a997b256be6ff4149d.pdf +func isValidIDNHostname(idnHostname string) error { + if len(idnHostname) > 255 { + return fmt.Errorf("invalid idn hostname string") + } + for _, r := range idnHostname { + s := string(r) + if disallowedIdnChars[s] { + return fmt.Errorf("invalid hostname: contains illegal character %#U", r) + } + } + return nil +} + +// A string instance is valid against "ipv4" if it is a valid +// representation of an IPv4 address according to the "dotted-quad" +// ABNF syntax as defined in RFC 2673, section 3.2 [RFC2673]. +// https://tools.ietf.org/html/rfc2673#section-3.2 +func isValidIPv4(ipv4 string) error { + parsedIP := net.ParseIP(ipv4) + hasDots := strings.Contains(ipv4, ".") + if !hasDots || parsedIP == nil { + return fmt.Errorf("invalid IPv4 address") + } + return nil +} + +// A string instance is valid against "ipv6" if it is a valid +// representation of an IPv6 address as defined in RFC 4291, section +// 2.2 [RFC4291]. +// https://tools.ietf.org/html/rfc4291#section-2.2 +func isValidIPv6(ipv6 string) error { + parsedIP := net.ParseIP(ipv6) + hasColons := strings.Contains(ipv6, ":") + if !hasColons || parsedIP == nil { + return fmt.Errorf("invalid IPv4 address") + } + return nil +} + +// A string instance is a valid against "iri-reference" if it is a +// valid IRI Reference (either an IRI or a relative-reference), +// according to [RFC3987]. +// https://tools.ietf.org/html/rfc3987 +func isValidIriRef(iriRef string) error { + return isValidURIRef(iriRef) +} + +// A string instance is a valid against "iri" if it is a valid IRI, +// according to [RFC3987]. +// https://tools.ietf.org/html/rfc3987 +func isValidIri(iri string) error { + return isValidURI(iri) +} + +// A string instance is a valid against "json-pointer" if it is a +// valid JSON string representation of a JSON Pointer, according to +// RFC 6901, section 5 [RFC6901]. +// https://tools.ietf.org/html/rfc6901#section-5 +func isValidJSONPointer(jsonPointer string) error { + if len(jsonPointer) == 0 { + return nil + } + if jsonPointer[0] != '/' { + return fmt.Errorf("non-empty references must begin with a '/' character") + } + str := jsonPointer[1:] + if unescaptedTildaPattern.MatchString(str) { + return fmt.Errorf("unescaped tilda error") + } + if endingTildaPattern.MatchString(str) { + return fmt.Errorf("unescaped tilda error") + } + return nil +} + +// A string instance is a valid against "regex" if it is a valid +// regular expression according to the ECMA 262 [ecma262] regular +// expression dialect. Implementations that validate Formats MUST +// accept at least the subset of ECMA 262 defined in the Regular +// Expressions [regexInterop] section of this specification, and +// SHOULD accept all valid ECMA 262 expressions. +// http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf +// http://json-schema.org/latest/jsoxn-schema-validation.html#regexInterop +// https://tools.ietf.org/html/rfc7159 +func isValidRegex(regex string) error { + if _, err := regexp.Compile(regex); err != nil { + return fmt.Errorf("invalid regex expression") + } + return nil +} + +// A string instance is a valid against "relative-json-pointer" if it +// is a valid Relative JSON Pointer [relative-json-pointer]. +// https://tools.ietf.org/html/draft-handrews-relative-json-pointer-00 +func isValidRelJSONPointer(relJSONPointer string) error { + parts := strings.Split(relJSONPointer, "/") + if len(parts) == 1 { + parts = strings.Split(relJSONPointer, "#") + } + if i, err := strconv.Atoi(parts[0]); err != nil || i < 0 { + return fmt.Errorf("RJP must begin with positive integer") + } + //skip over first part + str := relJSONPointer[len(parts[0]):] + if len(str) > 0 && str[0] == '#' { + return nil + } + return isValidJSONPointer(str) +} + +// A string instance is valid against "time" if it is a valid +// representation according to the "full-time" production derived +// from RFC 3339, section 5.6 [RFC3339] +// https://tools.ietf.org/html/rfc3339#section-5.6 +func isValidTime(time string) error { + arbitraryDate := "1963-06-19" + dateTime := fmt.Sprintf("%sT%s", arbitraryDate, time) + return isValidDateTime(dateTime) + return nil +} + +// A string instance is a valid against "uri-reference" if it is a +// valid URI Reference (either a URI or a relative-reference), +// according to [RFC3986]. +// https://tools.ietf.org/html/rfc3986 +func isValidURIRef(uriRef string) error { + if _, err := url.Parse(uriRef); err != nil { + return fmt.Errorf("uri incorrectly Formatted: %s", err.Error()) + } + if strings.Contains(uriRef, "\\") { + return fmt.Errorf("invalid uri") + } + return nil +} + +// A string instance is a valid against "uri-template" if it is a +// valid URI Template (of any level), according to [RFC6570]. Note +// that URI Templates may be used for IRIs; there is no separate IRI +// Template specification. +// https://tools.ietf.org/html/rfc6570 +func isValidURITemplate(uriTemplate string) error { + arbitraryValue := "aaa" + uriRef := uriTemplatePattern.ReplaceAllString(uriTemplate, arbitraryValue) + if strings.Contains(uriRef, "{") || strings.Contains(uriRef, "}") { + return fmt.Errorf("invalid uri template") + } + return isValidURIRef(uriRef) +} + +// A string instance is a valid against "uri" if it is a valid URI, +// according to [RFC3986]. +// https://tools.ietf.org/html/rfc3986 +func isValidURI(uri string) error { + if _, err := url.Parse(uri); err != nil { + return fmt.Errorf("uri incorrectly Formatted: %s", err.Error()) + } + if !schemePrefixPattern.MatchString(uri) { + return fmt.Errorf("uri missing scheme prefix") + } + return nil +} diff --git a/pkg/3rdparty/jsonschema/keywords_standard.go b/pkg/3rdparty/jsonschema/keywords_standard.go new file mode 100644 index 00000000..5fade9ea --- /dev/null +++ b/pkg/3rdparty/jsonschema/keywords_standard.go @@ -0,0 +1,287 @@ +package jsonschema + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" + + jptr "github.com/qri-io/jsonpointer" +) + +// Const defines the const JSON Schema keyword +type Const json.RawMessage + +// NewConst allocates a new Const keyword +func NewConst() Keyword { + return &Const{} +} + +// Register implements the Keyword interface for Const +func (c *Const) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Const +func (c *Const) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for Const +func (c Const) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Const] Validating") + var con interface{} + if err := json.Unmarshal(c, &con); err != nil { + currentState.AddError(data, err.Error()) + return + } + + if !reflect.DeepEqual(con, data) { + currentState.AddError(data, fmt.Sprintf(`must equal %s`, InvalidValueString(con))) + } +} + +// JSONProp implements the JSONPather for Const +func (c Const) JSONProp(name string) interface{} { + return nil +} + +// String implements the Stringer for Const +func (c Const) String() string { + return string(c) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Const +func (c *Const) UnmarshalJSON(data []byte) error { + *c = data + return nil +} + +// MarshalJSON implements the json.Marshaler interface for Const +func (c Const) MarshalJSON() ([]byte, error) { + return json.Marshal(json.RawMessage(c)) +} + +// Enum defines the enum JSON Schema keyword +type Enum []Const + +// NewEnum allocates a new Enum keyword +func NewEnum() Keyword { + return &Enum{} +} + +// Register implements the Keyword interface for Enum +func (e *Enum) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Enum +func (e *Enum) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for Enum +func (e Enum) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Enum] Validating") + subState := currentState.NewSubState() + subState.ClearState() + for _, v := range e { + subState.Errs = &[]KeyError{} + v.ValidateKeyword(ctx, subState, data) + if subState.IsValid() { + return + } + } + + currentState.AddError(data, fmt.Sprintf("should be one of %s", e.String())) +} + +// JSONProp implements the JSONPather for Enum +func (e Enum) JSONProp(name string) interface{} { + idx, err := strconv.Atoi(name) + if err != nil { + return nil + } + if idx > len(e) || idx < 0 { + return nil + } + return e[idx] +} + +// JSONChildren implements the JSONContainer interface for Enum +func (e Enum) JSONChildren() (res map[string]JSONPather) { + res = map[string]JSONPather{} + for i, bs := range e { + res[strconv.Itoa(i)] = bs + } + return +} + +// String implements the Stringer for Enum +func (e Enum) String() string { + str := "[" + for _, c := range e { + str += c.String() + ", " + } + return str[:len(str)-2] + "]" +} + +// List of primitive types supported and used by JSON Schema +var primitiveTypes = map[string]bool{ + "null": true, + "boolean": true, + "object": true, + "array": true, + "number": true, + "string": true, + "integer": true, +} + +// DataType attempts to parse the underlying data type +// from the raw data interface +func DataType(data interface{}) string { + if data == nil { + return "null" + } + + switch reflect.TypeOf(data).Kind() { + case reflect.Bool: + return "boolean" + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uintptr: + return "integer" + case reflect.Float32, reflect.Float64: + number := reflect.ValueOf(data).Float() + if float64(int(number)) == number { + return "integer" + } + return "number" + case reflect.String: + return "string" + case reflect.Array, reflect.Slice: + return "array" + case reflect.Map, reflect.Struct: + return "object" + default: + return "unknown" + } +} + +// DataTypeWithHint attempts to parse the underlying data type +// by leveraging the schema expectations for better results +func DataTypeWithHint(data interface{}, hint string) string { + dt := DataType(data) + if dt == "string" { + if hint == "boolean" { + _, err := strconv.ParseBool(data.(string)) + if err == nil { + return "boolean" + } + } + } + // deals with traling 0 floats + if dt == "integer" && hint == "number" { + return "number" + } + return dt +} + +// Type defines the type JSON Schema keyword +type Type struct { + StrVal bool + Vals []string +} + +// NewType allocates a new Type keyword +func NewType() Keyword { + return &Type{} +} + +// Register implements the Keyword interface for Type +func (t *Type) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Type +func (t *Type) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for Type +func (t Type) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Type] Validating") + jt := DataType(data) + for _, typestr := range t.Vals { + if jt == typestr || jt == "integer" && typestr == "number" { + return + } + if jt == "string" && (typestr == "boolean" || typestr == "number" || typestr == "integer") { + if DataTypeWithHint(data, typestr) == typestr { + return + } + } + if jt == "null" && (typestr == "string") { + if DataTypeWithHint(data, typestr) == typestr { + return + } + } + } + if len(t.Vals) == 1 { + currentState.AddError(data, fmt.Sprintf(`type should be %s, got %s`, t.Vals[0], jt)) + return + } + + str := "" + for _, ts := range t.Vals { + str += ts + "," + } + + currentState.AddError(data, fmt.Sprintf(`type should be one of: %s, got %s`, str[:len(str)-1], jt)) +} + +// String implements the Stringer for Type +func (t Type) String() string { + if len(t.Vals) == 0 { + return "unknown" + } + return strings.Join(t.Vals, ",") +} + +// JSONProp implements the JSONPather for Type +func (t Type) JSONProp(name string) interface{} { + idx, err := strconv.Atoi(name) + if err != nil { + return nil + } + if idx > len(t.Vals) || idx < 0 { + return nil + } + return t.Vals[idx] +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Type +func (t *Type) UnmarshalJSON(data []byte) error { + var single string + if err := json.Unmarshal(data, &single); err == nil { + *t = Type{StrVal: true, Vals: []string{single}} + } else { + var set []string + if err := json.Unmarshal(data, &set); err == nil { + *t = Type{Vals: set} + } else { + return err + } + } + + for _, pr := range t.Vals { + if !primitiveTypes[pr] { + return fmt.Errorf(`"%s" is not a valid type`, pr) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaler interface for Type +func (t Type) MarshalJSON() ([]byte, error) { + if t.StrVal { + return json.Marshal(t.Vals[0]) + } + return json.Marshal(t.Vals) +} diff --git a/pkg/3rdparty/jsonschema/keywords_string.go b/pkg/3rdparty/jsonschema/keywords_string.go new file mode 100644 index 00000000..cd33c750 --- /dev/null +++ b/pkg/3rdparty/jsonschema/keywords_string.go @@ -0,0 +1,113 @@ +package jsonschema + +import ( + "context" + "encoding/json" + "fmt" + "regexp" + "unicode/utf8" + + jptr "github.com/qri-io/jsonpointer" +) + +// MaxLength defines the maxLenght JSON Schema keyword +type MaxLength int + +// NewMaxLength allocates a new MaxLength keyword +func NewMaxLength() Keyword { + return new(MaxLength) +} + +// Register implements the Keyword interface for MaxLength +func (m *MaxLength) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for MaxLength +func (m *MaxLength) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for MaxLength +func (m MaxLength) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[MaxLength] Validating") + if str, ok := data.(string); ok { + if utf8.RuneCountInString(str) > int(m) { + currentState.AddError(data, fmt.Sprintf("max length of %d characters exceeded: %s", m, str)) + } + } +} + +// MinLength defines the maxLenght JSON Schema keyword +type MinLength int + +// NewMinLength allocates a new MinLength keyword +func NewMinLength() Keyword { + return new(MinLength) +} + +// Register implements the Keyword interface for MinLength +func (m *MinLength) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for MinLength +func (m *MinLength) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for MinLength +func (m MinLength) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[MinLength] Validating") + if str, ok := data.(string); ok { + if utf8.RuneCountInString(str) < int(m) { + currentState.AddError(data, fmt.Sprintf("min length of %d characters required: %s", m, str)) + } + } +} + +// Pattern defines the pattern JSON Schema keyword +type Pattern regexp.Regexp + +// NewPattern allocates a new Pattern keyword +func NewPattern() Keyword { + return &Pattern{} +} + +// Register implements the Keyword interface for Pattern +func (p *Pattern) Register(uri string, registry *SchemaRegistry) {} + +// Resolve implements the Keyword interface for Pattern +func (p *Pattern) Resolve(pointer jptr.Pointer, uri string) *Schema { + return nil +} + +// ValidateKeyword implements the Keyword interface for Pattern +func (p Pattern) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Pattern] Validating") + re := regexp.Regexp(p) + if str, ok := data.(string); ok { + if !re.Match([]byte(str)) { + currentState.AddError(data, fmt.Sprintf("regexp pattern %s mismatch on string: %s", re.String(), str)) + } + } +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Pattern +func (p *Pattern) UnmarshalJSON(data []byte) error { + var str string + if err := json.Unmarshal(data, &str); err != nil { + return err + } + + ptn, err := regexp.Compile(str) + if err != nil { + return err + } + + *p = Pattern(*ptn) + return nil +} + +// MarshalJSON implements the json.Marshaler interface for Pattern +func (p Pattern) MarshalJSON() ([]byte, error) { + re := regexp.Regexp(p) + rep := &re + return json.Marshal(rep.String()) +} diff --git a/pkg/3rdparty/jsonschema/schema.go b/pkg/3rdparty/jsonschema/schema.go new file mode 100644 index 00000000..a4bad514 --- /dev/null +++ b/pkg/3rdparty/jsonschema/schema.go @@ -0,0 +1,360 @@ +package jsonschema + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "sort" + "strings" + + jptr "github.com/qri-io/jsonpointer" +) + +// Must turns a JSON string into a *Schema, panicing if parsing fails. +// Useful for declaring Schemas in Go code. +func Must(jsonString string) *Schema { + s := &Schema{} + if err := s.UnmarshalJSON([]byte(jsonString)); err != nil { + panic(err) + } + return s +} + +type schemaType int + +const ( + SchemaTypeObject schemaType = iota + SchemaTypeFalse + SchemaTypeTrue +) + +// Schema is the top-level structure defining a json schema +type Schema struct { + SchemaType schemaType + DocPath string + HasRegistered bool + + Id string + + ExtraDefinitions map[string]json.RawMessage + Keywords map[string]Keyword + OrderedKeywords []string +} + +// NewSchema allocates a new Schema Keyword/Validator +func NewSchema() Keyword { + return &Schema{} +} + +// HasKeyword is a utility function for checking if the given schema +// has an instance of the required keyword +func (s *Schema) HasKeyword(key string) bool { + _, ok := s.Keywords[key] + return ok +} + +// Register implements the Keyword interface for Schema +func (s *Schema) Register(uri string, registry *SchemaRegistry) { + schemaDebug("[Schema] Register") + if s.HasRegistered { + return + } + s.HasRegistered = true + registry.RegisterLocal(s) + + // load default keyset if no other is present + if !IsRegistryLoaded() { + LoadDraft2019_09() + } + + address := s.Id + if uri != "" && address != "" { + address, _ = SafeResolveURL(uri, address) + } + if s.DocPath == "" && address != "" && address[0] != '#' { + docURI := "" + if u, err := url.Parse(address); err != nil { + docURI, _ = SafeResolveURL("https://qri.io", address) + } else { + docURI = u.String() + } + s.DocPath = docURI + GetSchemaRegistry().Register(s) + uri = docURI + } + + for _, keyword := range s.Keywords { + keyword.Register(uri, registry) + } +} + +// Resolve implements the Keyword interface for Schema +func (s *Schema) Resolve(pointer jptr.Pointer, uri string) *Schema { + if pointer.IsEmpty() { + if s.DocPath != "" { + s.DocPath, _ = SafeResolveURL(uri, s.DocPath) + } else { + s.DocPath = uri + } + return s + } + + current := pointer.Head() + + if s.Id != "" { + if u, err := url.Parse(s.Id); err == nil { + if u.IsAbs() { + uri = s.Id + } else { + uri, _ = SafeResolveURL(uri, s.Id) + } + } + } + + keyword := s.Keywords[*current] + var keywordSchema *Schema + if keyword != nil { + keywordSchema = keyword.Resolve(pointer.Tail(), uri) + } + + if keywordSchema != nil { + return keywordSchema + } + + found, err := pointer.Eval(s.ExtraDefinitions) + if err != nil { + return nil + } + if found == nil { + return nil + } + + if foundSchema, ok := found.(*Schema); ok { + return foundSchema + } + + return nil +} + +// JSONProp implements the JSONPather for Schema +func (s Schema) JSONProp(name string) interface{} { + if keyword, ok := s.Keywords[name]; ok { + return keyword + } + return s.ExtraDefinitions[name] +} + +// JSONChildren implements the JSONContainer interface for Schema +func (s Schema) JSONChildren() map[string]JSONPather { + ch := map[string]JSONPather{} + + if s.Keywords != nil { + for key, val := range s.Keywords { + if jp, ok := val.(JSONPather); ok { + ch[key] = jp + } + } + } + + return ch +} + +// _schema is an internal struct for encoding & decoding purposes +type _schema struct { + ID string `json:"$id,omitempty"` +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Schema +func (s *Schema) UnmarshalJSON(data []byte) error { + var b bool + if err := json.Unmarshal(data, &b); err == nil { + if b { + // boolean true Always passes validation, as if the empty schema {} + *s = Schema{SchemaType: SchemaTypeTrue} + return nil + } + // boolean false Always fails validation, as if the schema { "not":{} } + *s = Schema{SchemaType: SchemaTypeFalse} + return nil + } + + if !IsRegistryLoaded() { + LoadDraft2019_09() + } + + _s := _schema{} + if err := json.Unmarshal(data, &_s); err != nil { + return err + } + + sch := &Schema{ + Id: _s.ID, + Keywords: map[string]Keyword{}, + } + + valprops := map[string]json.RawMessage{} + if err := json.Unmarshal(data, &valprops); err != nil { + return err + } + + for prop, rawmsg := range valprops { + var keyword Keyword + if IsRegisteredKeyword(prop) { + keyword = GetKeyword(prop) + } else if IsNotSupportedKeyword(prop) { + schemaDebug(fmt.Sprintf("[Schema] WARN: '%s' is not supported and will be ignored\n", prop)) + continue + } else { + if sch.ExtraDefinitions == nil { + sch.ExtraDefinitions = map[string]json.RawMessage{} + } + sch.ExtraDefinitions[prop] = rawmsg + continue + } + if _, ok := keyword.(*Void); !ok { + if err := json.Unmarshal(rawmsg, keyword); err != nil { + return fmt.Errorf("error unmarshaling %s from json: %s", prop, err.Error()) + } + } + sch.Keywords[prop] = keyword + } + + // ensures proper and stable keyword validation order + keyOrders := make([]_keyOrder, len(sch.Keywords)) + i := 0 + for k := range sch.Keywords { + keyOrders[i] = _keyOrder{ + Key: k, + Order: GetKeywordOrder(k), + } + i++ + } + sort.SliceStable(keyOrders, func(i, j int) bool { + if keyOrders[i].Order == keyOrders[j].Order { + return GetKeywordInsertOrder(keyOrders[i].Key) < GetKeywordInsertOrder(keyOrders[j].Key) + } + return keyOrders[i].Order < keyOrders[j].Order + }) + orderedKeys := make([]string, len(sch.Keywords)) + i = 0 + for _, keyOrder := range keyOrders { + orderedKeys[i] = keyOrder.Key + i++ + } + sch.OrderedKeywords = orderedKeys + + *s = Schema(*sch) + return nil +} + +// _keyOrder is an internal struct assigning evaluation order of Keywords +type _keyOrder struct { + Key string + Order int +} + +// Validate initiates a fresh validation state and triggers the evaluation +func (s *Schema) Validate(ctx context.Context, data interface{}) *ValidationState { + currentState := NewValidationState(s) + s.ValidateKeyword(ctx, currentState, data) + return currentState +} + +// ValidateKeyword uses the schema to check an instance, collecting validation +// errors in a slice +func (s *Schema) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) { + schemaDebug("[Schema] Validating") + if s == nil { + currentState.AddError(data, fmt.Sprintf("schema is nil")) + return + } + if s.SchemaType == SchemaTypeTrue { + return + } + if s.SchemaType == SchemaTypeFalse { + currentState.AddError(data, fmt.Sprintf("schema is always false")) + return + } + + s.Register("", currentState.LocalRegistry) + currentState.LocalRegistry.RegisterLocal(s) + + currentState.Local = s + + refKeyword := s.Keywords["$ref"] + + if refKeyword == nil { + if currentState.BaseURI == "" { + currentState.BaseURI = s.DocPath + } else if s.DocPath != "" { + if u, err := url.Parse(s.DocPath); err == nil { + if u.IsAbs() { + currentState.BaseURI = s.DocPath + } else { + currentState.BaseURI, _ = SafeResolveURL(currentState.BaseURI, s.DocPath) + } + } + } + } + + if currentState.BaseURI != "" && strings.HasSuffix(currentState.BaseURI, "#") { + currentState.BaseURI = strings.TrimRight(currentState.BaseURI, "#") + } + + // TODO(arqu): only on versions bellow draft2019_09 + // if refKeyword != nil { + // refKeyword.ValidateKeyword(currentState, errs) + // return + // } + + s.validateSchemakeywords(ctx, currentState, data) +} + +// validateSchemakeywords triggers validation of sub schemas and Keywords +func (s *Schema) validateSchemakeywords(ctx context.Context, currentState *ValidationState, data interface{}) { + if s.Keywords != nil { + for _, keyword := range s.OrderedKeywords { + s.Keywords[keyword].ValidateKeyword(ctx, currentState, data) + } + } +} + +// ValidateBytes performs schema validation against a slice of json +// byte data +func (s *Schema) ValidateBytes(ctx context.Context, data []byte) ([]KeyError, error) { + var doc interface{} + if err := json.Unmarshal(data, &doc); err != nil { + return nil, fmt.Errorf("error parsing JSON bytes: %w", err) + } + vs := s.Validate(ctx, doc) + return *vs.Errs, nil +} + +// TopLevelType returns a string representing the schema's top-level type. +func (s *Schema) TopLevelType() string { + if t, ok := s.Keywords["type"].(*Type); ok { + return t.String() + } + return "unknown" +} + +// MarshalJSON implements the json.Marshaler interface for Schema +func (s Schema) MarshalJSON() ([]byte, error) { + switch s.SchemaType { + case SchemaTypeFalse: + return []byte("false"), nil + case SchemaTypeTrue: + return []byte("true"), nil + default: + obj := map[string]interface{}{} + + for k, v := range s.Keywords { + obj[k] = v + } + for k, v := range s.ExtraDefinitions { + obj[k] = v + } + return json.Marshal(obj) + } +} diff --git a/pkg/3rdparty/jsonschema/schema_registry.go b/pkg/3rdparty/jsonschema/schema_registry.go new file mode 100644 index 00000000..19c1f31b --- /dev/null +++ b/pkg/3rdparty/jsonschema/schema_registry.go @@ -0,0 +1,90 @@ +package jsonschema + +import ( + "context" + "fmt" + "strings" +) + +var sr *SchemaRegistry + +// SchemaRegistry maintains a lookup table between schema string references +// and actual schemas +type SchemaRegistry struct { + schemaLookup map[string]*Schema + contextLookup map[string]*Schema +} + +// GetSchemaRegistry provides an accessor to a globally available schema registry +func GetSchemaRegistry() *SchemaRegistry { + if sr == nil { + sr = &SchemaRegistry{ + schemaLookup: map[string]*Schema{}, + contextLookup: map[string]*Schema{}, + } + } + return sr +} + +// ResetSchemaRegistry resets the main SchemaRegistry +func ResetSchemaRegistry() { + sr = nil +} + +// Get fetches a schema from the top level context registry or fetches it from a remote +func (sr *SchemaRegistry) Get(ctx context.Context, uri string) *Schema { + uri = strings.TrimRight(uri, "#") + schema := sr.schemaLookup[uri] + if schema == nil { + fetchedSchema := &Schema{} + err := FetchSchema(ctx, uri, fetchedSchema) + if err != nil { + schemaDebug(fmt.Sprintf("[SchemaRegistry] Fetch error: %s", err.Error())) + return nil + } + if fetchedSchema == nil { + return nil + } + fetchedSchema.DocPath = uri + // TODO(arqu): meta validate schema + schema = fetchedSchema + sr.schemaLookup[uri] = schema + } + return schema +} + +// GetKnown fetches a schema from the top level context registry +func (sr *SchemaRegistry) GetKnown(uri string) *Schema { + uri = strings.TrimRight(uri, "#") + return sr.schemaLookup[uri] +} + +// GetLocal fetches a schema from the local context registry +func (sr *SchemaRegistry) GetLocal(uri string) *Schema { + uri = strings.TrimRight(uri, "#") + return sr.contextLookup[uri] +} + +// Register registers a schema to the top level context +func (sr *SchemaRegistry) Register(sch *Schema) { + if sch.DocPath == "" { + return + } + sr.schemaLookup[sch.DocPath] = sch +} + +// RegisterLocal registers a schema to a local context +func (sr *SchemaRegistry) RegisterLocal(sch *Schema) { + if sch.Id != "" && IsLocalSchemaID(sch.Id) { + sr.contextLookup[sch.Id] = sch + } + + if sch.HasKeyword("$anchor") { + anchorKeyword := sch.Keywords["$anchor"].(*Anchor) + anchorURI := sch.DocPath + "#" + string(*anchorKeyword) + if sr.contextLookup == nil { + sr.contextLookup = map[string]*Schema{} + } + sr.contextLookup[anchorURI] = sch + } +} diff --git a/pkg/3rdparty/jsonschema/traversal.go b/pkg/3rdparty/jsonschema/traversal.go new file mode 100644 index 00000000..224d99bb --- /dev/null +++ b/pkg/3rdparty/jsonschema/traversal.go @@ -0,0 +1,35 @@ +package jsonschema + +// JSONPather makes validators traversible by JSON-pointers, +// which is required to support references in JSON schemas. +type JSONPather interface { + // JSONProp take a string references for a given JSON property + // implementations must return any matching property of that name + // or nil if no such subproperty exists. + // Note this also applies to array values, which are expected to interpret + // valid numbers as an array index + JSONProp(name string) interface{} +} + +// JSONContainer is an interface that enables tree traversal by listing +// the immideate children of an object +type JSONContainer interface { + // JSONChildren should return all immidiate children of this element + JSONChildren() map[string]JSONPather +} + +func walkJSON(elem JSONPather, fn func(elem JSONPather) error) error { + if err := fn(elem); err != nil { + return err + } + + if con, ok := elem.(JSONContainer); ok { + for _, ch := range con.JSONChildren() { + if err := walkJSON(ch, fn); err != nil { + return err + } + } + } + + return nil +} diff --git a/pkg/3rdparty/jsonschema/util.go b/pkg/3rdparty/jsonschema/util.go new file mode 100644 index 00000000..27f14df5 --- /dev/null +++ b/pkg/3rdparty/jsonschema/util.go @@ -0,0 +1,95 @@ +package jsonschema + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" +) + +var showDebug = os.Getenv("JSON_SCHEMA_DEBUG") == "1" + +// schemaDebug provides a logging interface +// which is off by defauly but can be activated +// for debuging purposes +func schemaDebug(message string, args ...interface{}) { + if showDebug { + if message[len(message)-1] != '\n' { + message += "\n" + } + fmt.Printf(message, args...) + } +} + +// SafeResolveURL resolves a string url against the current context url +func SafeResolveURL(ctxURL, resURL string) (string, error) { + cu, err := url.Parse(ctxURL) + if err != nil { + return "", err + } + u, err := url.Parse(resURL) + if err != nil { + return "", err + } + resolvedURL := cu.ResolveReference(u) + if resolvedURL.Scheme == "file" && cu.Scheme != "file" { + return "", fmt.Errorf("cannot access file resources from network context") + } + resolvedURLString := resolvedURL.String() + return resolvedURLString, nil +} + +// IsLocalSchemaID validates if a given Id is a local Id +func IsLocalSchemaID(id string) bool { + splitID := strings.Split(id, "#") + if len(splitID) > 1 && len(splitID[0]) > 0 && splitID[0][0] != '#' { + return false + } + return id != "#" && !strings.HasPrefix(id, "#/") && strings.Contains(id, "#") +} + +// FetchSchema downloads and loads a schema from a remote location +func FetchSchema(ctx context.Context, uri string, schema *Schema) error { + schemaDebug(fmt.Sprintf("[FetchSchema] Fetching: %s", uri)) + u, err := url.Parse(uri) + if err != nil { + return err + } + // TODO(arqu): support other schemas like file or ipfs + if u.Scheme == "http" || u.Scheme == "https" { + var req *http.Request + if ctx != nil { + req, _ = http.NewRequestWithContext(ctx, "GET", u.String(), nil) + } else { + req, _ = http.NewRequest("GET", u.String(), nil) + } + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return err + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + if schema == nil { + schema = &Schema{} + } + return json.Unmarshal(body, schema) + } + if u.Scheme == "file" { + body, err := ioutil.ReadFile(u.Path) + if err != nil { + return err + } + if schema == nil { + schema = &Schema{} + } + return json.Unmarshal(body, schema) + } + return fmt.Errorf("URI scheme %s is not supported for uri: %s", u.Scheme, uri) +} diff --git a/pkg/3rdparty/jsonschema/validation_state.go b/pkg/3rdparty/jsonschema/validation_state.go new file mode 100644 index 00000000..8288a6f6 --- /dev/null +++ b/pkg/3rdparty/jsonschema/validation_state.go @@ -0,0 +1,194 @@ +package jsonschema + +import ( + jptr "github.com/qri-io/jsonpointer" +) + +// ValidationState holds the schema validation state +// The aim is to have one global validation state +// and use local sub states when evaluating parallel branches +// TODO(arqu): make sure this is safe for concurrent use +type ValidationState struct { + Local *Schema + Root *Schema + RecursiveAnchor *Schema + BaseURI string + InstanceLocation *jptr.Pointer + RelativeLocation *jptr.Pointer + BaseRelativeLocation *jptr.Pointer + + LocalRegistry *SchemaRegistry + + EvaluatedPropertyNames *map[string]bool + LocalEvaluatedPropertyNames *map[string]bool + LastEvaluatedIndex int + LocalLastEvaluatedIndex int + Misc map[string]interface{} + + Errs *[]KeyError +} + +// NewValidationState creates a new ValidationState with the provided location pointers and data instance +func NewValidationState(s *Schema) *ValidationState { + tmpBRLprt := jptr.NewPointer() + tmpRLprt := jptr.NewPointer() + tmpILprt := jptr.NewPointer() + return &ValidationState{ + Root: s, + BaseRelativeLocation: &tmpBRLprt, + RelativeLocation: &tmpRLprt, + InstanceLocation: &tmpILprt, + LocalRegistry: &SchemaRegistry{}, + LastEvaluatedIndex: -1, + LocalLastEvaluatedIndex: -1, + EvaluatedPropertyNames: &map[string]bool{}, + LocalEvaluatedPropertyNames: &map[string]bool{}, + Misc: map[string]interface{}{}, + Errs: &[]KeyError{}, + } +} + +// NewSubState creates a new ValidationState from an existing ValidationState +func (vs *ValidationState) NewSubState() *ValidationState { + return &ValidationState{ + Local: vs.Local, + Root: vs.Root, + RecursiveAnchor: vs.RecursiveAnchor, + LastEvaluatedIndex: vs.LastEvaluatedIndex, + LocalLastEvaluatedIndex: vs.LocalLastEvaluatedIndex, + BaseURI: vs.BaseURI, + InstanceLocation: vs.InstanceLocation, + RelativeLocation: vs.RelativeLocation, + BaseRelativeLocation: vs.RelativeLocation, + LocalRegistry: vs.LocalRegistry, + EvaluatedPropertyNames: vs.EvaluatedPropertyNames, + LocalEvaluatedPropertyNames: vs.LocalEvaluatedPropertyNames, + Misc: map[string]interface{}{}, + Errs: vs.Errs, + } +} + +// ClearState resets a schema to it's core elements +func (vs *ValidationState) ClearState() { + vs.EvaluatedPropertyNames = &map[string]bool{} + vs.LocalEvaluatedPropertyNames = &map[string]bool{} + if len(vs.Misc) > 0 { + vs.Misc = map[string]interface{}{} + } +} + +// SetEvaluatedKey updates the evaluation properties of the current state +func (vs *ValidationState) SetEvaluatedKey(key string) { + (*vs.EvaluatedPropertyNames)[key] = true + (*vs.LocalEvaluatedPropertyNames)[key] = true +} + +// IsEvaluatedKey checks if the key is evaluated against the state context +func (vs *ValidationState) IsEvaluatedKey(key string) bool { + _, ok := (*vs.EvaluatedPropertyNames)[key] + return ok +} + +// IsLocallyEvaluatedKey checks if the key is evaluated against the local state context +func (vs *ValidationState) IsLocallyEvaluatedKey(key string) bool { + _, ok := (*vs.LocalEvaluatedPropertyNames)[key] + return ok +} + +// SetEvaluatedIndex sets the evaluation index for the current state +func (vs *ValidationState) SetEvaluatedIndex(i int) { + vs.LastEvaluatedIndex = i + vs.LocalLastEvaluatedIndex = i +} + +// UpdateEvaluatedPropsAndItems is a utility function to join evaluated properties and set the +// current evaluation position index +func (vs *ValidationState) UpdateEvaluatedPropsAndItems(subState *ValidationState) { + joinSets(vs.EvaluatedPropertyNames, *subState.EvaluatedPropertyNames) + joinSets(vs.LocalEvaluatedPropertyNames, *subState.LocalEvaluatedPropertyNames) + if subState.LastEvaluatedIndex > vs.LastEvaluatedIndex { + vs.LastEvaluatedIndex = subState.LastEvaluatedIndex + } + if subState.LocalLastEvaluatedIndex > vs.LastEvaluatedIndex { + vs.LastEvaluatedIndex = subState.LocalLastEvaluatedIndex + } +} + +func copySet(input map[string]bool) map[string]bool { + copy := make(map[string]bool, len(input)) + for k, v := range input { + copy[k] = v + } + return copy +} + +func joinSets(consumer *map[string]bool, supplier map[string]bool) { + for k, v := range supplier { + (*consumer)[k] = v + } +} + +// AddError creates and appends a KeyError to errs of the current state +func (vs *ValidationState) AddError(data interface{}, msg string) { + schemaDebug("[AddError] Error: %s", msg) + instancePath := vs.InstanceLocation.String() + if len(instancePath) == 0 { + instancePath = "/" + } + *vs.Errs = append(*vs.Errs, KeyError{ + PropertyPath: instancePath, + InvalidValue: data, + Message: msg, + }) +} + +// AddSubErrors appends a list of KeyError to the current state +func (vs *ValidationState) AddSubErrors(errs ...KeyError) { + for _, err := range errs { + schemaDebug("[AddSubErrors] Error: %s", err.Message) + } + *vs.Errs = append(*vs.Errs, errs...) +} + +// IsValid returns if the current state is valid +func (vs *ValidationState) IsValid() bool { + if vs.Errs == nil { + return true + } + return len(*vs.Errs) == 0 +} + +// DescendBase descends the base relative pointer relative to itself +func (vs *ValidationState) DescendBase(token ...string) { + vs.DescendBaseFromState(vs, token...) +} + +// DescendBaseFromState descends the base relative pointer relative to the provided state +func (vs *ValidationState) DescendBaseFromState(base *ValidationState, token ...string) { + if base.BaseRelativeLocation != nil { + newPtr := base.BaseRelativeLocation.RawDescendant(token...) + vs.BaseRelativeLocation = &newPtr + } +} + +// DescendRelative descends the relative pointer relative to itself +func (vs *ValidationState) DescendRelative(token ...string) { + vs.DescendRelativeFromState(vs, token...) +} + +// DescendRelativeFromState descends the relative pointer relative to the provided state +func (vs *ValidationState) DescendRelativeFromState(base *ValidationState, token ...string) { + newPtr := base.InstanceLocation.RawDescendant(token...) + vs.RelativeLocation = &newPtr +} + +// DescendInstance descends the instance pointer relative to itself +func (vs *ValidationState) DescendInstance(token ...string) { + vs.DescendInstanceFromState(vs, token...) +} + +// DescendInstanceFromState descends the instance pointer relative to the provided state +func (vs *ValidationState) DescendInstanceFromState(base *ValidationState, token ...string) { + newPtr := base.InstanceLocation.RawDescendant(token...) + vs.InstanceLocation = &newPtr +} diff --git a/pkg/tools/gen/genkcl.go b/pkg/tools/gen/genkcl.go index 7b44647f..3ed28d84 100644 --- a/pkg/tools/gen/genkcl.go +++ b/pkg/tools/gen/genkcl.go @@ -12,14 +12,24 @@ import ( ) type GenKclOptions struct { + Mode Mode ParseFromTag bool } +// Mode is the mode of kcl schema code generation. +type Mode int + +const ( + ModeAuto Mode = iota + ModeGoStruct + ModeJsonSchema +) + type kclGenerator struct { opts *GenKclOptions } -// GenKcl translate go struct to kcl schema code. +// GenKcl translate other formats to kcl schema code. Now support go struct and json schema. func GenKcl(w io.Writer, filename string, src interface{}, opts *GenKclOptions) error { return newKclGenerator(opts).GenSchema(w, filename, src) } @@ -34,6 +44,40 @@ func newKclGenerator(opts *GenKclOptions) *kclGenerator { } func (k *kclGenerator) GenSchema(w io.Writer, filename string, src interface{}) error { + if k.opts.Mode == ModeAuto { + switch { + case strings.HasSuffix(filename, ".go"): + k.opts.Mode = ModeGoStruct + case strings.HasSuffix(filename, ".json"): + k.opts.Mode = ModeJsonSchema + default: + code, err := readSource(filename, src) + if err != nil { + return err + } + codeStr := string(code) + switch { + case strings.Contains(codeStr, "package "): + k.opts.Mode = ModeGoStruct + case strings.Contains(codeStr, "$schema"): + k.opts.Mode = ModeJsonSchema + default: + return errors.New("failed to detect mode") + } + } + } + + switch k.opts.Mode { + case ModeGoStruct: + return k.genSchemaFromGoStruct(w, filename, src) + case ModeJsonSchema: + return k.genSchemaFromJsonSchema(w, filename, src) + default: + return errors.New("unknown mode") + } +} + +func (k *kclGenerator) genSchemaFromGoStruct(w io.Writer, filename string, src interface{}) error { fmt.Fprintln(w) goStructs, err := ParseGoSourceCode(filename, src) if err != nil { diff --git a/pkg/tools/gen/genkcl_jsonschema.go b/pkg/tools/gen/genkcl_jsonschema.go new file mode 100644 index 00000000..ed7814d2 --- /dev/null +++ b/pkg/tools/gen/genkcl_jsonschema.go @@ -0,0 +1,194 @@ +package gen + +import ( + "context" + "encoding/json" + "io" + "path/filepath" + "strings" + + "github.com/iancoleman/strcase" + "kcl-lang.io/kcl-go/pkg/3rdparty/jsonschema" + "kcl-lang.io/kcl-go/pkg/logger" +) + +type convertContext struct { + imports map[string]struct{} + resultMap map[string]convertResult +} + +type convertResult struct { + IsSchema bool + Name string + Description string + schema + property +} + +func (k *kclGenerator) genSchemaFromJsonSchema(w io.Writer, filename string, src interface{}) error { + code, err := readSource(filename, src) + if err != nil { + return err + } + js := &jsonschema.Schema{} + if err = js.UnmarshalJSON(code); err != nil { + return err + } + // use Validate to trigger the evaluation of json schema + js.Validate(context.Background(), nil) + + // convert json schema to kcl schema + ctx := convertContext{resultMap: make(map[string]convertResult)} + result := convertSchemaFromJsonSchema(ctx, js, + strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))) + if !result.IsSchema { + panic("result is not schema") + } + kclSch := kclSchema{ + Imports: []string{}, + Schemas: []schema{result.schema}, + } + for imp := range ctx.imports { + kclSch.Imports = append(kclSch.Imports, imp) + } + for _, res := range ctx.resultMap { + if res.IsSchema { + kclSch.Schemas = append(kclSch.Schemas, res.schema) + } + } + + // generate kcl schema code + return k.genKclSchema(w, kclSch) +} + +func convertSchemaFromJsonSchema(ctx convertContext, s *jsonschema.Schema, name string) convertResult { + // in jsonschema, type is one of True, False and Object + // we only convert Object type + if s.SchemaType != jsonschema.SchemaTypeObject { + return convertResult{IsSchema: false} + } + + result := convertResult{IsSchema: false, Name: name} + if result.Name == "" { + result.Name = "MyType" + } + + isArray := false + typeList := typeUnion{} + required := make(map[string]struct{}) + for _, k := range s.OrderedKeywords { + switch v := s.Keywords[k].(type) { + case *jsonschema.SchemaURI: + case *jsonschema.ID: + // if the schema has ID, use it as the name + lastSlashIndex := strings.LastIndex(string(*v), "/") + if lastSlashIndex != -1 { + result.Name = strings.Trim(string(*v)[lastSlashIndex+1:], ".json") + } + case *jsonschema.Title: + result.Description += string(*v) + case *jsonschema.Description: + result.Description += string(*v) + case *jsonschema.Comment: + result.Description += string(*v) + case *jsonschema.Type: + if len(v.Vals) == 1 { + switch v.Vals[0] { + case "object": + result.IsSchema = true + continue + case "array": + isArray = true + continue + } + } + typeList.Items = append(typeList.Items, jsonTypesToKclTypes(v.Vals)) + case *jsonschema.Items: + if !v.Single { + logger.GetLogger().Warningf("unsupported multiple items: %#v", v) + break + } + for _, i := range v.Schemas { + item := convertSchemaFromJsonSchema(ctx, i, "items") + if item.IsSchema { + typeList.Items = append(typeList.Items, typeCustom{Name: item.Name}) + } else { + typeList.Items = append(typeList.Items, item.Type) + } + } + case *jsonschema.Required: + for _, key := range []string(*v) { + required[key] = struct{}{} + } + case *jsonschema.Properties: + for _, prop := range *v { + key := prop.Key + val := prop.Value + propSch := convertSchemaFromJsonSchema(ctx, val, key) + _, propSch.Required = required[key] + if propSch.IsSchema { + propSch.Name = strcase.ToCamel(key) + ctx.resultMap[propSch.Name] = propSch + } + propSch.Name = strcase.ToSnake(key) + result.Properties = append(result.Properties, propSch.property) + } + case *jsonschema.Default: + result.HasDefault = true + result.DefaultValue = v.Data + case *jsonschema.Enum: + for _, val := range *v { + unmarshalledVal := interface{}(nil) + err := json.Unmarshal(val, &unmarshalledVal) + if err != nil { + logger.GetLogger().Warningf("failed to unmarshal enum value: %s", err) + continue + } + typeList.Items = append(typeList.Items, typeValue{ + Value: unmarshalledVal, + }) + } + default: + logger.GetLogger().Warningf("unknown Keyword: %s", k) + } + } + + if result.IsSchema { + result.Type = typeCustom{Name: strcase.ToCamel(name)} + } else { + if isArray { + result.Type = typeArray{Items: typeList} + } else { + result.Type = typeList + } + } + result.schema.Name = strcase.ToCamel(result.Name) + result.schema.Description = result.Description + result.property.Name = strcase.ToSnake(result.Name) + result.property.Description = result.Description + return result +} + +func jsonTypesToKclTypes(t []string) typeInterface { + var kclTypes typeUnion + for _, v := range t { + kclTypes.Items = append(kclTypes.Items, jsonTypeToKclType(v)) + } + return kclTypes +} + +func jsonTypeToKclType(t string) typeInterface { + switch t { + case "string": + return typePrimitive(typStr) + case "boolean": + return typePrimitive(typBool) + case "integer": + return typePrimitive(typInt) + case "number": + return typePrimitive(typFloat) + default: + logger.GetLogger().Warningf("unknown type: %s", t) + return typePrimitive(typStr) + } +} diff --git a/pkg/tools/gen/genkcl_test.go b/pkg/tools/gen/genkcl_test.go index 5ad4c282..d6d1a3ed 100644 --- a/pkg/tools/gen/genkcl_test.go +++ b/pkg/tools/gen/genkcl_test.go @@ -3,7 +3,10 @@ package gen import ( "bytes" "fmt" + assert2 "github.com/stretchr/testify/assert" "log" + "os" + "path/filepath" "testing" ) @@ -111,3 +114,48 @@ schema Company: } } + +func TestGenKclFromJson(t *testing.T) { + type testCase struct { + name string + input string + expect string + } + var cases []testCase + + casesPath := filepath.Join("testdata", "jsonschema") + caseFiles, err := os.ReadDir(casesPath) + if err != nil { + t.Fatal(err) + } + + for _, caseFile := range caseFiles { + input := filepath.Join(casesPath, caseFile.Name(), "input.json") + expectFilepath := filepath.Join(casesPath, caseFile.Name(), "expect.k") + cases = append(cases, testCase{ + name: caseFile.Name(), + input: input, + expect: readFileString(t, expectFilepath), + }) + } + + for _, testcase := range cases { + t.Run(testcase.name, func(t *testing.T) { + var buf bytes.Buffer + err := GenKcl(&buf, testcase.input, nil, &GenKclOptions{}) + if err != nil { + t.Fatal(err) + } + assert2.Equal(t, testcase.expect, buf.String()) + }) + } +} + +func readFileString(t testing.TB, p string) (content string) { + data, err := os.ReadFile(p) + if err != nil { + t.Errorf("read file failed, %s", err) + } + data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n")) + return string(data) +} diff --git a/pkg/tools/gen/template.go b/pkg/tools/gen/template.go new file mode 100644 index 00000000..1dfffc67 --- /dev/null +++ b/pkg/tools/gen/template.go @@ -0,0 +1,58 @@ +package gen + +import ( + _ "embed" + "fmt" + "io" + "text/template" +) + +var ( + //go:embed templates/header.gotmpl + headerTmpl string + //go:embed templates/schema.gotmpl + schemaTmpl string +) + +var funcs = template.FuncMap{ + "formatType": formatType, + "formatValue": formatValue, + "formatName": formatName, +} + +func (k *kclGenerator) genKclSchema(w io.Writer, s kclSchema) error { + tmpl := &template.Template{} + tmpl = addTemplate(tmpl, "header", headerTmpl) + tmpl = addTemplate(tmpl, "schema", schemaTmpl) + return tmpl.Funcs(funcs).Execute(w, s) +} + +func addTemplate(tmpl *template.Template, name, data string) *template.Template { + newTmpl := template.Must(template.New(name).Funcs(funcs).Parse(data)) + return template.Must(tmpl.AddParseTree(name, newTmpl.Tree)) +} + +func formatType(t typeInterface) string { + return t.Format() +} + +func formatValue(v interface{}) string { + if v == nil { + return "None" + } + switch value := v.(type) { + case string: + return fmt.Sprintf("\"%s\"", value) + case bool: + if value { + return "True" + } + return "False" + default: + return fmt.Sprintf("%v", value) + } +} + +func formatName(name string) string { + return name +} diff --git a/pkg/tools/gen/templates/header.gotmpl b/pkg/tools/gen/templates/header.gotmpl new file mode 100644 index 00000000..ed9334b9 --- /dev/null +++ b/pkg/tools/gen/templates/header.gotmpl @@ -0,0 +1,10 @@ +""" +This file was generated by the KCL auto-gen tool. DO NOT EDIT. +Editing this file might prove futile when you re-run the KCL auto-gen generate command. +""" + +{{- if .Imports }} +{{- range .Imports }} +import {{ . }} +{{- end }} +{{- end }} diff --git a/pkg/tools/gen/templates/schema.gotmpl b/pkg/tools/gen/templates/schema.gotmpl new file mode 100644 index 00000000..1fcb6fcd --- /dev/null +++ b/pkg/tools/gen/templates/schema.gotmpl @@ -0,0 +1,8 @@ +{{ template "header" . }} +{{ range .Schemas -}} +schema {{ formatName .Name }}: + {{- range .Properties }} + {{ formatName .Name }}{{ if not .Required }}?{{ end }}: {{ formatType .Type }}{{ if .HasDefault }} = {{ formatValue .DefaultValue }}{{ end }} + {{- end }} + +{{ end -}} diff --git a/pkg/tools/gen/testdata/jsonschema/basic/expect.k b/pkg/tools/gen/testdata/jsonschema/basic/expect.k new file mode 100644 index 00000000..508ec768 --- /dev/null +++ b/pkg/tools/gen/testdata/jsonschema/basic/expect.k @@ -0,0 +1,12 @@ +""" +This file was generated by the KCL auto-gen tool. DO NOT EDIT. +Editing this file might prove futile when you re-run the KCL auto-gen generate command. +""" + +schema Book: + title: str + authors?: [str] + price?: float + available?: bool = True + category?: "Fiction" | "Science" | "History" + diff --git a/pkg/tools/gen/testdata/jsonschema/basic/input.json b/pkg/tools/gen/testdata/jsonschema/basic/input.json new file mode 100644 index 00000000..7b441116 --- /dev/null +++ b/pkg/tools/gen/testdata/jsonschema/basic/input.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/schemas/book.json", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "authors": { + "type": "array", + "items": { + "type": "string" + } + }, + "price": { + "type": "number" + }, + "available": { + "type": "boolean", + "default": true + }, + "category": { + "enum": [ + "Fiction", + "Science", + "History" + ] + } + }, + "required": [ + "title" + ] +} diff --git a/pkg/tools/gen/testdata/jsonschema/nested/expect.k b/pkg/tools/gen/testdata/jsonschema/nested/expect.k new file mode 100644 index 00000000..ecb42a0d --- /dev/null +++ b/pkg/tools/gen/testdata/jsonschema/nested/expect.k @@ -0,0 +1,13 @@ +""" +This file was generated by the KCL auto-gen tool. DO NOT EDIT. +Editing this file might prove futile when you re-run the KCL auto-gen generate command. +""" + +schema Book: + title?: str + author?: Author + +schema Author: + name?: str + address?: str + diff --git a/pkg/tools/gen/testdata/jsonschema/nested/input.json b/pkg/tools/gen/testdata/jsonschema/nested/input.json new file mode 100644 index 00000000..c061d4c8 --- /dev/null +++ b/pkg/tools/gen/testdata/jsonschema/nested/input.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/schemas/book.json", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "address": { + "type": "string" + } + } + } + } +} diff --git a/pkg/tools/gen/types.go b/pkg/tools/gen/types.go index fafca118..15076dcf 100644 --- a/pkg/tools/gen/types.go +++ b/pkg/tools/gen/types.go @@ -117,3 +117,82 @@ func getSortedFieldNames(fields map[string]*pb.KclType) []string { } return ss } + +// kclSchema is the top-level structure for a kcl schema file. +// It contains all the imports and schemas in this file. +type kclSchema struct { + Imports []string + Schemas []schema +} + +// schema is a kcl schema definition. +type schema struct { + Name string + Description string + Properties []property +} + +// property is a kcl schema property definition. +type property struct { + Name string + Description string + Type typeInterface + Required bool + HasDefault bool + DefaultValue interface{} +} + +type typeInterface interface { + Format() string +} + +type typePrimitive string + +func (t typePrimitive) Format() string { + return string(t) +} + +type typeArray struct { + Items typeInterface +} + +func (t typeArray) Format() string { + return "[" + t.Items.Format() + "]" +} + +type typeUnion struct { + Items []typeInterface +} + +func (t typeUnion) Format() string { + var items []string + for _, v := range t.Items { + items = append(items, v.Format()) + } + return strings.Join(items, " | ") +} + +type typeDict struct { + Key typeInterface + Value typeInterface +} + +func (t typeDict) Format() string { + return "{" + t.Key.Format() + ":" + t.Value.Format() + "}" +} + +type typeCustom struct { + Name string +} + +func (t typeCustom) Format() string { + return t.Name +} + +type typeValue struct { + Value interface{} +} + +func (t typeValue) Format() string { + return formatValue(t.Value) +}