diff --git a/cli/compose/schema/schema.go b/cli/compose/schema/schema.go index 45b719d79d..de26ffac3d 100644 --- a/cli/compose/schema/schema.go +++ b/cli/compose/schema/schema.go @@ -18,15 +18,19 @@ const ( type portsFormatChecker struct{} -func (checker portsFormatChecker) IsFormat(input string) bool { +func (checker portsFormatChecker) IsFormat(input interface{}) bool { // TODO: implement this return true } type durationFormatChecker struct{} -func (checker durationFormatChecker) IsFormat(input string) bool { - _, err := time.ParseDuration(input) +func (checker durationFormatChecker) IsFormat(input interface{}) bool { + value, ok := input.(string) + if !ok { + return false + } + _, err := time.ParseDuration(value) return err == nil } diff --git a/vendor.conf b/vendor.conf index 04ecac9fcd..bffeb0b355 100755 --- a/vendor.conf +++ b/vendor.conf @@ -78,7 +78,7 @@ github.com/tonistiigi/fsutil 7f9f9232dd24c4c9c68ab3c8030c github.com/tonistiigi/units 6950e57a87eaf136bbe44ef2ec8e75b9e3569de2 github.com/xeipuuv/gojsonpointer 4e3ac2762d5f479393488629ee9370b50873b3a6 github.com/xeipuuv/gojsonreference bd5ef7bd5415a7ac448318e64f11a24cd21e594b -github.com/xeipuuv/gojsonschema 93e72a773fade158921402d6a24c819b48aba29d +github.com/xeipuuv/gojsonschema f971f3cd73b2899de6923801c147f075263e0c50 # v1.1.0 golang.org/x/crypto 88737f569e3a9c7ab309cdc09a07fe7fc87233c3 golang.org/x/net f3200d17e092c607f615320ecaad13d87ad9a2b3 golang.org/x/oauth2 ef147856a6ddbb60760db74283d2424e98c87bff diff --git a/vendor/github.com/xeipuuv/gojsonschema/README.md b/vendor/github.com/xeipuuv/gojsonschema/README.md index 187da61e1b..24ca347443 100644 --- a/vendor/github.com/xeipuuv/gojsonschema/README.md +++ b/vendor/github.com/xeipuuv/gojsonschema/README.md @@ -1,10 +1,11 @@ +[![GoDoc](https://godoc.org/github.com/xeipuuv/gojsonschema?status.svg)](https://godoc.org/github.com/xeipuuv/gojsonschema) [![Build Status](https://travis-ci.org/xeipuuv/gojsonschema.svg)](https://travis-ci.org/xeipuuv/gojsonschema) # gojsonschema ## Description -An implementation of JSON Schema, based on IETF's draft v4 - Go language +An implementation of JSON Schema for the Go programming language. Supports draft-04, draft-06 and draft-07. References : @@ -54,7 +55,6 @@ func main() { fmt.Printf("- %s\n", desc) } } - } @@ -148,6 +148,87 @@ To check the result : } ``` + +## Loading local schemas + +By default `file` and `http(s)` references to external schemas are loaded automatically via the file system or via http(s). An external schema can also be loaded using a `SchemaLoader`. + +```go + sl := gojsonschema.NewSchemaLoader() + loader1 := gojsonschema.NewStringLoader(`{ "type" : "string" }`) + err := sl.AddSchema("http://some_host.com/string.json", loader1) +``` + +Alternatively if your schema already has an `$id` you can use the `AddSchemas` function +```go + loader2 := gojsonschema.NewStringLoader(`{ + "$id" : "http://some_host.com/maxlength.json", + "maxLength" : 5 + }`) + err = sl.AddSchemas(loader2) +``` + +The main schema should be passed to the `Compile` function. This main schema can then directly reference the added schemas without needing to download them. +```go + loader3 := gojsonschema.NewStringLoader(`{ + "$id" : "http://some_host.com/main.json", + "allOf" : [ + { "$ref" : "http://some_host.com/string.json" }, + { "$ref" : "http://some_host.com/maxlength.json" } + ] + }`) + + schema, err := sl.Compile(loader3) + + documentLoader := gojsonschema.NewStringLoader(`"hello world"`) + + result, err := schema.Validate(documentLoader) +``` + +It's also possible to pass a `ReferenceLoader` to the `Compile` function that references a loaded schema. + +```go +err = sl.AddSchemas(loader3) +schema, err := sl.Compile(gojsonschema.NewReferenceLoader("http://some_host.com/main.json")) +``` + +Schemas added by `AddSchema` and `AddSchemas` are only validated when the entire schema is compiled, unless meta-schema validation is used. + +## Using a specific draft +By default `gojsonschema` will try to detect the draft of a schema by using the `$schema` keyword and parse it in a strict draft-04, draft-06 or draft-07 mode. If `$schema` is missing, or the draft version is not explicitely set, a hybrid mode is used which merges together functionality of all drafts into one mode. + +Autodectection can be turned off with the `AutoDetect` property. Specific draft versions can be specified with the `Draft` property. + +```go +sl := gojsonschema.NewSchemaLoader() +sl.Draft = gojsonschema.Draft7 +sl.AutoDetect = false +``` + +If autodetection is on (default), a draft-07 schema can savely reference draft-04 schemas and vice-versa, as long as `$schema` is specified in all schemas. + +## Meta-schema validation +Schemas that are added using the `AddSchema`, `AddSchemas` and `Compile` can be validated against their meta-schema by setting the `Validate` property. + +The following example will produce an error as `multipleOf` must be a number. If `Validate` is off (default), this error is only returned at the `Compile` step. + +```go +sl := gojsonschema.NewSchemaLoader() +sl.Validate = true +err := sl.AddSchemas(gojsonschema.NewStringLoader(`{ + $id" : "http://some_host.com/invalid.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "multipleOf" : true +}`)) + ``` +``` + ``` + +Errors returned by meta-schema validation are more readable and contain more information, which helps significantly if you are developing a schema. + +Meta-schema validation also works with a custom `$schema`. In case `$schema` is missing, or `AutoDetect` is set to `false`, the meta-schema of the used draft is used. + + ## Working with Errors The library handles string error codes which you can customize by creating your own gojsonschema.locale and setting it @@ -155,7 +236,9 @@ The library handles string error codes which you can customize by creating your gojsonschema.Locale = YourCustomLocale{} ``` -However, each error contains additional contextual information. +However, each error contains additional contextual information. + +Newer versions of `gojsonschema` may have new additional errors, so code that uses a custom locale will need to be updated when this happens. **err.Type()**: *string* Returns the "type" of error that occurred. Note you can also type check. See below @@ -169,15 +252,18 @@ Note: An error of RequiredType has an err.Type() return value of "required" "number_not": NumberNotError "missing_dependency": MissingDependencyError "internal": InternalError + "const": ConstEror "enum": EnumError "array_no_additional_items": ArrayNoAdditionalItemsError "array_min_items": ArrayMinItemsError "array_max_items": ArrayMaxItemsError "unique": ItemsMustBeUniqueError + "contains" : ArrayContainsError "array_min_properties": ArrayMinPropertiesError "array_max_properties": ArrayMaxPropertiesError "additional_property_not_allowed": AdditionalPropertyNotAllowedError "invalid_property_pattern": InvalidPropertyPatternError + "invalid_property_name": InvalidPropertyNameError "string_gte": StringLengthGTEError "string_lte": StringLengthLTEError "pattern": DoesNotMatchPatternError @@ -186,28 +272,78 @@ Note: An error of RequiredType has an err.Type() return value of "required" "number_gt": NumberGTError "number_lte": NumberLTEError "number_lt": NumberLTError + "condition_then" : ConditionThenError + "condition_else" : ConditionElseError **err.Value()**: *interface{}* Returns the value given -**err.Context()**: *gojsonschema.jsonContext* Returns the context. This has a String() method that will print something like this: (root).firstName +**err.Context()**: *gojsonschema.JsonContext* Returns the context. This has a String() method that will print something like this: (root).firstName **err.Field()**: *string* Returns the fieldname in the format firstName, or for embedded properties, person.firstName. This returns the same as the String() method on *err.Context()* but removes the (root). prefix. **err.Description()**: *string* The error description. This is based on the locale you are using. See the beginning of this section for overwriting the locale with a custom implementation. +**err.DescriptionFormat()**: *string* The error description format. This is relevant if you are adding custom validation errors afterwards to the result. + **err.Details()**: *gojsonschema.ErrorDetails* Returns a map[string]interface{} of additional error details specific to the error. For example, GTE errors will have a "min" value, LTE will have a "max" value. See errors.go for a full description of all the error details. Every error always contains a "field" key that holds the value of *err.Field()* -Note in most cases, the err.Details() will be used to generate replacement strings in your locales. and not used directly i.e. +Note in most cases, the err.Details() will be used to generate replacement strings in your locales, and not used directly. These strings follow the text/template format i.e. ``` -%field% must be greater than or equal to %min% +{{.field}} must be greater than or equal to {{.min}} ``` +The library allows you to specify custom template functions, should you require more complex error message handling. +```go +gojsonschema.ErrorTemplateFuncs = map[string]interface{}{ + "allcaps": func(s string) string { + return strings.ToUpper(s) + }, +} +``` + +Given the above definition, you can use the custom function `"allcaps"` in your localization templates: +``` +{{allcaps .field}} must be greater than or equal to {{.min}} +``` + +The above error message would then be rendered with the `field` value in capital letters. For example: +``` +"PASSWORD must be greater than or equal to 8" +``` + +Learn more about what types of template functions you can use in `ErrorTemplateFuncs` by referring to Go's [text/template FuncMap](https://golang.org/pkg/text/template/#FuncMap) type. + ## Formats -JSON Schema allows for optional "format" property to validate strings against well-known formats. gojsonschema ships with all of the formats defined in the spec that you can use like this: +JSON Schema allows for optional "format" property to validate instances against well-known formats. gojsonschema ships with all of the formats defined in the spec that you can use like this: + ````json {"type": "string", "format": "email"} ```` -Available formats: date-time, hostname, email, ipv4, ipv6, uri. + +Not all formats defined in draft-07 are available. Implemented formats are: + +* `date` +* `time` +* `date-time` +* `hostname`. Subdomains that start with a number are also supported, but this means that it doesn't strictly follow [RFC1034](http://tools.ietf.org/html/rfc1034#section-3.5) and has the implication that ipv4 addresses are also recognized as valid hostnames. +* `email`. Go's email parser deviates slightly from [RFC5322](https://tools.ietf.org/html/rfc5322). Includes unicode support. +* `idn-email`. Same caveat as `email`. +* `ipv4` +* `ipv6` +* `uri`. Includes unicode support. +* `uri-reference`. Includes unicode support. +* `iri` +* `iri-reference` +* `uri-template` +* `uuid` +* `regex`. Go uses the [RE2](https://github.com/google/re2/wiki/Syntax) engine and is not [ECMA262](http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf) compatible. +* `json-pointer` +* `relative-json-pointer` + +`email`, `uri` and `uri-reference` use the same validation code as their unicode counterparts `idn-email`, `iri` and `iri-reference`. If you rely on unicode support you should use the specific +unicode enabled formats for the sake of interoperability as other implementations might not support unicode in the regular formats. + +The validation code for `uri`, `idn-email` and their relatives use mostly standard library code. Go 1.5 and 1.6 contain some minor bugs with handling URIs and unicode. You are encouraged to use Go 1.7+ if you rely on these formats. For repetitive or more complex formats, you can create custom format checkers and add them to gojsonschema like this: @@ -216,8 +352,14 @@ For repetitive or more complex formats, you can create custom format checkers an type RoleFormatChecker struct {} // Ensure it meets the gojsonschema.FormatChecker interface -func (f RoleFormatChecker) IsFormat(input string) bool { - return strings.HasPrefix("ROLE_", input) +func (f RoleFormatChecker) IsFormat(input interface{}) bool { + + asString, ok := input.(string) + if ok == false { + return false + } + + return strings.HasPrefix("ROLE_", asString) } // Add it to the library @@ -229,6 +371,93 @@ Now to use in your json schema: {"type": "string", "format": "role"} ```` +Another example would be to check if the provided integer matches an id on database: + +JSON schema: +```json +{"type": "integer", "format": "ValidUserId"} +``` + +```go +// Define the format checker +type ValidUserIdFormatChecker struct {} + +// Ensure it meets the gojsonschema.FormatChecker interface +func (f ValidUserIdFormatChecker) IsFormat(input interface{}) bool { + + asFloat64, ok := input.(float64) // Numbers are always float64 here + if ok == false { + return false + } + + // XXX + // do the magic on the database looking for the int(asFloat64) + + return true +} + +// Add it to the library +gojsonschema.FormatCheckers.Add("ValidUserId", ValidUserIdFormatChecker{}) +```` + +Formats can also be removed, for example if you want to override one of the formats that is defined by default. + +```go +gojsonschema.FormatCheckers.Remove("hostname") +``` + + +## Additional custom validation +After the validation has run and you have the results, you may add additional +errors using `Result.AddError`. This is useful to maintain the same format within the resultset instead +of having to add special exceptions for your own errors. Below is an example. + +```go +type AnswerInvalidError struct { + gojsonschema.ResultErrorFields +} + +func newAnswerInvalidError(context *gojsonschema.JsonContext, value interface{}, details gojsonschema.ErrorDetails) *AnswerInvalidError { + err := AnswerInvalidError{} + err.SetContext(context) + err.SetType("custom_invalid_error") + // it is important to use SetDescriptionFormat() as this is used to call SetDescription() after it has been parsed + // using the description of err will be overridden by this. + err.SetDescriptionFormat("Answer to the Ultimate Question of Life, the Universe, and Everything is {{.answer}}") + err.SetValue(value) + err.SetDetails(details) + + return &err +} + +func main() { + // ... + schema, err := gojsonschema.NewSchema(schemaLoader) + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + + if true { // some validation + jsonContext := gojsonschema.NewJsonContext("question", nil) + errDetail := gojsonschema.ErrorDetails{ + "answer": 42, + } + result.AddError( + newAnswerInvalidError( + gojsonschema.NewJsonContext("answer", jsonContext), + 52, + errDetail, + ), + errDetail, + ) + } + + return result, err + +} +``` + +This is especially useful if you want to add validation beyond what the +json schema drafts can provide such business specific logic. + ## Uses gojsonschema uses the following test suite : diff --git a/vendor/github.com/xeipuuv/gojsonschema/draft.go b/vendor/github.com/xeipuuv/gojsonschema/draft.go new file mode 100644 index 0000000000..bfde4a2e1e --- /dev/null +++ b/vendor/github.com/xeipuuv/gojsonschema/draft.go @@ -0,0 +1,118 @@ +// Copyright 2018 johandorland ( https://github.com/johandorland ) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gojsonschema + +import ( + "errors" + "math" + "reflect" + + "github.com/xeipuuv/gojsonreference" +) + +type Draft int + +const ( + Draft4 Draft = 4 + Draft6 Draft = 6 + Draft7 Draft = 7 + Hybrid Draft = math.MaxInt32 +) + +type draftConfig struct { + Version Draft + MetaSchemaURL string + MetaSchema string +} +type draftConfigs []draftConfig + +var drafts draftConfigs + +func init() { + drafts = []draftConfig{ + draftConfig{ + Version: Draft4, + MetaSchemaURL: "http://json-schema.org/draft-04/schema", + MetaSchema: `{"id":"http://json-schema.org/draft-04/schema#","$schema":"http://json-schema.org/draft-04/schema#","description":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"positiveInteger":{"type":"integer","minimum":0},"positiveIntegerDefault0":{"allOf":[{"$ref":"#/definitions/positiveInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"minItems":1,"uniqueItems":true}},"type":"object","properties":{"id":{"type":"string"},"$schema":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"default":{},"multipleOf":{"type":"number","minimum":0,"exclusiveMinimum":true},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"boolean","default":false},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"boolean","default":false},"maxLength":{"$ref":"#/definitions/positiveInteger"},"minLength":{"$ref":"#/definitions/positiveIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"anyOf":[{"type":"boolean"},{"$ref":"#"}],"default":{}},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":{}},"maxItems":{"$ref":"#/definitions/positiveInteger"},"minItems":{"$ref":"#/definitions/positiveIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"maxProperties":{"$ref":"#/definitions/positiveInteger"},"minProperties":{"$ref":"#/definitions/positiveIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"anyOf":[{"type":"boolean"},{"$ref":"#"}],"default":{}},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"enum":{"type":"array","minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"dependencies":{"exclusiveMaximum":["maximum"],"exclusiveMinimum":["minimum"]},"default":{}}`, + }, + draftConfig{ + Version: Draft6, + MetaSchemaURL: "http://json-schema.org/draft-06/schema", + MetaSchema: `{"$schema":"http://json-schema.org/draft-06/schema#","$id":"http://json-schema.org/draft-06/schema#","title":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"allOf":[{"$ref":"#/definitions/nonNegativeInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}},"type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference"},"$schema":{"type":"string","format":"uri"},"$ref":{"type":"string","format":"uri-reference"},"title":{"type":"string"},"description":{"type":"string"},"default":{},"examples":{"type":"array","items":{}},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/definitions/nonNegativeInteger"},"minLength":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"$ref":"#"},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":{}},"maxItems":{"$ref":"#/definitions/nonNegativeInteger"},"minItems":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"contains":{"$ref":"#"},"maxProperties":{"$ref":"#/definitions/nonNegativeInteger"},"minProperties":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"$ref":"#"},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"propertyNames":{"$ref":"#"},"const":{},"enum":{"type":"array","minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"default":{}}`, + }, + draftConfig{ + Version: Draft7, + MetaSchemaURL: "http://json-schema.org/draft-07/schema", + MetaSchema: `{"$schema":"http://json-schema.org/draft-07/schema#","$id":"http://json-schema.org/draft-07/schema#","title":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"allOf":[{"$ref":"#/definitions/nonNegativeInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}},"type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference"},"$schema":{"type":"string","format":"uri"},"$ref":{"type":"string","format":"uri-reference"},"$comment":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"default":true,"readOnly":{"type":"boolean","default":false},"examples":{"type":"array","items":true},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/definitions/nonNegativeInteger"},"minLength":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"$ref":"#"},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":true},"maxItems":{"$ref":"#/definitions/nonNegativeInteger"},"minItems":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"contains":{"$ref":"#"},"maxProperties":{"$ref":"#/definitions/nonNegativeInteger"},"minProperties":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"$ref":"#"},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"propertyNames":{"$ref":"#"},"const":true,"enum":{"type":"array","items":true,"minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"if":{"$ref":"#"},"then":{"$ref":"#"},"else":{"$ref":"#"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"default":true}`, + }, + } +} + +func (dc draftConfigs) GetMetaSchema(url string) string { + for _, config := range dc { + if config.MetaSchemaURL == url { + return config.MetaSchema + } + } + return "" +} +func (dc draftConfigs) GetDraftVersion(url string) *Draft { + for _, config := range dc { + if config.MetaSchemaURL == url { + return &config.Version + } + } + return nil +} +func (dc draftConfigs) GetSchemaURL(draft Draft) string { + for _, config := range dc { + if config.Version == draft { + return config.MetaSchemaURL + } + } + return "" +} + +func parseSchemaURL(documentNode interface{}) (string, *Draft, error) { + + if isKind(documentNode, reflect.Bool) { + return "", nil, nil + } + m := documentNode.(map[string]interface{}) + + if existsMapKey(m, KEY_SCHEMA) { + if !isKind(m[KEY_SCHEMA], reflect.String) { + return "", nil, errors.New(formatErrorDescription( + Locale.MustBeOfType(), + ErrorDetails{ + "key": KEY_SCHEMA, + "type": TYPE_STRING, + }, + )) + } + + schemaReference, err := gojsonreference.NewJsonReference(m[KEY_SCHEMA].(string)) + + if err != nil { + return "", nil, err + } + + schema := schemaReference.String() + + return schema, drafts.GetDraftVersion(schema), nil + } + + return "", nil, nil +} diff --git a/vendor/github.com/xeipuuv/gojsonschema/errors.go b/vendor/github.com/xeipuuv/gojsonschema/errors.go index 5146cbbab6..2f01a1c2c7 100644 --- a/vendor/github.com/xeipuuv/gojsonschema/errors.go +++ b/vendor/github.com/xeipuuv/gojsonschema/errors.go @@ -1,10 +1,20 @@ package gojsonschema import ( - "fmt" - "strings" + "bytes" + "sync" + "text/template" ) +var errorTemplates errorTemplate = errorTemplate{template.New("errors-new"), sync.RWMutex{}} + +// template.Template is not thread-safe for writing, so some locking is done +// sync.RWMutex is used for efficiently locking when new templates are created +type errorTemplate struct { + *template.Template + sync.RWMutex +} + type ( // RequiredError. ErrorDetails: property string RequiredError struct { @@ -46,6 +56,11 @@ type ( ResultErrorFields } + // ConstError. ErrorDetails: allowed + ConstError struct { + ResultErrorFields + } + // EnumError. ErrorDetails: allowed EnumError struct { ResultErrorFields @@ -66,11 +81,16 @@ type ( ResultErrorFields } - // ItemsMustBeUniqueError. ErrorDetails: type + // ItemsMustBeUniqueError. ErrorDetails: type, i, j ItemsMustBeUniqueError struct { ResultErrorFields } + // ArrayContainsError. ErrorDetails: + ArrayContainsError struct { + ResultErrorFields + } + // ArrayMinPropertiesError. ErrorDetails: min ArrayMinPropertiesError struct { ResultErrorFields @@ -91,6 +111,11 @@ type ( ResultErrorFields } + // InvalidPopertyNameError. ErrorDetails: property + InvalidPropertyNameError struct { + ResultErrorFields + } + // StringLengthGTEError. ErrorDetails: min StringLengthGTEError struct { ResultErrorFields @@ -135,10 +160,20 @@ type ( NumberLTError struct { ResultErrorFields } + + // ConditionThenError. ErrorDetails: - + ConditionThenError struct { + ResultErrorFields + } + + // ConditionElseError. ErrorDetails: - + ConditionElseError struct { + ResultErrorFields + } ) // newError takes a ResultError type and sets the type, context, description, details, value, and field -func newError(err ResultError, context *jsonContext, value interface{}, locale locale, details ErrorDetails) { +func newError(err ResultError, context *JsonContext, value interface{}, locale locale, details ErrorDetails) { var t string var d string switch err.(type) { @@ -166,6 +201,9 @@ func newError(err ResultError, context *jsonContext, value interface{}, locale l case *InternalError: t = "internal" d = locale.Internal() + case *ConstError: + t = "const" + d = locale.Const() case *EnumError: t = "enum" d = locale.Enum() @@ -181,6 +219,9 @@ func newError(err ResultError, context *jsonContext, value interface{}, locale l case *ItemsMustBeUniqueError: t = "unique" d = locale.Unique() + case *ArrayContainsError: + t = "contains" + d = locale.ArrayContains() case *ArrayMinPropertiesError: t = "array_min_properties" d = locale.ArrayMinProperties() @@ -193,6 +234,9 @@ func newError(err ResultError, context *jsonContext, value interface{}, locale l case *InvalidPropertyPatternError: t = "invalid_property_pattern" d = locale.InvalidPropertyPattern() + case *InvalidPropertyNameError: + t = "invalid_property_name" + d = locale.InvalidPropertyName() case *StringLengthGTEError: t = "string_gte" d = locale.StringGTE() @@ -220,23 +264,61 @@ func newError(err ResultError, context *jsonContext, value interface{}, locale l case *NumberLTError: t = "number_lt" d = locale.NumberLT() + case *ConditionThenError: + t = "condition_then" + d = locale.ConditionThen() + case *ConditionElseError: + t = "condition_else" + d = locale.ConditionElse() } err.SetType(t) err.SetContext(context) err.SetValue(value) err.SetDetails(details) + err.SetDescriptionFormat(d) details["field"] = err.Field() - err.SetDescription(formatErrorDescription(d, details)) -} -// formatErrorDescription takes a string in this format: %field% is required -// and converts it to a string with replacements. The fields come from -// the ErrorDetails struct and vary for each type of error. -func formatErrorDescription(s string, details ErrorDetails) string { - for name, val := range details { - s = strings.Replace(s, "%"+strings.ToLower(name)+"%", fmt.Sprintf("%v", val), -1) + if _, exists := details["context"]; !exists && context != nil { + details["context"] = context.String() } - return s + err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details)) +} + +// formatErrorDescription takes a string in the default text/template +// format and converts it to a string with replacements. The fields come +// from the ErrorDetails struct and vary for each type of error. +func formatErrorDescription(s string, details ErrorDetails) string { + + var tpl *template.Template + var descrAsBuffer bytes.Buffer + var err error + + errorTemplates.RLock() + tpl = errorTemplates.Lookup(s) + errorTemplates.RUnlock() + + if tpl == nil { + errorTemplates.Lock() + tpl = errorTemplates.New(s) + + if ErrorTemplateFuncs != nil { + tpl.Funcs(ErrorTemplateFuncs) + } + + tpl, err = tpl.Parse(s) + errorTemplates.Unlock() + + if err != nil { + return err.Error() + } + } + + err = tpl.Execute(&descrAsBuffer, details) + if err != nil { + return err.Error() + } + + return descrAsBuffer.String() } diff --git a/vendor/github.com/xeipuuv/gojsonschema/format_checkers.go b/vendor/github.com/xeipuuv/gojsonschema/format_checkers.go index b91c360a81..26217fca12 100644 --- a/vendor/github.com/xeipuuv/gojsonschema/format_checkers.go +++ b/vendor/github.com/xeipuuv/gojsonschema/format_checkers.go @@ -2,17 +2,18 @@ package gojsonschema import ( "net" + "net/mail" "net/url" - "reflect" "regexp" "strings" + "sync" "time" ) type ( // FormatChecker is the interface all formatters added to FormatCheckerChain must implement FormatChecker interface { - IsFormat(input string) bool + IsFormat(input interface{}) bool } // FormatCheckerChain holds the formatters @@ -52,14 +53,33 @@ type ( // http://tools.ietf.org/html/rfc3339#section-5.6 DateTimeFormatChecker struct{} - // URIFormatCheckers validates a URI with a valid Scheme per RFC3986 + DateFormatChecker struct{} + + TimeFormatChecker struct{} + + // URIFormatChecker validates a URI with a valid Scheme per RFC3986 URIFormatChecker struct{} + // URIReferenceFormatChecker validates a URI or relative-reference per RFC3986 + URIReferenceFormatChecker struct{} + + // URITemplateFormatChecker validates a URI template per RFC6570 + URITemplateFormatChecker struct{} + // HostnameFormatChecker validates a hostname is in the correct format HostnameFormatChecker struct{} // UUIDFormatChecker validates a UUID is in the correct format UUIDFormatChecker struct{} + + // RegexFormatChecker validates a regex is in the correct format + RegexFormatChecker struct{} + + // JSONPointerFormatChecker validates a JSON Pointer per RFC6901 + JSONPointerFormatChecker struct{} + + // RelativeJSONPointerFormatChecker validates a relative JSON Pointer is in the correct format + RelativeJSONPointerFormatChecker struct{} ) var ( @@ -67,43 +87,65 @@ var ( // so library users can add custom formatters FormatCheckers = FormatCheckerChain{ formatters: map[string]FormatChecker{ - "date-time": DateTimeFormatChecker{}, - "hostname": HostnameFormatChecker{}, - "email": EmailFormatChecker{}, - "ipv4": IPV4FormatChecker{}, - "ipv6": IPV6FormatChecker{}, - "uri": URIFormatChecker{}, - "uuid": UUIDFormatChecker{}, + "date": DateFormatChecker{}, + "time": TimeFormatChecker{}, + "date-time": DateTimeFormatChecker{}, + "hostname": HostnameFormatChecker{}, + "email": EmailFormatChecker{}, + "idn-email": EmailFormatChecker{}, + "ipv4": IPV4FormatChecker{}, + "ipv6": IPV6FormatChecker{}, + "uri": URIFormatChecker{}, + "uri-reference": URIReferenceFormatChecker{}, + "iri": URIFormatChecker{}, + "iri-reference": URIReferenceFormatChecker{}, + "uri-template": URITemplateFormatChecker{}, + "uuid": UUIDFormatChecker{}, + "regex": RegexFormatChecker{}, + "json-pointer": JSONPointerFormatChecker{}, + "relative-json-pointer": RelativeJSONPointerFormatChecker{}, }, } - // Regex credit: https://github.com/asaskevich/govalidator - rxEmail = regexp.MustCompile("^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$") - // Regex credit: https://www.socketloop.com/tutorials/golang-validate-hostname rxHostname = regexp.MustCompile(`^([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]))*$`) + // Use a regex to make sure curly brackets are balanced properly after validating it as a AURI + rxURITemplate = regexp.MustCompile("^([^{]*({[^}]*})?)*$") + rxUUID = regexp.MustCompile("^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$") + + rxJSONPointer = regexp.MustCompile("^(?:/(?:[^~/]|~0|~1)*)*$") + + rxRelJSONPointer = regexp.MustCompile("^(?:0|[1-9][0-9]*)(?:#|(?:/(?:[^~/]|~0|~1)*)*)$") + + lock = new(sync.Mutex) ) // Add adds a FormatChecker to the FormatCheckerChain // The name used will be the value used for the format key in your json schema func (c *FormatCheckerChain) Add(name string, f FormatChecker) *FormatCheckerChain { + lock.Lock() c.formatters[name] = f + lock.Unlock() return c } // Remove deletes a FormatChecker from the FormatCheckerChain (if it exists) func (c *FormatCheckerChain) Remove(name string) *FormatCheckerChain { + lock.Lock() delete(c.formatters, name) + lock.Unlock() return c } // Has checks to see if the FormatCheckerChain holds a FormatChecker with the given name func (c *FormatCheckerChain) Has(name string) bool { + lock.Lock() _, ok := c.formatters[name] + lock.Unlock() return ok } @@ -117,32 +159,52 @@ func (c *FormatCheckerChain) IsFormat(name string, input interface{}) bool { return false } - if !isKind(input, reflect.String) { + return f.IsFormat(input) +} + +func (f EmailFormatChecker) IsFormat(input interface{}) bool { + + asString, ok := input.(string) + if ok == false { return false } - inputString := input.(string) + _, err := mail.ParseAddress(asString) - return f.IsFormat(inputString) -} - -func (f EmailFormatChecker) IsFormat(input string) bool { - return rxEmail.MatchString(input) + return err == nil } // Credit: https://github.com/asaskevich/govalidator -func (f IPV4FormatChecker) IsFormat(input string) bool { - ip := net.ParseIP(input) - return ip != nil && strings.Contains(input, ".") +func (f IPV4FormatChecker) IsFormat(input interface{}) bool { + + asString, ok := input.(string) + if ok == false { + return false + } + + ip := net.ParseIP(asString) + return ip != nil && strings.Contains(asString, ".") } // Credit: https://github.com/asaskevich/govalidator -func (f IPV6FormatChecker) IsFormat(input string) bool { - ip := net.ParseIP(input) - return ip != nil && strings.Contains(input, ":") +func (f IPV6FormatChecker) IsFormat(input interface{}) bool { + + asString, ok := input.(string) + if ok == false { + return false + } + + ip := net.ParseIP(asString) + return ip != nil && strings.Contains(asString, ":") } -func (f DateTimeFormatChecker) IsFormat(input string) bool { +func (f DateTimeFormatChecker) IsFormat(input interface{}) bool { + + asString, ok := input.(string) + if ok == false { + return false + } + formats := []string{ "15:04:05", "15:04:05Z07:00", @@ -152,7 +214,7 @@ func (f DateTimeFormatChecker) IsFormat(input string) bool { } for _, format := range formats { - if _, err := time.Parse(format, input); err == nil { + if _, err := time.Parse(format, asString); err == nil { return true } } @@ -160,19 +222,122 @@ func (f DateTimeFormatChecker) IsFormat(input string) bool { return false } -func (f URIFormatChecker) IsFormat(input string) bool { - u, err := url.Parse(input) +func (f DateFormatChecker) IsFormat(input interface{}) bool { + asString, ok := input.(string) + if ok == false { + return false + } + _, err := time.Parse("2006-01-02", asString) + return err == nil +} + +func (f TimeFormatChecker) IsFormat(input interface{}) bool { + asString, ok := input.(string) + if ok == false { + return false + } + + if _, err := time.Parse("15:04:05Z07:00", asString); err == nil { + return true + } + + _, err := time.Parse("15:04:05", asString) + return err == nil +} + +func (f URIFormatChecker) IsFormat(input interface{}) bool { + + asString, ok := input.(string) + if ok == false { + return false + } + + u, err := url.Parse(asString) + if err != nil || u.Scheme == "" { return false } + return !strings.Contains(asString, `\`) +} + +func (f URIReferenceFormatChecker) IsFormat(input interface{}) bool { + + asString, ok := input.(string) + if ok == false { + return false + } + + _, err := url.Parse(asString) + return err == nil && !strings.Contains(asString, `\`) +} + +func (f URITemplateFormatChecker) IsFormat(input interface{}) bool { + asString, ok := input.(string) + if ok == false { + return false + } + + u, err := url.Parse(asString) + if err != nil || strings.Contains(asString, `\`) { + return false + } + + return rxURITemplate.MatchString(u.Path) +} + +func (f HostnameFormatChecker) IsFormat(input interface{}) bool { + + asString, ok := input.(string) + if ok == false { + return false + } + + return rxHostname.MatchString(asString) && len(asString) < 256 +} + +func (f UUIDFormatChecker) IsFormat(input interface{}) bool { + + asString, ok := input.(string) + if ok == false { + return false + } + + return rxUUID.MatchString(asString) +} + +// IsFormat implements FormatChecker interface. +func (f RegexFormatChecker) IsFormat(input interface{}) bool { + + asString, ok := input.(string) + if ok == false { + return false + } + + if asString == "" { + return true + } + _, err := regexp.Compile(asString) + if err != nil { + return false + } return true } -func (f HostnameFormatChecker) IsFormat(input string) bool { - return rxHostname.MatchString(input) && len(input) < 256 +func (f JSONPointerFormatChecker) IsFormat(input interface{}) bool { + asString, ok := input.(string) + if ok == false { + return false + } + + return rxJSONPointer.MatchString(asString) } -func (f UUIDFormatChecker) IsFormat(input string) bool { - return rxUUID.MatchString(input) +func (f RelativeJSONPointerFormatChecker) IsFormat(input interface{}) bool { + asString, ok := input.(string) + if ok == false { + return false + } + + return rxRelJSONPointer.MatchString(asString) } diff --git a/vendor/github.com/xeipuuv/gojsonschema/jsonContext.go b/vendor/github.com/xeipuuv/gojsonschema/jsonContext.go index fcc8d9d6f1..f40668a74c 100644 --- a/vendor/github.com/xeipuuv/gojsonschema/jsonContext.go +++ b/vendor/github.com/xeipuuv/gojsonschema/jsonContext.go @@ -26,20 +26,20 @@ package gojsonschema import "bytes" -// jsonContext implements a persistent linked-list of strings -type jsonContext struct { +// JsonContext implements a persistent linked-list of strings +type JsonContext struct { head string - tail *jsonContext + tail *JsonContext } -func newJsonContext(head string, tail *jsonContext) *jsonContext { - return &jsonContext{head, tail} +func NewJsonContext(head string, tail *JsonContext) *JsonContext { + return &JsonContext{head, tail} } // String displays the context in reverse. // This plays well with the data structure's persistent nature with // Cons and a json document's tree structure. -func (c *jsonContext) String(del ...string) string { +func (c *JsonContext) String(del ...string) string { byteArr := make([]byte, 0, c.stringLen()) buf := bytes.NewBuffer(byteArr) c.writeStringToBuffer(buf, del) @@ -47,7 +47,7 @@ func (c *jsonContext) String(del ...string) string { return buf.String() } -func (c *jsonContext) stringLen() int { +func (c *JsonContext) stringLen() int { length := 0 if c.tail != nil { length = c.tail.stringLen() + 1 // add 1 for "." @@ -57,7 +57,7 @@ func (c *jsonContext) stringLen() int { return length } -func (c *jsonContext) writeStringToBuffer(buf *bytes.Buffer, del []string) { +func (c *JsonContext) writeStringToBuffer(buf *bytes.Buffer, del []string) { if c.tail != nil { c.tail.writeStringToBuffer(buf, del) diff --git a/vendor/github.com/xeipuuv/gojsonschema/jsonLoader.go b/vendor/github.com/xeipuuv/gojsonschema/jsonLoader.go index a9458a8e5d..cfa5f6a3a0 100644 --- a/vendor/github.com/xeipuuv/gojsonschema/jsonLoader.go +++ b/vendor/github.com/xeipuuv/gojsonschema/jsonLoader.go @@ -33,6 +33,7 @@ import ( "io" "io/ioutil" "net/http" + "os" "path/filepath" "runtime" "strings" @@ -40,34 +41,92 @@ import ( "github.com/xeipuuv/gojsonreference" ) +var osFS = osFileSystem(os.Open) + // JSON loader interface type JSONLoader interface { - jsonSource() interface{} - loadJSON() (interface{}, error) - loadSchema() (*Schema, error) + JsonSource() interface{} + LoadJSON() (interface{}, error) + JsonReference() (gojsonreference.JsonReference, error) + LoaderFactory() JSONLoaderFactory +} + +type JSONLoaderFactory interface { + New(source string) JSONLoader +} + +type DefaultJSONLoaderFactory struct { +} + +type FileSystemJSONLoaderFactory struct { + fs http.FileSystem +} + +func (d DefaultJSONLoaderFactory) New(source string) JSONLoader { + return &jsonReferenceLoader{ + fs: osFS, + source: source, + } +} + +func (f FileSystemJSONLoaderFactory) New(source string) JSONLoader { + return &jsonReferenceLoader{ + fs: f.fs, + source: source, + } +} + +// osFileSystem is a functional wrapper for os.Open that implements http.FileSystem. +type osFileSystem func(string) (*os.File, error) + +func (o osFileSystem) Open(name string) (http.File, error) { + return o(name) } // JSON Reference loader // references are used to load JSONs from files and HTTP type jsonReferenceLoader struct { + fs http.FileSystem source string } -func (l *jsonReferenceLoader) jsonSource() interface{} { +func (l *jsonReferenceLoader) JsonSource() interface{} { return l.source } -func NewReferenceLoader(source string) *jsonReferenceLoader { - return &jsonReferenceLoader{source: source} +func (l *jsonReferenceLoader) JsonReference() (gojsonreference.JsonReference, error) { + return gojsonreference.NewJsonReference(l.JsonSource().(string)) } -func (l *jsonReferenceLoader) loadJSON() (interface{}, error) { +func (l *jsonReferenceLoader) LoaderFactory() JSONLoaderFactory { + return &FileSystemJSONLoaderFactory{ + fs: l.fs, + } +} + +// NewReferenceLoader returns a JSON reference loader using the given source and the local OS file system. +func NewReferenceLoader(source string) JSONLoader { + return &jsonReferenceLoader{ + fs: osFS, + source: source, + } +} + +// NewReferenceLoaderFileSystem returns a JSON reference loader using the given source and file system. +func NewReferenceLoaderFileSystem(source string, fs http.FileSystem) JSONLoader { + return &jsonReferenceLoader{ + fs: fs, + source: source, + } +} + +func (l *jsonReferenceLoader) LoadJSON() (interface{}, error) { var err error - reference, err := gojsonreference.NewJsonReference(l.jsonSource().(string)) + reference, err := gojsonreference.NewJsonReference(l.JsonSource().(string)) if err != nil { return nil, err } @@ -79,15 +138,12 @@ func (l *jsonReferenceLoader) loadJSON() (interface{}, error) { if reference.HasFileScheme { - filename := strings.Replace(refToUrl.String(), "file://", "", -1) + filename := strings.TrimPrefix(refToUrl.String(), "file://") if runtime.GOOS == "windows" { // on Windows, a file URL may have an extra leading slash, use slashes // instead of backslashes, and have spaces escaped - if strings.HasPrefix(filename, "/") { - filename = filename[1:] - } + filename = strings.TrimPrefix(filename, "/") filename = filepath.FromSlash(filename) - filename = strings.Replace(filename, "%20", " ", -1) } document, err = l.loadFromFile(filename) @@ -108,35 +164,14 @@ func (l *jsonReferenceLoader) loadJSON() (interface{}, error) { } -func (l *jsonReferenceLoader) loadSchema() (*Schema, error) { - - var err error - - d := Schema{} - d.pool = newSchemaPool() - d.referencePool = newSchemaReferencePool() - - d.documentReference, err = gojsonreference.NewJsonReference(l.jsonSource().(string)) - if err != nil { - return nil, err - } - - spd, err := d.pool.GetDocument(d.documentReference) - if err != nil { - return nil, err - } - - err = d.parse(spd.Document) - if err != nil { - return nil, err - } - - return &d, nil - -} - func (l *jsonReferenceLoader) loadFromHTTP(address string) (interface{}, error) { + // returned cached versions for metaschemas for drafts 4, 6 and 7 + // for performance and allow for easier offline use + if metaSchema := drafts.GetMetaSchema(address); metaSchema != "" { + return decodeJsonUsingNumber(strings.NewReader(metaSchema)) + } + resp, err := http.Get(address) if err != nil { return nil, err @@ -144,7 +179,7 @@ func (l *jsonReferenceLoader) loadFromHTTP(address string) (interface{}, error) // must return HTTP Status 200 OK if resp.StatusCode != http.StatusOK { - return nil, errors.New(formatErrorDescription(Locale.httpBadStatus(), ErrorDetails{"status": resp.Status})) + return nil, errors.New(formatErrorDescription(Locale.HttpBadStatus(), ErrorDetails{"status": resp.Status})) } bodyBuff, err := ioutil.ReadAll(resp.Body) @@ -153,12 +188,16 @@ func (l *jsonReferenceLoader) loadFromHTTP(address string) (interface{}, error) } return decodeJsonUsingNumber(bytes.NewReader(bodyBuff)) - } func (l *jsonReferenceLoader) loadFromFile(path string) (interface{}, error) { + f, err := l.fs.Open(path) + if err != nil { + return nil, err + } + defer f.Close() - bodyBuff, err := ioutil.ReadFile(path) + bodyBuff, err := ioutil.ReadAll(f) if err != nil { return nil, err } @@ -173,45 +212,52 @@ type jsonStringLoader struct { source string } -func (l *jsonStringLoader) jsonSource() interface{} { +func (l *jsonStringLoader) JsonSource() interface{} { return l.source } -func NewStringLoader(source string) *jsonStringLoader { +func (l *jsonStringLoader) JsonReference() (gojsonreference.JsonReference, error) { + return gojsonreference.NewJsonReference("#") +} + +func (l *jsonStringLoader) LoaderFactory() JSONLoaderFactory { + return &DefaultJSONLoaderFactory{} +} + +func NewStringLoader(source string) JSONLoader { return &jsonStringLoader{source: source} } -func (l *jsonStringLoader) loadJSON() (interface{}, error) { +func (l *jsonStringLoader) LoadJSON() (interface{}, error) { - return decodeJsonUsingNumber(strings.NewReader(l.jsonSource().(string))) + return decodeJsonUsingNumber(strings.NewReader(l.JsonSource().(string))) } -func (l *jsonStringLoader) loadSchema() (*Schema, error) { +// JSON bytes loader - var err error +type jsonBytesLoader struct { + source []byte +} - document, err := l.loadJSON() - if err != nil { - return nil, err - } +func (l *jsonBytesLoader) JsonSource() interface{} { + return l.source +} - d := Schema{} - d.pool = newSchemaPool() - d.referencePool = newSchemaReferencePool() - d.documentReference, err = gojsonreference.NewJsonReference("#") - d.pool.SetStandaloneDocument(document) - if err != nil { - return nil, err - } +func (l *jsonBytesLoader) JsonReference() (gojsonreference.JsonReference, error) { + return gojsonreference.NewJsonReference("#") +} - err = d.parse(document) - if err != nil { - return nil, err - } +func (l *jsonBytesLoader) LoaderFactory() JSONLoaderFactory { + return &DefaultJSONLoaderFactory{} +} - return &d, nil +func NewBytesLoader(source []byte) JSONLoader { + return &jsonBytesLoader{source: source} +} +func (l *jsonBytesLoader) LoadJSON() (interface{}, error) { + return decodeJsonUsingNumber(bytes.NewReader(l.JsonSource().([]byte))) } // JSON Go (types) loader @@ -221,19 +267,27 @@ type jsonGoLoader struct { source interface{} } -func (l *jsonGoLoader) jsonSource() interface{} { +func (l *jsonGoLoader) JsonSource() interface{} { return l.source } -func NewGoLoader(source interface{}) *jsonGoLoader { +func (l *jsonGoLoader) JsonReference() (gojsonreference.JsonReference, error) { + return gojsonreference.NewJsonReference("#") +} + +func (l *jsonGoLoader) LoaderFactory() JSONLoaderFactory { + return &DefaultJSONLoaderFactory{} +} + +func NewGoLoader(source interface{}) JSONLoader { return &jsonGoLoader{source: source} } -func (l *jsonGoLoader) loadJSON() (interface{}, error) { +func (l *jsonGoLoader) LoadJSON() (interface{}, error) { // convert it to a compliant JSON first to avoid types "mismatches" - jsonBytes, err := json.Marshal(l.jsonSource()) + jsonBytes, err := json.Marshal(l.JsonSource()) if err != nil { return nil, err } @@ -242,31 +296,58 @@ func (l *jsonGoLoader) loadJSON() (interface{}, error) { } -func (l *jsonGoLoader) loadSchema() (*Schema, error) { +type jsonIOLoader struct { + buf *bytes.Buffer +} - var err error +func NewReaderLoader(source io.Reader) (JSONLoader, io.Reader) { + buf := &bytes.Buffer{} + return &jsonIOLoader{buf: buf}, io.TeeReader(source, buf) +} - document, err := l.loadJSON() - if err != nil { - return nil, err - } +func NewWriterLoader(source io.Writer) (JSONLoader, io.Writer) { + buf := &bytes.Buffer{} + return &jsonIOLoader{buf: buf}, io.MultiWriter(source, buf) +} - d := Schema{} - d.pool = newSchemaPool() - d.referencePool = newSchemaReferencePool() - d.documentReference, err = gojsonreference.NewJsonReference("#") - d.pool.SetStandaloneDocument(document) - if err != nil { - return nil, err - } +func (l *jsonIOLoader) JsonSource() interface{} { + return l.buf.String() +} - err = d.parse(document) - if err != nil { - return nil, err - } +func (l *jsonIOLoader) LoadJSON() (interface{}, error) { + return decodeJsonUsingNumber(l.buf) +} - return &d, nil +func (l *jsonIOLoader) JsonReference() (gojsonreference.JsonReference, error) { + return gojsonreference.NewJsonReference("#") +} +func (l *jsonIOLoader) LoaderFactory() JSONLoaderFactory { + return &DefaultJSONLoaderFactory{} +} + +// JSON raw loader +// In case the JSON is already marshalled to interface{} use this loader +// This is used for testing as otherwise there is no guarantee the JSON is marshalled +// "properly" by using https://golang.org/pkg/encoding/json/#Decoder.UseNumber +type jsonRawLoader struct { + source interface{} +} + +func NewRawLoader(source interface{}) *jsonRawLoader { + return &jsonRawLoader{source: source} +} +func (l *jsonRawLoader) JsonSource() interface{} { + return l.source +} +func (l *jsonRawLoader) LoadJSON() (interface{}, error) { + return l.source, nil +} +func (l *jsonRawLoader) JsonReference() (gojsonreference.JsonReference, error) { + return gojsonreference.NewJsonReference("#") +} +func (l *jsonRawLoader) LoaderFactory() JSONLoaderFactory { + return &DefaultJSONLoaderFactory{} } func decodeJsonUsingNumber(r io.Reader) (interface{}, error) { diff --git a/vendor/github.com/xeipuuv/gojsonschema/locales.go b/vendor/github.com/xeipuuv/gojsonschema/locales.go index de05d60df3..9b4570f011 100644 --- a/vendor/github.com/xeipuuv/gojsonschema/locales.go +++ b/vendor/github.com/xeipuuv/gojsonschema/locales.go @@ -26,7 +26,7 @@ package gojsonschema type ( - // locale is an interface for definining custom error strings + // locale is an interface for defining custom error strings locale interface { Required() string InvalidType() string @@ -36,15 +36,19 @@ type ( NumberNot() string MissingDependency() string Internal() string + Const() string Enum() string + ArrayNotEnoughItems() string ArrayNoAdditionalItems() string ArrayMinItems() string ArrayMaxItems() string Unique() string + ArrayContains() string ArrayMinProperties() string ArrayMaxProperties() string AdditionalPropertyNotAllowed() string InvalidPropertyPattern() string + InvalidPropertyName() string StringGTE() string StringLTE() string DoesNotMatchPattern() string @@ -72,7 +76,11 @@ type ( ReferenceMustBeCanonical() string NotAValidType() string Duplicated() string - httpBadStatus() string + HttpBadStatus() string + ParseError() string + + ConditionThen() string + ConditionElse() string // ErrorFormat ErrorFormat() string @@ -83,11 +91,11 @@ type ( ) func (l DefaultLocale) Required() string { - return `%property% is required` + return `{{.property}} is required` } func (l DefaultLocale) InvalidType() string { - return `Invalid type. Expected: %expected%, given: %given%` + return `Invalid type. Expected: {{.expected}}, given: {{.given}}` } func (l DefaultLocale) NumberAnyOf() string { @@ -107,164 +115,194 @@ func (l DefaultLocale) NumberNot() string { } func (l DefaultLocale) MissingDependency() string { - return `Has a dependency on %dependency%` + return `Has a dependency on {{.dependency}}` } func (l DefaultLocale) Internal() string { - return `Internal Error %error%` + return `Internal Error {{.error}}` +} + +func (l DefaultLocale) Const() string { + return `{{.field}} does not match: {{.allowed}}` } func (l DefaultLocale) Enum() string { - return `%field% must be one of the following: %allowed%` + return `{{.field}} must be one of the following: {{.allowed}}` } func (l DefaultLocale) ArrayNoAdditionalItems() string { return `No additional items allowed on array` } +func (l DefaultLocale) ArrayNotEnoughItems() string { + return `Not enough items on array to match positional list of schema` +} + func (l DefaultLocale) ArrayMinItems() string { - return `Array must have at least %min% items` + return `Array must have at least {{.min}} items` } func (l DefaultLocale) ArrayMaxItems() string { - return `Array must have at most %max% items` + return `Array must have at most {{.max}} items` } func (l DefaultLocale) Unique() string { - return `%type% items must be unique` + return `{{.type}} items[{{.i}},{{.j}}] must be unique` +} + +func (l DefaultLocale) ArrayContains() string { + return `At least one of the items must match` } func (l DefaultLocale) ArrayMinProperties() string { - return `Must have at least %min% properties` + return `Must have at least {{.min}} properties` } func (l DefaultLocale) ArrayMaxProperties() string { - return `Must have at most %max% properties` + return `Must have at most {{.max}} properties` } func (l DefaultLocale) AdditionalPropertyNotAllowed() string { - return `Additional property %property% is not allowed` + return `Additional property {{.property}} is not allowed` } func (l DefaultLocale) InvalidPropertyPattern() string { - return `Property "%property%" does not match pattern %pattern%` + return `Property "{{.property}}" does not match pattern {{.pattern}}` +} + +func (l DefaultLocale) InvalidPropertyName() string { + return `Property name of "{{.property}}" does not match` } func (l DefaultLocale) StringGTE() string { - return `String length must be greater than or equal to %min%` + return `String length must be greater than or equal to {{.min}}` } func (l DefaultLocale) StringLTE() string { - return `String length must be less than or equal to %max%` + return `String length must be less than or equal to {{.max}}` } func (l DefaultLocale) DoesNotMatchPattern() string { - return `Does not match pattern '%pattern%'` + return `Does not match pattern '{{.pattern}}'` } func (l DefaultLocale) DoesNotMatchFormat() string { - return `Does not match format '%format%'` + return `Does not match format '{{.format}}'` } func (l DefaultLocale) MultipleOf() string { - return `Must be a multiple of %multiple%` + return `Must be a multiple of {{.multiple}}` } func (l DefaultLocale) NumberGTE() string { - return `Must be greater than or equal to %min%` + return `Must be greater than or equal to {{.min}}` } func (l DefaultLocale) NumberGT() string { - return `Must be greater than %min%` + return `Must be greater than {{.min}}` } func (l DefaultLocale) NumberLTE() string { - return `Must be less than or equal to %max%` + return `Must be less than or equal to {{.max}}` } func (l DefaultLocale) NumberLT() string { - return `Must be less than %max%` + return `Must be less than {{.max}}` } // Schema validators func (l DefaultLocale) RegexPattern() string { - return `Invalid regex pattern '%pattern%'` + return `Invalid regex pattern '{{.pattern}}'` } func (l DefaultLocale) GreaterThanZero() string { - return `%number% must be strictly greater than 0` + return `{{.number}} must be strictly greater than 0` } func (l DefaultLocale) MustBeOfA() string { - return `%x% must be of a %y%` + return `{{.x}} must be of a {{.y}}` } func (l DefaultLocale) MustBeOfAn() string { - return `%x% must be of an %y%` + return `{{.x}} must be of an {{.y}}` } func (l DefaultLocale) CannotBeUsedWithout() string { - return `%x% cannot be used without %y%` + return `{{.x}} cannot be used without {{.y}}` } func (l DefaultLocale) CannotBeGT() string { - return `%x% cannot be greater than %y%` + return `{{.x}} cannot be greater than {{.y}}` } func (l DefaultLocale) MustBeOfType() string { - return `%key% must be of type %type%` + return `{{.key}} must be of type {{.type}}` } func (l DefaultLocale) MustBeValidRegex() string { - return `%key% must be a valid regex` + return `{{.key}} must be a valid regex` } func (l DefaultLocale) MustBeValidFormat() string { - return `%key% must be a valid format %given%` + return `{{.key}} must be a valid format {{.given}}` } func (l DefaultLocale) MustBeGTEZero() string { - return `%key% must be greater than or equal to 0` + return `{{.key}} must be greater than or equal to 0` } func (l DefaultLocale) KeyCannotBeGreaterThan() string { - return `%key% cannot be greater than %y%` + return `{{.key}} cannot be greater than {{.y}}` } func (l DefaultLocale) KeyItemsMustBeOfType() string { - return `%key% items must be %type%` + return `{{.key}} items must be {{.type}}` } func (l DefaultLocale) KeyItemsMustBeUnique() string { - return `%key% items must be unique` + return `{{.key}} items must be unique` } func (l DefaultLocale) ReferenceMustBeCanonical() string { - return `Reference %reference% must be canonical` + return `Reference {{.reference}} must be canonical` } func (l DefaultLocale) NotAValidType() string { - return `%type% is not a valid type -- ` + return `has a primitive type that is NOT VALID -- given: {{.given}} Expected valid values are:{{.expected}}` } func (l DefaultLocale) Duplicated() string { - return `%type% type is duplicated` + return `{{.type}} type is duplicated` } -func (l DefaultLocale) httpBadStatus() string { - return `Could not read schema from HTTP, response status is %status%` +func (l DefaultLocale) HttpBadStatus() string { + return `Could not read schema from HTTP, response status is {{.status}}` } // Replacement options: field, description, context, value func (l DefaultLocale) ErrorFormat() string { - return `%field%: %description%` + return `{{.field}}: {{.description}}` +} + +//Parse error +func (l DefaultLocale) ParseError() string { + return `Expected: {{.expected}}, given: Invalid JSON` +} + +//If/Else +func (l DefaultLocale) ConditionThen() string { + return `Must validate "then" as "if" was valid` +} + +func (l DefaultLocale) ConditionElse() string { + return `Must validate "else" as "if" was not valid` } const ( STRING_NUMBER = "number" STRING_ARRAY_OF_STRINGS = "array of strings" STRING_ARRAY_OF_SCHEMAS = "array of schemas" - STRING_SCHEMA = "schema" + STRING_SCHEMA = "valid schema" STRING_SCHEMA_OR_ARRAY_OF_STRINGS = "schema or array of strings" STRING_PROPERTIES = "properties" STRING_DEPENDENCY = "dependency" diff --git a/vendor/github.com/xeipuuv/gojsonschema/result.go b/vendor/github.com/xeipuuv/gojsonschema/result.go index c6d337587b..040d35c8df 100644 --- a/vendor/github.com/xeipuuv/gojsonschema/result.go +++ b/vendor/github.com/xeipuuv/gojsonschema/result.go @@ -40,25 +40,29 @@ type ( Field() string SetType(string) Type() string - SetContext(*jsonContext) - Context() *jsonContext + SetContext(*JsonContext) + Context() *JsonContext SetDescription(string) Description() string + SetDescriptionFormat(string) + DescriptionFormat() string SetValue(interface{}) Value() interface{} SetDetails(ErrorDetails) Details() ErrorDetails + String() string } // ResultErrorFields holds the fields for each ResultError implementation. // ResultErrorFields implements the ResultError interface, so custom errors // can be defined by just embedding this type ResultErrorFields struct { - errorType string // A string with the type of error (i.e. invalid_type) - context *jsonContext // Tree like notation of the part that failed the validation. ex (root).a.b ... - description string // A human readable error message - value interface{} // Value given by the JSON file that is the source of the error - details ErrorDetails + errorType string // A string with the type of error (i.e. invalid_type) + context *JsonContext // Tree like notation of the part that failed the validation. ex (root).a.b ... + description string // A human readable error message + descriptionFormat string // A format for human readable error message + value interface{} // Value given by the JSON file that is the source of the error + details ErrorDetails } Result struct { @@ -72,12 +76,6 @@ type ( // Field outputs the field name without the root context // i.e. firstName or person.firstName instead of (root).firstName or (root).person.firstName func (v *ResultErrorFields) Field() string { - if p, ok := v.Details()["property"]; ok { - if str, isString := p.(string); isString { - return str - } - } - return strings.TrimPrefix(v.context.String(), STRING_ROOT_SCHEMA_PROPERTY+".") } @@ -89,11 +87,11 @@ func (v *ResultErrorFields) Type() string { return v.errorType } -func (v *ResultErrorFields) SetContext(context *jsonContext) { +func (v *ResultErrorFields) SetContext(context *JsonContext) { v.context = context } -func (v *ResultErrorFields) Context() *jsonContext { +func (v *ResultErrorFields) Context() *JsonContext { return v.context } @@ -105,6 +103,14 @@ func (v *ResultErrorFields) Description() string { return v.description } +func (v *ResultErrorFields) SetDescriptionFormat(descriptionFormat string) { + v.descriptionFormat = descriptionFormat +} + +func (v *ResultErrorFields) DescriptionFormat() string { + return v.descriptionFormat +} + func (v *ResultErrorFields) SetValue(value interface{}) { v.value = value } @@ -154,7 +160,19 @@ func (v *Result) Errors() []ResultError { return v.errors } -func (v *Result) addError(err ResultError, context *jsonContext, value interface{}, details ErrorDetails) { +// Add a fully filled error to the error set +// SetDescription() will be called with the result of the parsed err.DescriptionFormat() +func (v *Result) AddError(err ResultError, details ErrorDetails) { + if _, exists := details["context"]; !exists && err.Context() != nil { + details["context"] = err.Context().String() + } + + err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details)) + + v.errors = append(v.errors, err) +} + +func (v *Result) addInternalError(err ResultError, context *JsonContext, value interface{}, details ErrorDetails) { newError(err, context, value, Locale, details) v.errors = append(v.errors, err) v.score -= 2 // results in a net -1 when added to the +1 we get at the end of the validation function diff --git a/vendor/github.com/xeipuuv/gojsonschema/schema.go b/vendor/github.com/xeipuuv/gojsonschema/schema.go index 577c7863f3..323fe8559b 100644 --- a/vendor/github.com/xeipuuv/gojsonschema/schema.go +++ b/vendor/github.com/xeipuuv/gojsonschema/schema.go @@ -27,10 +27,11 @@ package gojsonschema import ( - // "encoding/json" "errors" + "math/big" "reflect" "regexp" + "text/template" "github.com/xeipuuv/gojsonreference" ) @@ -39,10 +40,13 @@ var ( // Locale is the default locale to use // Library users can overwrite with their own implementation Locale locale = DefaultLocale{} + + // ErrorTemplateFuncs allows you to define custom template funcs for use in localization. + ErrorTemplateFuncs template.FuncMap ) func NewSchema(l JSONLoader) (*Schema, error) { - return l.loadSchema() + return NewSchemaLoader().Compile(l) } type Schema struct { @@ -52,8 +56,8 @@ type Schema struct { referencePool *schemaReferencePool } -func (d *Schema) parse(document interface{}) error { - d.rootSchema = &subSchema{property: STRING_ROOT_SCHEMA_PROPERTY} +func (d *Schema) parse(document interface{}, draft Draft) error { + d.rootSchema = &subSchema{property: STRING_ROOT_SCHEMA_PROPERTY, draft: &draft} return d.parseSchema(document, d.rootSchema) } @@ -69,80 +73,95 @@ func (d *Schema) SetRootSchemaName(name string) { // func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) error { + if currentSchema.draft == nil { + if currentSchema.parent == nil { + return errors.New("Draft not set") + } + currentSchema.draft = currentSchema.parent.draft + } + + // As of draft 6 "true" is equivalent to an empty schema "{}" and false equals "{"not":{}}" + if *currentSchema.draft >= Draft6 && isKind(documentNode, reflect.Bool) { + b := documentNode.(bool) + if b { + documentNode = map[string]interface{}{} + } else { + documentNode = map[string]interface{}{"not": true} + } + } + if !isKind(documentNode, reflect.Map) { return errors.New(formatErrorDescription( - Locale.InvalidType(), + Locale.ParseError(), ErrorDetails{ - "expected": TYPE_OBJECT, - "given": STRING_SCHEMA, + "expected": STRING_SCHEMA, }, )) } m := documentNode.(map[string]interface{}) - if currentSchema == d.rootSchema { + if currentSchema.parent == nil { currentSchema.ref = &d.documentReference + currentSchema.id = &d.documentReference } - // $subSchema - if existsMapKey(m, KEY_SCHEMA) { - if !isKind(m[KEY_SCHEMA], reflect.String) { - return errors.New(formatErrorDescription( - Locale.InvalidType(), - ErrorDetails{ - "expected": TYPE_STRING, - "given": KEY_SCHEMA, - }, - )) - } - schemaRef := m[KEY_SCHEMA].(string) - schemaReference, err := gojsonreference.NewJsonReference(schemaRef) - currentSchema.subSchema = &schemaReference - if err != nil { - return err - } + if currentSchema.id == nil && currentSchema.parent != nil { + currentSchema.id = currentSchema.parent.id } - // $ref - if existsMapKey(m, KEY_REF) && !isKind(m[KEY_REF], reflect.String) { + // In draft 6 the id keyword was renamed to $id + // Hybrid mode uses the old id by default + var keyID string + + switch *currentSchema.draft { + case Draft4: + keyID = KEY_ID + case Hybrid: + keyID = KEY_ID_NEW + if existsMapKey(m, KEY_ID) { + keyID = KEY_ID + } + default: + keyID = KEY_ID_NEW + } + if existsMapKey(m, keyID) && !isKind(m[keyID], reflect.String) { return errors.New(formatErrorDescription( Locale.InvalidType(), ErrorDetails{ "expected": TYPE_STRING, - "given": KEY_REF, + "given": keyID, }, )) } - if k, ok := m[KEY_REF].(string); ok { - - if sch, ok := d.referencePool.Get(currentSchema.ref.String() + k); ok { - - currentSchema.refSchema = sch - + if k, ok := m[keyID].(string); ok { + jsonReference, err := gojsonreference.NewJsonReference(k) + if err != nil { + return err + } + if currentSchema == d.rootSchema { + currentSchema.id = &jsonReference } else { - - var err error - err = d.parseReference(documentNode, currentSchema, k) + ref, err := currentSchema.parent.id.Inherits(jsonReference) if err != nil { return err } - - return nil + currentSchema.id = ref } } // definitions if existsMapKey(m, KEY_DEFINITIONS) { - if isKind(m[KEY_DEFINITIONS], reflect.Map) { - currentSchema.definitions = make(map[string]*subSchema) - for dk, dv := range m[KEY_DEFINITIONS].(map[string]interface{}) { - if isKind(dv, reflect.Map) { - newSchema := &subSchema{property: KEY_DEFINITIONS, parent: currentSchema, ref: currentSchema.ref} - currentSchema.definitions[dk] = newSchema + if isKind(m[KEY_DEFINITIONS], reflect.Map, reflect.Bool) { + for _, dv := range m[KEY_DEFINITIONS].(map[string]interface{}) { + if isKind(dv, reflect.Map, reflect.Bool) { + + newSchema := &subSchema{property: KEY_DEFINITIONS, parent: currentSchema} + err := d.parseSchema(dv, newSchema) + if err != nil { - return errors.New(err.Error()) + return err } } else { return errors.New(formatErrorDescription( @@ -166,20 +185,6 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) } - // id - if existsMapKey(m, KEY_ID) && !isKind(m[KEY_ID], reflect.String) { - return errors.New(formatErrorDescription( - Locale.InvalidType(), - ErrorDetails{ - "expected": TYPE_STRING, - "given": KEY_ID, - }, - )) - } - if k, ok := m[KEY_ID].(string); ok { - currentSchema.id = &k - } - // title if existsMapKey(m, KEY_TITLE) && !isKind(m[KEY_TITLE], reflect.String) { return errors.New(formatErrorDescription( @@ -208,6 +213,39 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) currentSchema.description = &k } + // $ref + if existsMapKey(m, KEY_REF) && !isKind(m[KEY_REF], reflect.String) { + return errors.New(formatErrorDescription( + Locale.InvalidType(), + ErrorDetails{ + "expected": TYPE_STRING, + "given": KEY_REF, + }, + )) + } + + if k, ok := m[KEY_REF].(string); ok { + + jsonReference, err := gojsonreference.NewJsonReference(k) + if err != nil { + return err + } + + currentSchema.ref = &jsonReference + + if sch, ok := d.referencePool.Get(currentSchema.ref.String()); ok { + currentSchema.refSchema = sch + } else { + err := d.parseReference(documentNode, currentSchema) + + if err != nil { + return err + } + + return nil + } + } + // type if existsMapKey(m, KEY_TYPE) { if isKind(m[KEY_TYPE], reflect.String) { @@ -309,6 +347,26 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) } } + // propertyNames + if existsMapKey(m, KEY_PROPERTY_NAMES) && *currentSchema.draft >= Draft6 { + if isKind(m[KEY_PROPERTY_NAMES], reflect.Map, reflect.Bool) { + newSchema := &subSchema{property: KEY_PROPERTY_NAMES, parent: currentSchema, ref: currentSchema.ref} + currentSchema.propertyNames = newSchema + err := d.parseSchema(m[KEY_PROPERTY_NAMES], newSchema) + if err != nil { + return err + } + } else { + return errors.New(formatErrorDescription( + Locale.InvalidType(), + ErrorDetails{ + "expected": STRING_SCHEMA, + "given": KEY_PATTERN_PROPERTIES, + }, + )) + } + } + // dependencies if existsMapKey(m, KEY_DEPENDENCIES) { err := d.parseDependencies(m[KEY_DEPENDENCIES], currentSchema) @@ -321,7 +379,7 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) if existsMapKey(m, KEY_ITEMS) { if isKind(m[KEY_ITEMS], reflect.Slice) { for _, itemElement := range m[KEY_ITEMS].([]interface{}) { - if isKind(itemElement, reflect.Map) { + if isKind(itemElement, reflect.Map, reflect.Bool) { newSchema := &subSchema{parent: currentSchema, property: KEY_ITEMS} newSchema.ref = currentSchema.ref currentSchema.AddItemsChild(newSchema) @@ -340,7 +398,7 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) } currentSchema.itemsChildrenIsSingleSchema = false } - } else if isKind(m[KEY_ITEMS], reflect.Map) { + } else if isKind(m[KEY_ITEMS], reflect.Map, reflect.Bool) { newSchema := &subSchema{parent: currentSchema, property: KEY_ITEMS} newSchema.ref = currentSchema.ref currentSchema.AddItemsChild(newSchema) @@ -395,7 +453,7 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) }, )) } - if *multipleOfValue <= 0 { + if multipleOfValue.Cmp(big.NewRat(0, 1)) <= 0 { return errors.New(formatErrorDescription( Locale.GreaterThanZero(), ErrorDetails{"number": KEY_MULTIPLE_OF}, @@ -416,20 +474,62 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) } if existsMapKey(m, KEY_EXCLUSIVE_MINIMUM) { - if isKind(m[KEY_EXCLUSIVE_MINIMUM], reflect.Bool) { + switch *currentSchema.draft { + case Draft4: + if !isKind(m[KEY_EXCLUSIVE_MINIMUM], reflect.Bool) { + return errors.New(formatErrorDescription( + Locale.InvalidType(), + ErrorDetails{ + "expected": TYPE_BOOLEAN, + "given": KEY_EXCLUSIVE_MINIMUM, + }, + )) + } if currentSchema.minimum == nil { return errors.New(formatErrorDescription( Locale.CannotBeUsedWithout(), ErrorDetails{"x": KEY_EXCLUSIVE_MINIMUM, "y": KEY_MINIMUM}, )) } - exclusiveMinimumValue := m[KEY_EXCLUSIVE_MINIMUM].(bool) - currentSchema.exclusiveMinimum = exclusiveMinimumValue - } else { - return errors.New(formatErrorDescription( - Locale.MustBeOfA(), - ErrorDetails{"x": KEY_EXCLUSIVE_MINIMUM, "y": TYPE_BOOLEAN}, - )) + if m[KEY_EXCLUSIVE_MINIMUM].(bool) { + currentSchema.exclusiveMinimum = currentSchema.minimum + currentSchema.minimum = nil + } + case Hybrid: + if isKind(m[KEY_EXCLUSIVE_MINIMUM], reflect.Bool) { + if currentSchema.minimum == nil { + return errors.New(formatErrorDescription( + Locale.CannotBeUsedWithout(), + ErrorDetails{"x": KEY_EXCLUSIVE_MINIMUM, "y": KEY_MINIMUM}, + )) + } + if m[KEY_EXCLUSIVE_MINIMUM].(bool) { + currentSchema.exclusiveMinimum = currentSchema.minimum + currentSchema.minimum = nil + } + } else if isJsonNumber(m[KEY_EXCLUSIVE_MINIMUM]) { + currentSchema.exclusiveMinimum = mustBeNumber(m[KEY_EXCLUSIVE_MINIMUM]) + } else { + return errors.New(formatErrorDescription( + Locale.InvalidType(), + ErrorDetails{ + "expected": TYPE_BOOLEAN + "/" + TYPE_NUMBER, + "given": KEY_EXCLUSIVE_MINIMUM, + }, + )) + } + default: + if isJsonNumber(m[KEY_EXCLUSIVE_MINIMUM]) { + currentSchema.exclusiveMinimum = mustBeNumber(m[KEY_EXCLUSIVE_MINIMUM]) + } else { + return errors.New(formatErrorDescription( + Locale.InvalidType(), + ErrorDetails{ + "expected": TYPE_NUMBER, + "given": KEY_EXCLUSIVE_MINIMUM, + }, + )) + } } } @@ -445,29 +545,62 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) } if existsMapKey(m, KEY_EXCLUSIVE_MAXIMUM) { - if isKind(m[KEY_EXCLUSIVE_MAXIMUM], reflect.Bool) { + switch *currentSchema.draft { + case Draft4: + if !isKind(m[KEY_EXCLUSIVE_MAXIMUM], reflect.Bool) { + return errors.New(formatErrorDescription( + Locale.InvalidType(), + ErrorDetails{ + "expected": TYPE_BOOLEAN, + "given": KEY_EXCLUSIVE_MAXIMUM, + }, + )) + } if currentSchema.maximum == nil { return errors.New(formatErrorDescription( Locale.CannotBeUsedWithout(), ErrorDetails{"x": KEY_EXCLUSIVE_MAXIMUM, "y": KEY_MAXIMUM}, )) } - exclusiveMaximumValue := m[KEY_EXCLUSIVE_MAXIMUM].(bool) - currentSchema.exclusiveMaximum = exclusiveMaximumValue - } else { - return errors.New(formatErrorDescription( - Locale.MustBeOfA(), - ErrorDetails{"x": KEY_EXCLUSIVE_MAXIMUM, "y": STRING_NUMBER}, - )) - } - } - - if currentSchema.minimum != nil && currentSchema.maximum != nil { - if *currentSchema.minimum > *currentSchema.maximum { - return errors.New(formatErrorDescription( - Locale.CannotBeGT(), - ErrorDetails{"x": KEY_MINIMUM, "y": KEY_MAXIMUM}, - )) + if m[KEY_EXCLUSIVE_MAXIMUM].(bool) { + currentSchema.exclusiveMaximum = currentSchema.maximum + currentSchema.maximum = nil + } + case Hybrid: + if isKind(m[KEY_EXCLUSIVE_MAXIMUM], reflect.Bool) { + if currentSchema.maximum == nil { + return errors.New(formatErrorDescription( + Locale.CannotBeUsedWithout(), + ErrorDetails{"x": KEY_EXCLUSIVE_MAXIMUM, "y": KEY_MAXIMUM}, + )) + } + if m[KEY_EXCLUSIVE_MAXIMUM].(bool) { + currentSchema.exclusiveMaximum = currentSchema.maximum + currentSchema.maximum = nil + } + } else if isJsonNumber(m[KEY_EXCLUSIVE_MAXIMUM]) { + currentSchema.exclusiveMaximum = mustBeNumber(m[KEY_EXCLUSIVE_MAXIMUM]) + } else { + return errors.New(formatErrorDescription( + Locale.InvalidType(), + ErrorDetails{ + "expected": TYPE_BOOLEAN + "/" + TYPE_NUMBER, + "given": KEY_EXCLUSIVE_MAXIMUM, + }, + )) + } + default: + if isJsonNumber(m[KEY_EXCLUSIVE_MAXIMUM]) { + currentSchema.exclusiveMaximum = mustBeNumber(m[KEY_EXCLUSIVE_MAXIMUM]) + } else { + return errors.New(formatErrorDescription( + Locale.InvalidType(), + ErrorDetails{ + "expected": TYPE_NUMBER, + "given": KEY_EXCLUSIVE_MAXIMUM, + }, + )) + } } } @@ -538,11 +671,6 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) formatString, ok := m[KEY_FORMAT].(string) if ok && FormatCheckers.Has(formatString) { currentSchema.format = formatString - } else { - return errors.New(formatErrorDescription( - Locale.MustBeValidFormat(), - ErrorDetails{"key": KEY_FORMAT, "given": m[KEY_FORMAT]}, - )) } } @@ -662,8 +790,24 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) } } + if existsMapKey(m, KEY_CONTAINS) && *currentSchema.draft >= Draft6 { + newSchema := &subSchema{property: KEY_CONTAINS, parent: currentSchema, ref: currentSchema.ref} + currentSchema.contains = newSchema + err := d.parseSchema(m[KEY_CONTAINS], newSchema) + if err != nil { + return err + } + } + // validation : all + if existsMapKey(m, KEY_CONST) && *currentSchema.draft >= Draft6 { + err := currentSchema.AddConst(m[KEY_CONST]) + if err != nil { + return err + } + } + if existsMapKey(m, KEY_ENUM) { if isKind(m[KEY_ENUM], reflect.Slice) { for _, v := range m[KEY_ENUM].([]interface{}) { @@ -737,7 +881,7 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) } if existsMapKey(m, KEY_NOT) { - if isKind(m[KEY_NOT], reflect.Map) { + if isKind(m[KEY_NOT], reflect.Map, reflect.Bool) { newSchema := &subSchema{property: KEY_NOT, parent: currentSchema, ref: currentSchema.ref} currentSchema.SetNot(newSchema) err := d.parseSchema(m[KEY_NOT], newSchema) @@ -752,71 +896,91 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) } } + if *currentSchema.draft >= Draft7 { + if existsMapKey(m, KEY_IF) { + if isKind(m[KEY_IF], reflect.Map, reflect.Bool) { + newSchema := &subSchema{property: KEY_IF, parent: currentSchema, ref: currentSchema.ref} + currentSchema.SetIf(newSchema) + err := d.parseSchema(m[KEY_IF], newSchema) + if err != nil { + return err + } + } else { + return errors.New(formatErrorDescription( + Locale.MustBeOfAn(), + ErrorDetails{"x": KEY_IF, "y": TYPE_OBJECT}, + )) + } + } + + if existsMapKey(m, KEY_THEN) { + if isKind(m[KEY_THEN], reflect.Map, reflect.Bool) { + newSchema := &subSchema{property: KEY_THEN, parent: currentSchema, ref: currentSchema.ref} + currentSchema.SetThen(newSchema) + err := d.parseSchema(m[KEY_THEN], newSchema) + if err != nil { + return err + } + } else { + return errors.New(formatErrorDescription( + Locale.MustBeOfAn(), + ErrorDetails{"x": KEY_THEN, "y": TYPE_OBJECT}, + )) + } + } + + if existsMapKey(m, KEY_ELSE) { + if isKind(m[KEY_ELSE], reflect.Map, reflect.Bool) { + newSchema := &subSchema{property: KEY_ELSE, parent: currentSchema, ref: currentSchema.ref} + currentSchema.SetElse(newSchema) + err := d.parseSchema(m[KEY_ELSE], newSchema) + if err != nil { + return err + } + } else { + return errors.New(formatErrorDescription( + Locale.MustBeOfAn(), + ErrorDetails{"x": KEY_ELSE, "y": TYPE_OBJECT}, + )) + } + } + } + return nil } -func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSchema, reference string) (e error) { +func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSchema) error { + var ( + refdDocumentNode interface{} + dsp *schemaPoolDocument + err error + ) - var err error + newSchema := &subSchema{property: KEY_REF, parent: currentSchema, ref: currentSchema.ref} + + d.referencePool.Add(currentSchema.ref.String(), newSchema) + + dsp, err = d.pool.GetDocument(*currentSchema.ref) + if err != nil { + return err + } + newSchema.id = currentSchema.ref + + refdDocumentNode = dsp.Document + newSchema.draft = dsp.Draft - jsonReference, err := gojsonreference.NewJsonReference(reference) if err != nil { return err } - standaloneDocument := d.pool.GetStandaloneDocument() - - if jsonReference.HasFullUrl { - currentSchema.ref = &jsonReference - } else { - inheritedReference, err := currentSchema.ref.Inherits(jsonReference) - if err != nil { - return err - } - currentSchema.ref = inheritedReference - } - - jsonPointer := currentSchema.ref.GetPointer() - - var refdDocumentNode interface{} - - if standaloneDocument != nil { - - var err error - refdDocumentNode, _, err = jsonPointer.Get(standaloneDocument) - if err != nil { - return err - } - - } else { - - var err error - dsp, err := d.pool.GetDocument(*currentSchema.ref) - if err != nil { - return err - } - - refdDocumentNode, _, err = jsonPointer.Get(dsp.Document) - if err != nil { - return err - } - - } - - if !isKind(refdDocumentNode, reflect.Map) { + if !isKind(refdDocumentNode, reflect.Map, reflect.Bool) { return errors.New(formatErrorDescription( Locale.MustBeOfType(), ErrorDetails{"key": STRING_SCHEMA, "type": TYPE_OBJECT}, )) } - // returns the loaded referenced subSchema for the caller to update its current subSchema - newSchemaDocument := refdDocumentNode.(map[string]interface{}) - - newSchema := &subSchema{property: KEY_REF, parent: currentSchema, ref: currentSchema.ref} - d.referencePool.Add(currentSchema.ref.String()+reference, newSchema) - - err = d.parseSchema(newSchemaDocument, newSchema) + err = d.parseSchema(refdDocumentNode, newSchema) if err != nil { return err } @@ -884,7 +1048,7 @@ func (d *Schema) parseDependencies(documentNode interface{}, currentSchema *subS currentSchema.dependencies[k] = valuesToRegister } - case reflect.Map: + case reflect.Map, reflect.Bool: depSchema := &subSchema{property: k, parent: currentSchema, ref: currentSchema.ref} err := d.parseSchema(m[k], depSchema) if err != nil { diff --git a/vendor/github.com/xeipuuv/gojsonschema/schemaLoader.go b/vendor/github.com/xeipuuv/gojsonschema/schemaLoader.go new file mode 100644 index 0000000000..95e0568abb --- /dev/null +++ b/vendor/github.com/xeipuuv/gojsonschema/schemaLoader.go @@ -0,0 +1,203 @@ +// Copyright 2018 johandorland ( https://github.com/johandorland ) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gojsonschema + +import ( + "bytes" + "errors" + + "github.com/xeipuuv/gojsonreference" +) + +type SchemaLoader struct { + pool *schemaPool + AutoDetect bool + Validate bool + Draft Draft +} + +func NewSchemaLoader() *SchemaLoader { + + ps := &SchemaLoader{ + pool: &schemaPool{ + schemaPoolDocuments: make(map[string]*schemaPoolDocument), + }, + AutoDetect: true, + Validate: false, + Draft: Hybrid, + } + ps.pool.autoDetect = &ps.AutoDetect + + return ps +} + +func (sl *SchemaLoader) validateMetaschema(documentNode interface{}) error { + + var ( + schema string + err error + ) + if sl.AutoDetect { + schema, _, err = parseSchemaURL(documentNode) + if err != nil { + return err + } + } + + // If no explicit "$schema" is used, use the default metaschema associated with the draft used + if schema == "" { + if sl.Draft == Hybrid { + return nil + } + schema = drafts.GetSchemaURL(sl.Draft) + } + + //Disable validation when loading the metaschema to prevent an infinite recursive loop + sl.Validate = false + + metaSchema, err := sl.Compile(NewReferenceLoader(schema)) + + if err != nil { + return err + } + + sl.Validate = true + + result := metaSchema.validateDocument(documentNode) + + if !result.Valid() { + var res bytes.Buffer + for _, err := range result.Errors() { + res.WriteString(err.String()) + res.WriteString("\n") + } + return errors.New(res.String()) + } + + return nil +} + +// AddSchemas adds an arbritrary amount of schemas to the schema cache. As this function does not require +// an explicit URL, every schema should contain an $id, so that it can be referenced by the main schema +func (sl *SchemaLoader) AddSchemas(loaders ...JSONLoader) error { + emptyRef, _ := gojsonreference.NewJsonReference("") + + for _, loader := range loaders { + doc, err := loader.LoadJSON() + + if err != nil { + return err + } + + if sl.Validate { + if err := sl.validateMetaschema(doc); err != nil { + return err + } + } + + // Directly use the Recursive function, so that it get only added to the schema pool by $id + // and not by the ref of the document as it's empty + if err = sl.pool.parseReferences(doc, emptyRef, false); err != nil { + return err + } + } + + return nil +} + +//AddSchema adds a schema under the provided URL to the schema cache +func (sl *SchemaLoader) AddSchema(url string, loader JSONLoader) error { + + ref, err := gojsonreference.NewJsonReference(url) + + if err != nil { + return err + } + + doc, err := loader.LoadJSON() + + if err != nil { + return err + } + + if sl.Validate { + if err := sl.validateMetaschema(doc); err != nil { + return err + } + } + + return sl.pool.parseReferences(doc, ref, true) +} + +func (sl *SchemaLoader) Compile(rootSchema JSONLoader) (*Schema, error) { + + ref, err := rootSchema.JsonReference() + + if err != nil { + return nil, err + } + + d := Schema{} + d.pool = sl.pool + d.pool.jsonLoaderFactory = rootSchema.LoaderFactory() + d.documentReference = ref + d.referencePool = newSchemaReferencePool() + + var doc interface{} + if ref.String() != "" { + // Get document from schema pool + spd, err := d.pool.GetDocument(d.documentReference) + if err != nil { + return nil, err + } + doc = spd.Document + } else { + // Load JSON directly + doc, err = rootSchema.LoadJSON() + if err != nil { + return nil, err + } + // References need only be parsed if loading JSON directly + // as pool.GetDocument already does this for us if loading by reference + err = sl.pool.parseReferences(doc, ref, true) + if err != nil { + return nil, err + } + } + + if sl.Validate { + if err := sl.validateMetaschema(doc); err != nil { + return nil, err + } + } + + draft := sl.Draft + if sl.AutoDetect { + _, detectedDraft, err := parseSchemaURL(doc) + if err != nil { + return nil, err + } + if detectedDraft != nil { + draft = *detectedDraft + } + } + + err = d.parse(doc, draft) + if err != nil { + return nil, err + } + + return &d, nil +} diff --git a/vendor/github.com/xeipuuv/gojsonschema/schemaPool.go b/vendor/github.com/xeipuuv/gojsonschema/schemaPool.go index 79fbb60cb8..f124e038d7 100644 --- a/vendor/github.com/xeipuuv/gojsonschema/schemaPool.go +++ b/vendor/github.com/xeipuuv/gojsonschema/schemaPool.go @@ -28,80 +28,188 @@ package gojsonschema import ( "errors" + "fmt" + "reflect" "github.com/xeipuuv/gojsonreference" ) type schemaPoolDocument struct { Document interface{} + Draft *Draft } type schemaPool struct { schemaPoolDocuments map[string]*schemaPoolDocument - standaloneDocument interface{} + jsonLoaderFactory JSONLoaderFactory + autoDetect *bool } -func newSchemaPool() *schemaPool { +func (p *schemaPool) parseReferences(document interface{}, ref gojsonreference.JsonReference, pooled bool) error { - p := &schemaPool{} - p.schemaPoolDocuments = make(map[string]*schemaPoolDocument) - p.standaloneDocument = nil + var ( + draft *Draft + err error + reference = ref.String() + ) + // Only the root document should be added to the schema pool if pooled is true + if _, ok := p.schemaPoolDocuments[reference]; pooled && ok { + return fmt.Errorf("Reference already exists: \"%s\"", reference) + } - return p + if *p.autoDetect { + _, draft, err = parseSchemaURL(document) + if err != nil { + return err + } + } + + err = p.parseReferencesRecursive(document, ref, draft) + + if pooled { + p.schemaPoolDocuments[reference] = &schemaPoolDocument{Document: document, Draft: draft} + } + + return err } -func (p *schemaPool) SetStandaloneDocument(document interface{}) { - p.standaloneDocument = document -} +func (p *schemaPool) parseReferencesRecursive(document interface{}, ref gojsonreference.JsonReference, draft *Draft) error { + // parseReferencesRecursive parses a JSON document and resolves all $id and $ref references. + // For $ref references it takes into account the $id scope it is in and replaces + // the reference by the absolute resolved reference -func (p *schemaPool) GetStandaloneDocument() (document interface{}) { - return p.standaloneDocument + // When encountering errors it fails silently. Error handling is done when the schema + // is syntactically parsed and any error encountered here should also come up there. + switch m := document.(type) { + case []interface{}: + for _, v := range m { + p.parseReferencesRecursive(v, ref, draft) + } + case map[string]interface{}: + localRef := &ref + + keyID := KEY_ID_NEW + if existsMapKey(m, KEY_ID) { + keyID = KEY_ID + } + if existsMapKey(m, keyID) && isKind(m[keyID], reflect.String) { + jsonReference, err := gojsonreference.NewJsonReference(m[keyID].(string)) + if err == nil { + localRef, err = ref.Inherits(jsonReference) + if err == nil { + if _, ok := p.schemaPoolDocuments[localRef.String()]; ok { + return fmt.Errorf("Reference already exists: \"%s\"", localRef.String()) + } + p.schemaPoolDocuments[localRef.String()] = &schemaPoolDocument{Document: document, Draft: draft} + } + } + } + + if existsMapKey(m, KEY_REF) && isKind(m[KEY_REF], reflect.String) { + jsonReference, err := gojsonreference.NewJsonReference(m[KEY_REF].(string)) + if err == nil { + absoluteRef, err := localRef.Inherits(jsonReference) + if err == nil { + m[KEY_REF] = absoluteRef.String() + } + } + } + + for k, v := range m { + // const and enums should be interpreted literally, so ignore them + if k == KEY_CONST || k == KEY_ENUM { + continue + } + // Something like a property or a dependency is not a valid schema, as it might describe properties named "$ref", "$id" or "const", etc + // Therefore don't treat it like a schema. + if k == KEY_PROPERTIES || k == KEY_DEPENDENCIES || k == KEY_PATTERN_PROPERTIES { + if child, ok := v.(map[string]interface{}); ok { + for _, v := range child { + p.parseReferencesRecursive(v, *localRef, draft) + } + } + } else { + p.parseReferencesRecursive(v, *localRef, draft) + } + } + } + return nil } func (p *schemaPool) GetDocument(reference gojsonreference.JsonReference) (*schemaPoolDocument, error) { + var ( + spd *schemaPoolDocument + draft *Draft + ok bool + err error + ) + if internalLogEnabled { internalLog("Get Document ( %s )", reference.String()) } - var err error + // Create a deep copy, so we can remove the fragment part later on without altering the original + refToUrl, _ := gojsonreference.NewJsonReference(reference.String()) - // It is not possible to load anything that is not canonical... - if !reference.IsCanonical() { - return nil, errors.New(formatErrorDescription( - Locale.ReferenceMustBeCanonical(), - ErrorDetails{"reference": reference}, - )) - } + // First check if the given fragment is a location independent identifier + // http://json-schema.org/latest/json-schema-core.html#rfc.section.8.2.3 - refToUrl := reference - refToUrl.GetUrl().Fragment = "" - - var spd *schemaPoolDocument - - // Try to find the requested document in the pool - for k := range p.schemaPoolDocuments { - if k == refToUrl.String() { - spd = p.schemaPoolDocuments[k] - } - } - - if spd != nil { + if spd, ok = p.schemaPoolDocuments[refToUrl.String()]; ok { if internalLogEnabled { internalLog(" From pool") } return spd, nil } - jsonReferenceLoader := NewReferenceLoader(reference.String()) - document, err := jsonReferenceLoader.loadJSON() + // If the given reference is not a location independent identifier, + // strip the fragment and look for a document with it's base URI + + refToUrl.GetUrl().Fragment = "" + + if cachedSpd, ok := p.schemaPoolDocuments[refToUrl.String()]; ok { + document, _, err := reference.GetPointer().Get(cachedSpd.Document) + + if err != nil { + return nil, err + } + + if internalLogEnabled { + internalLog(" From pool") + } + + spd = &schemaPoolDocument{Document: document, Draft: cachedSpd.Draft} + p.schemaPoolDocuments[reference.String()] = spd + + return spd, nil + } + + // It is not possible to load anything remotely that is not canonical... + if !reference.IsCanonical() { + return nil, errors.New(formatErrorDescription( + Locale.ReferenceMustBeCanonical(), + ErrorDetails{"reference": reference.String()}, + )) + } + + jsonReferenceLoader := p.jsonLoaderFactory.New(reference.String()) + document, err := jsonReferenceLoader.LoadJSON() + if err != nil { return nil, err } - spd = &schemaPoolDocument{Document: document} - // add the document to the pool for potential later use - p.schemaPoolDocuments[refToUrl.String()] = spd + // add the whole document to the pool for potential re-use + p.parseReferences(document, refToUrl, true) - return spd, nil + _, draft, _ = parseSchemaURL(document) + + // resolve the potential fragment and also cache it + document, _, err = reference.GetPointer().Get(document) + + if err != nil { + return nil, err + } + + return &schemaPoolDocument{Document: document, Draft: draft}, nil } diff --git a/vendor/github.com/xeipuuv/gojsonschema/schemaReferencePool.go b/vendor/github.com/xeipuuv/gojsonschema/schemaReferencePool.go index 294e36a732..6e5e1b5cdb 100644 --- a/vendor/github.com/xeipuuv/gojsonschema/schemaReferencePool.go +++ b/vendor/github.com/xeipuuv/gojsonschema/schemaReferencePool.go @@ -62,6 +62,7 @@ func (p *schemaReferencePool) Add(ref string, sch *subSchema) { if internalLogEnabled { internalLog(fmt.Sprintf("Add Schema Reference %s to pool", ref)) } - - p.documents[ref] = sch + if _, ok := p.documents[ref]; !ok { + p.documents[ref] = sch + } } diff --git a/vendor/github.com/xeipuuv/gojsonschema/schemaType.go b/vendor/github.com/xeipuuv/gojsonschema/schemaType.go index e13a0fb0cb..36b447a291 100644 --- a/vendor/github.com/xeipuuv/gojsonschema/schemaType.go +++ b/vendor/github.com/xeipuuv/gojsonschema/schemaType.go @@ -44,7 +44,7 @@ func (t *jsonSchemaType) IsTyped() bool { func (t *jsonSchemaType) Add(etype string) error { if !isStringInSlice(JSON_TYPES, etype) { - return errors.New(formatErrorDescription(Locale.NotAValidType(), ErrorDetails{"type": etype})) + return errors.New(formatErrorDescription(Locale.NotAValidType(), ErrorDetails{"given": "/" + etype + "/", "expected": JSON_TYPES})) } if t.Contains(etype) { diff --git a/vendor/github.com/xeipuuv/gojsonschema/subSchema.go b/vendor/github.com/xeipuuv/gojsonschema/subSchema.go index b249b7e550..362d86ca95 100644 --- a/vendor/github.com/xeipuuv/gojsonschema/subSchema.go +++ b/vendor/github.com/xeipuuv/gojsonschema/subSchema.go @@ -28,6 +28,7 @@ package gojsonschema import ( "errors" + "math/big" "regexp" "strings" @@ -35,8 +36,9 @@ import ( ) const ( - KEY_SCHEMA = "$subSchema" - KEY_ID = "$id" + KEY_SCHEMA = "$schema" + KEY_ID = "id" + KEY_ID_NEW = "$id" KEY_REF = "$ref" KEY_TITLE = "title" KEY_DESCRIPTION = "description" @@ -46,6 +48,7 @@ const ( KEY_PROPERTIES = "properties" KEY_PATTERN_PROPERTIES = "patternProperties" KEY_ADDITIONAL_PROPERTIES = "additionalProperties" + KEY_PROPERTY_NAMES = "propertyNames" KEY_DEFINITIONS = "definitions" KEY_MULTIPLE_OF = "multipleOf" KEY_MINIMUM = "minimum" @@ -63,17 +66,23 @@ const ( KEY_MIN_ITEMS = "minItems" KEY_MAX_ITEMS = "maxItems" KEY_UNIQUE_ITEMS = "uniqueItems" + KEY_CONTAINS = "contains" + KEY_CONST = "const" KEY_ENUM = "enum" KEY_ONE_OF = "oneOf" KEY_ANY_OF = "anyOf" KEY_ALL_OF = "allOf" KEY_NOT = "not" + KEY_IF = "if" + KEY_THEN = "then" + KEY_ELSE = "else" ) type subSchema struct { + draft *Draft // basic subSchema meta properties - id *string + id *gojsonreference.JsonReference title *string description *string @@ -86,23 +95,19 @@ type subSchema struct { ref *gojsonreference.JsonReference // Schema referenced refSchema *subSchema - // Json reference - subSchema *gojsonreference.JsonReference // hierarchy parent *subSchema - definitions map[string]*subSchema - definitionsChildren []*subSchema itemsChildren []*subSchema itemsChildrenIsSingleSchema bool propertiesChildren []*subSchema // validation : number / integer - multipleOf *float64 - maximum *float64 - exclusiveMaximum bool - minimum *float64 - exclusiveMinimum bool + multipleOf *big.Rat + maximum *big.Rat + exclusiveMaximum *big.Rat + minimum *big.Rat + exclusiveMinimum *big.Rat // validation : string minLength *int @@ -118,27 +123,43 @@ type subSchema struct { dependencies map[string]interface{} additionalProperties interface{} patternProperties map[string]*subSchema + propertyNames *subSchema // validation : array minItems *int maxItems *int uniqueItems bool + contains *subSchema additionalItems interface{} // validation : all - enum []string + _const *string //const is a golang keyword + enum []string // validation : subSchema oneOf []*subSchema anyOf []*subSchema allOf []*subSchema not *subSchema + _if *subSchema // if/else are golang keywords + _then *subSchema + _else *subSchema +} + +func (s *subSchema) AddConst(i interface{}) error { + + is, err := marshalWithoutNumber(i) + if err != nil { + return err + } + s._const = is + return nil } func (s *subSchema) AddEnum(i interface{}) error { - is, err := marshalToJsonString(i) + is, err := marshalWithoutNumber(i) if err != nil { return err } @@ -157,7 +178,7 @@ func (s *subSchema) AddEnum(i interface{}) error { func (s *subSchema) ContainsEnum(i interface{}) (bool, error) { - is, err := marshalToJsonString(i) + is, err := marshalWithoutNumber(i) if err != nil { return false, err } @@ -181,6 +202,18 @@ func (s *subSchema) SetNot(subSchema *subSchema) { s.not = subSchema } +func (s *subSchema) SetIf(subSchema *subSchema) { + s._if = subSchema +} + +func (s *subSchema) SetThen(subSchema *subSchema) { + s._then = subSchema +} + +func (s *subSchema) SetElse(subSchema *subSchema) { + s._else = subSchema +} + func (s *subSchema) AddRequired(value string) error { if isStringInSlice(s.required, value) { @@ -195,10 +228,6 @@ func (s *subSchema) AddRequired(value string) error { return nil } -func (s *subSchema) AddDefinitionChild(child *subSchema) { - s.definitionsChildren = append(s.definitionsChildren, child) -} - func (s *subSchema) AddItemsChild(child *subSchema) { s.itemsChildren = append(s.itemsChildren, child) } @@ -214,7 +243,7 @@ func (s *subSchema) PatternPropertiesString() string { } patternPropertiesKeySlice := []string{} - for pk, _ := range s.patternProperties { + for pk := range s.patternProperties { patternPropertiesKeySlice = append(patternPropertiesKeySlice, `"`+pk+`"`) } diff --git a/vendor/github.com/xeipuuv/gojsonschema/utils.go b/vendor/github.com/xeipuuv/gojsonschema/utils.go index cfeb628eb5..88d223fbf8 100644 --- a/vendor/github.com/xeipuuv/gojsonschema/utils.go +++ b/vendor/github.com/xeipuuv/gojsonschema/utils.go @@ -29,12 +29,23 @@ import ( "encoding/json" "fmt" "math" + "math/big" "reflect" - "strconv" ) -func isKind(what interface{}, kind reflect.Kind) bool { - return reflect.ValueOf(what).Kind() == kind +func isKind(what interface{}, kinds ...reflect.Kind) bool { + target := what + if isJsonNumber(what) { + // JSON Numbers are strings! + target = *mustBeNumber(what) + } + targetKind := reflect.ValueOf(target).Kind() + for _, kind := range kinds { + if targetKind == kind { + return true + } + } + return false } func existsMapKey(m map[string]interface{}, k string) bool { @@ -51,6 +62,16 @@ func isStringInSlice(s []string, what string) bool { return false } +// indexStringInSlice returns the index of the first instance of 'what' in s or -1 if it is not found in s. +func indexStringInSlice(s []string, what string) int { + for i := range s { + if s[i] == what { + return i + } + } + return -1 +} + func marshalToJsonString(value interface{}) (*string, error) { mBytes, err := json.Marshal(value) @@ -62,6 +83,28 @@ func marshalToJsonString(value interface{}) (*string, error) { return &sBytes, nil } +func marshalWithoutNumber(value interface{}) (*string, error) { + + // The JSON is decoded using https://golang.org/pkg/encoding/json/#Decoder.UseNumber + // This means the numbers are internally still represented as strings and therefore 1.00 is unequal to 1 + // One way to eliminate these differences is to decode and encode the JSON one more time without Decoder.UseNumber + // so that these differences in representation are removed + + jsonString, err := marshalToJsonString(value) + if err != nil { + return nil, err + } + + var document interface{} + + err = json.Unmarshal([]byte(*jsonString), &document) + if err != nil { + return nil, err + } + + return marshalToJsonString(document) +} + func isJsonNumber(what interface{}) bool { switch what.(type) { @@ -73,20 +116,13 @@ func isJsonNumber(what interface{}) bool { return false } -func checkJsonNumber(what interface{}) (isValidFloat64 bool, isValidInt64 bool, isValidInt32 bool) { +func checkJsonInteger(what interface{}) (isInt bool) { jsonNumber := what.(json.Number) - _, errFloat64 := jsonNumber.Float64() - _, errInt64 := jsonNumber.Int64() + bigFloat, isValidNumber := new(big.Rat).SetString(string(jsonNumber)) - isValidFloat64 = errFloat64 == nil - isValidInt64 = errInt64 == nil - - _, errInt32 := strconv.ParseInt(jsonNumber.String(), 10, 32) - isValidInt32 = isValidInt64 && errInt32 == nil - - return + return isValidNumber && bigFloat.IsInt() } @@ -111,9 +147,9 @@ func mustBeInteger(what interface{}) *int { number := what.(json.Number) - _, _, isValidInt32 := checkJsonNumber(number) + isInt := checkJsonInteger(number) - if isValidInt32 { + if isInt { int64Value, err := number.Int64() if err != nil { @@ -132,15 +168,13 @@ func mustBeInteger(what interface{}) *int { return nil } -func mustBeNumber(what interface{}) *float64 { +func mustBeNumber(what interface{}) *big.Rat { if isJsonNumber(what) { - number := what.(json.Number) - float64Value, err := number.Float64() - - if err == nil { - return &float64Value + float64Value, success := new(big.Rat).SetString(string(number)) + if success { + return float64Value } else { return nil } diff --git a/vendor/github.com/xeipuuv/gojsonschema/validation.go b/vendor/github.com/xeipuuv/gojsonschema/validation.go index 2dc0df2275..090c11e932 100644 --- a/vendor/github.com/xeipuuv/gojsonschema/validation.go +++ b/vendor/github.com/xeipuuv/gojsonschema/validation.go @@ -27,6 +27,7 @@ package gojsonschema import ( "encoding/json" + "math/big" "reflect" "regexp" "strconv" @@ -55,29 +56,32 @@ func (v *Schema) Validate(l JSONLoader) (*Result, error) { // load document - root, err := l.loadJSON() + root, err := l.LoadJSON() if err != nil { return nil, err } + return v.validateDocument(root), nil +} + +func (v *Schema) validateDocument(root interface{}) *Result { // begin validation result := &Result{} - context := newJsonContext(STRING_CONTEXT_ROOT, nil) + context := NewJsonContext(STRING_CONTEXT_ROOT, nil) v.rootSchema.validateRecursive(v.rootSchema, root, result, context) - return result, nil - + return result } -func (v *subSchema) subValidateWithContext(document interface{}, context *jsonContext) *Result { +func (v *subSchema) subValidateWithContext(document interface{}, context *JsonContext) *Result { result := &Result{} v.validateRecursive(v, document, result, context) return result } // Walker function to validate the json recursively against the subSchema -func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode interface{}, result *Result, context *jsonContext) { +func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode interface{}, result *Result, context *JsonContext) { if internalLogEnabled { internalLog("validateRecursive %s", context.String()) @@ -93,7 +97,7 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i // Check for null value if currentNode == nil { if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_NULL) { - result.addError( + result.addInternalError( new(InvalidTypeError), context, currentNode, @@ -114,18 +118,18 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i value := currentNode.(json.Number) - _, isValidInt64, _ := checkJsonNumber(value) + isInt := checkJsonInteger(value) - validType := currentSubSchema.types.Contains(TYPE_NUMBER) || (isValidInt64 && currentSubSchema.types.Contains(TYPE_INTEGER)) + validType := currentSubSchema.types.Contains(TYPE_NUMBER) || (isInt && currentSubSchema.types.Contains(TYPE_INTEGER)) if currentSubSchema.types.IsTyped() && !validType { givenType := TYPE_INTEGER - if !isValidInt64 { + if !isInt { givenType = TYPE_NUMBER } - result.addError( + result.addInternalError( new(InvalidTypeError), context, currentNode, @@ -154,7 +158,7 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i case reflect.Slice: if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_ARRAY) { - result.addError( + result.addInternalError( new(InvalidTypeError), context, currentNode, @@ -177,7 +181,7 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i case reflect.Map: if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_OBJECT) { - result.addError( + result.addInternalError( new(InvalidTypeError), context, currentNode, @@ -202,7 +206,7 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i for _, pSchema := range currentSubSchema.propertiesChildren { nextNode, ok := castCurrentNode[pSchema.property] if ok { - subContext := newJsonContext(pSchema.property, context) + subContext := NewJsonContext(pSchema.property, context) v.validateRecursive(pSchema, nextNode, result, subContext) } } @@ -212,7 +216,7 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i case reflect.Bool: if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_BOOLEAN) { - result.addError( + result.addInternalError( new(InvalidTypeError), context, currentNode, @@ -234,7 +238,7 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i case reflect.String: if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_STRING) { - result.addError( + result.addInternalError( new(InvalidTypeError), context, currentNode, @@ -263,7 +267,7 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i } // Different kinds of validation there, subSchema / common / array / object / string... -func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode interface{}, result *Result, context *jsonContext) { +func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode interface{}, result *Result, context *JsonContext) { if internalLogEnabled { internalLog("validateSchema %s", context.String()) @@ -287,7 +291,7 @@ func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode inte } if !validatedAnyOf { - result.addError(new(NumberAnyOfError), context, currentNode, ErrorDetails{}) + result.addInternalError(new(NumberAnyOfError), context, currentNode, ErrorDetails{}) if bestValidationResult != nil { // add error messages of closest matching subSchema as @@ -313,7 +317,7 @@ func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode inte if nbValidated != 1 { - result.addError(new(NumberOneOfError), context, currentNode, ErrorDetails{}) + result.addInternalError(new(NumberOneOfError), context, currentNode, ErrorDetails{}) if nbValidated == 0 { // add error messages of closest matching subSchema as @@ -336,14 +340,14 @@ func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode inte } if nbValidated != len(currentSubSchema.allOf) { - result.addError(new(NumberAllOfError), context, currentNode, ErrorDetails{}) + result.addInternalError(new(NumberAllOfError), context, currentNode, ErrorDetails{}) } } if currentSubSchema.not != nil { validationResult := currentSubSchema.not.subValidateWithContext(currentNode, context) if validationResult.Valid() { - result.addError(new(NumberNotError), context, currentNode, ErrorDetails{}) + result.addInternalError(new(NumberNotError), context, currentNode, ErrorDetails{}) } } @@ -356,7 +360,7 @@ func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode inte case []string: for _, dependOnKey := range dependency { if _, dependencyResolved := currentNode.(map[string]interface{})[dependOnKey]; !dependencyResolved { - result.addError( + result.addInternalError( new(MissingDependencyError), context, currentNode, @@ -367,31 +371,65 @@ func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode inte case *subSchema: dependency.validateRecursive(dependency, currentNode, result, context) - } } } } } + if currentSubSchema._if != nil { + validationResultIf := currentSubSchema._if.subValidateWithContext(currentNode, context) + if currentSubSchema._then != nil && validationResultIf.Valid() { + validationResultThen := currentSubSchema._then.subValidateWithContext(currentNode, context) + if !validationResultThen.Valid() { + result.addInternalError(new(ConditionThenError), context, currentNode, ErrorDetails{}) + result.mergeErrors(validationResultThen) + } + } + if currentSubSchema._else != nil && !validationResultIf.Valid() { + validationResultElse := currentSubSchema._else.subValidateWithContext(currentNode, context) + if !validationResultElse.Valid() { + result.addInternalError(new(ConditionElseError), context, currentNode, ErrorDetails{}) + result.mergeErrors(validationResultElse) + } + } + } + result.incrementScore() } -func (v *subSchema) validateCommon(currentSubSchema *subSchema, value interface{}, result *Result, context *jsonContext) { +func (v *subSchema) validateCommon(currentSubSchema *subSchema, value interface{}, result *Result, context *JsonContext) { if internalLogEnabled { internalLog("validateCommon %s", context.String()) internalLog(" %v", value) } + // const: + if currentSubSchema._const != nil { + vString, err := marshalWithoutNumber(value) + if err != nil { + result.addInternalError(new(InternalError), context, value, ErrorDetails{"error": err}) + } + if *vString != *currentSubSchema._const { + result.addInternalError(new(ConstError), + context, + value, + ErrorDetails{ + "allowed": *currentSubSchema._const, + }, + ) + } + } + // enum: if len(currentSubSchema.enum) > 0 { has, err := currentSubSchema.ContainsEnum(value) if err != nil { - result.addError(new(InternalError), context, value, ErrorDetails{"error": err}) + result.addInternalError(new(InternalError), context, value, ErrorDetails{"error": err}) } if !has { - result.addError( + result.addInternalError( new(EnumError), context, value, @@ -405,19 +443,19 @@ func (v *subSchema) validateCommon(currentSubSchema *subSchema, value interface{ result.incrementScore() } -func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface{}, result *Result, context *jsonContext) { +func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface{}, result *Result, context *JsonContext) { if internalLogEnabled { internalLog("validateArray %s", context.String()) internalLog(" %v", value) } - nbItems := len(value) + nbValues := len(value) // TODO explain if currentSubSchema.itemsChildrenIsSingleSchema { for i := range value { - subContext := newJsonContext(strconv.Itoa(i), context) + subContext := NewJsonContext(strconv.Itoa(i), context) validationResult := currentSubSchema.itemsChildren[0].subValidateWithContext(value[i], subContext) result.mergeErrors(validationResult) } @@ -425,24 +463,27 @@ func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface if currentSubSchema.itemsChildren != nil && len(currentSubSchema.itemsChildren) > 0 { nbItems := len(currentSubSchema.itemsChildren) - nbValues := len(value) - if nbItems == nbValues { - for i := 0; i != nbItems; i++ { - subContext := newJsonContext(strconv.Itoa(i), context) - validationResult := currentSubSchema.itemsChildren[i].subValidateWithContext(value[i], subContext) - result.mergeErrors(validationResult) - } - } else if nbItems < nbValues { + // while we have both schemas and values, check them against each other + for i := 0; i != nbItems && i != nbValues; i++ { + subContext := NewJsonContext(strconv.Itoa(i), context) + validationResult := currentSubSchema.itemsChildren[i].subValidateWithContext(value[i], subContext) + result.mergeErrors(validationResult) + } + + if nbItems < nbValues { + // we have less schemas than elements in the instance array, + // but that might be ok if "additionalItems" is specified. + switch currentSubSchema.additionalItems.(type) { case bool: if !currentSubSchema.additionalItems.(bool) { - result.addError(new(ArrayNoAdditionalItemsError), context, value, ErrorDetails{}) + result.addInternalError(new(ArrayNoAdditionalItemsError), context, value, ErrorDetails{}) } case *subSchema: additionalItemSchema := currentSubSchema.additionalItems.(*subSchema) for i := nbItems; i != nbValues; i++ { - subContext := newJsonContext(strconv.Itoa(i), context) + subContext := NewJsonContext(strconv.Itoa(i), context) validationResult := additionalItemSchema.subValidateWithContext(value[i], subContext) result.mergeErrors(validationResult) } @@ -453,8 +494,8 @@ func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface // minItems & maxItems if currentSubSchema.minItems != nil { - if nbItems < int(*currentSubSchema.minItems) { - result.addError( + if nbValues < int(*currentSubSchema.minItems) { + result.addInternalError( new(ArrayMinItemsError), context, value, @@ -463,8 +504,8 @@ func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface } } if currentSubSchema.maxItems != nil { - if nbItems > int(*currentSubSchema.maxItems) { - result.addError( + if nbValues > int(*currentSubSchema.maxItems) { + result.addInternalError( new(ArrayMaxItemsError), context, value, @@ -476,27 +517,59 @@ func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface // uniqueItems: if currentSubSchema.uniqueItems { var stringifiedItems []string - for _, v := range value { - vString, err := marshalToJsonString(v) + for j, v := range value { + vString, err := marshalWithoutNumber(v) if err != nil { - result.addError(new(InternalError), context, value, ErrorDetails{"err": err}) + result.addInternalError(new(InternalError), context, value, ErrorDetails{"err": err}) } - if isStringInSlice(stringifiedItems, *vString) { - result.addError( + if i := indexStringInSlice(stringifiedItems, *vString); i > -1 { + result.addInternalError( new(ItemsMustBeUniqueError), context, value, - ErrorDetails{"type": TYPE_ARRAY}, + ErrorDetails{"type": TYPE_ARRAY, "i": i, "j": j}, ) } stringifiedItems = append(stringifiedItems, *vString) } } + // contains: + + if currentSubSchema.contains != nil { + validatedOne := false + var bestValidationResult *Result + + for i, v := range value { + subContext := NewJsonContext(strconv.Itoa(i), context) + + validationResult := currentSubSchema.contains.subValidateWithContext(v, subContext) + if validationResult.Valid() { + validatedOne = true + break + } else { + if bestValidationResult == nil || validationResult.score > bestValidationResult.score { + bestValidationResult = validationResult + } + } + } + if !validatedOne { + result.addInternalError( + new(ArrayContainsError), + context, + value, + ErrorDetails{}, + ) + if bestValidationResult != nil { + result.mergeErrors(bestValidationResult) + } + } + } + result.incrementScore() } -func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string]interface{}, result *Result, context *jsonContext) { +func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string]interface{}, result *Result, context *JsonContext) { if internalLogEnabled { internalLog("validateObject %s", context.String()) @@ -506,7 +579,7 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string // minProperties & maxProperties: if currentSubSchema.minProperties != nil { if len(value) < int(*currentSubSchema.minProperties) { - result.addError( + result.addInternalError( new(ArrayMinPropertiesError), context, value, @@ -516,7 +589,7 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string } if currentSubSchema.maxProperties != nil { if len(value) > int(*currentSubSchema.maxProperties) { - result.addError( + result.addInternalError( new(ArrayMaxPropertiesError), context, value, @@ -531,7 +604,7 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string if ok { result.incrementScore() } else { - result.addError( + result.addInternalError( new(RequiredError), context, value, @@ -562,10 +635,10 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string if found { if pp_has && !pp_match { - result.addError( + result.addInternalError( new(AdditionalPropertyNotAllowedError), context, - value, + value[pk], ErrorDetails{"property": pk}, ) } @@ -573,10 +646,10 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string } else { if !pp_has || !pp_match { - result.addError( + result.addInternalError( new(AdditionalPropertyNotAllowedError), context, - value, + value[pk], ErrorDetails{"property": pk}, ) } @@ -625,10 +698,10 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string if pp_has && !pp_match { - result.addError( + result.addInternalError( new(InvalidPropertyPatternError), context, - value, + value[pk], ErrorDetails{ "property": pk, "pattern": currentSubSchema.PatternPropertiesString(), @@ -639,10 +712,25 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string } } + // propertyNames: + if currentSubSchema.propertyNames != nil { + for pk := range value { + validationResult := currentSubSchema.propertyNames.subValidateWithContext(pk, context) + if !validationResult.Valid() { + result.addInternalError(new(InvalidPropertyNameError), + context, + value, ErrorDetails{ + "property": pk, + }) + result.mergeErrors(validationResult) + } + } + } + result.incrementScore() } -func (v *subSchema) validatePatternProperty(currentSubSchema *subSchema, key string, value interface{}, result *Result, context *jsonContext) (has bool, matched bool) { +func (v *subSchema) validatePatternProperty(currentSubSchema *subSchema, key string, value interface{}, result *Result, context *JsonContext) (has bool, matched bool) { if internalLogEnabled { internalLog("validatePatternProperty %s", context.String()) @@ -656,12 +744,10 @@ func (v *subSchema) validatePatternProperty(currentSubSchema *subSchema, key str for pk, pv := range currentSubSchema.patternProperties { if matches, _ := regexp.MatchString(pk, key); matches { has = true - subContext := newJsonContext(key, context) + subContext := NewJsonContext(key, context) validationResult := pv.subValidateWithContext(value, subContext) result.mergeErrors(validationResult) - if validationResult.Valid() { - validatedkey = true - } + validatedkey = true } } @@ -674,7 +760,7 @@ func (v *subSchema) validatePatternProperty(currentSubSchema *subSchema, key str return has, true } -func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{}, result *Result, context *jsonContext) { +func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{}, result *Result, context *JsonContext) { // Ignore JSON numbers if isJsonNumber(value) { @@ -696,7 +782,7 @@ func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{ // minLength & maxLength: if currentSubSchema.minLength != nil { if utf8.RuneCount([]byte(stringValue)) < int(*currentSubSchema.minLength) { - result.addError( + result.addInternalError( new(StringLengthGTEError), context, value, @@ -706,7 +792,7 @@ func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{ } if currentSubSchema.maxLength != nil { if utf8.RuneCount([]byte(stringValue)) > int(*currentSubSchema.maxLength) { - result.addError( + result.addInternalError( new(StringLengthLTEError), context, value, @@ -718,7 +804,7 @@ func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{ // pattern: if currentSubSchema.pattern != nil { if !currentSubSchema.pattern.MatchString(stringValue) { - result.addError( + result.addInternalError( new(DoesNotMatchPatternError), context, value, @@ -731,7 +817,7 @@ func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{ // format if currentSubSchema.format != "" { if !FormatCheckers.IsFormat(currentSubSchema.format, stringValue) { - result.addError( + result.addInternalError( new(DoesNotMatchFormatError), context, value, @@ -743,7 +829,7 @@ func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{ result.incrementScore() } -func (v *subSchema) validateNumber(currentSubSchema *subSchema, value interface{}, result *Result, context *jsonContext) { +func (v *subSchema) validateNumber(currentSubSchema *subSchema, value interface{}, result *Result, context *JsonContext) { // Ignore non numbers if !isJsonNumber(value) { @@ -756,72 +842,82 @@ func (v *subSchema) validateNumber(currentSubSchema *subSchema, value interface{ } number := value.(json.Number) - float64Value, _ := number.Float64() + float64Value, _ := new(big.Rat).SetString(string(number)) // multipleOf: if currentSubSchema.multipleOf != nil { - - if !isFloat64AnInteger(float64Value / *currentSubSchema.multipleOf) { - result.addError( + if q := new(big.Rat).Quo(float64Value, currentSubSchema.multipleOf); !q.IsInt() { + result.addInternalError( new(MultipleOfError), context, resultErrorFormatJsonNumber(number), - ErrorDetails{"multiple": *currentSubSchema.multipleOf}, + ErrorDetails{"multiple": new(big.Float).SetRat(currentSubSchema.multipleOf)}, ) } } //maximum & exclusiveMaximum: if currentSubSchema.maximum != nil { - if currentSubSchema.exclusiveMaximum { - if float64Value >= *currentSubSchema.maximum { - result.addError( - new(NumberLTError), - context, - resultErrorFormatJsonNumber(number), - ErrorDetails{ - "max": resultErrorFormatNumber(*currentSubSchema.maximum), - }, - ) - } - } else { - if float64Value > *currentSubSchema.maximum { - result.addError( - new(NumberLTEError), - context, - resultErrorFormatJsonNumber(number), - ErrorDetails{ - "max": resultErrorFormatNumber(*currentSubSchema.maximum), - }, - ) - } + if float64Value.Cmp(currentSubSchema.maximum) == 1 { + result.addInternalError( + new(NumberLTEError), + context, + resultErrorFormatJsonNumber(number), + ErrorDetails{ + "max": currentSubSchema.maximum, + }, + ) + } + } + if currentSubSchema.exclusiveMaximum != nil { + if float64Value.Cmp(currentSubSchema.exclusiveMaximum) >= 0 { + result.addInternalError( + new(NumberLTError), + context, + resultErrorFormatJsonNumber(number), + ErrorDetails{ + "max": currentSubSchema.exclusiveMaximum, + }, + ) } } //minimum & exclusiveMinimum: if currentSubSchema.minimum != nil { - if currentSubSchema.exclusiveMinimum { - if float64Value <= *currentSubSchema.minimum { - result.addError( - new(NumberGTError), - context, - resultErrorFormatJsonNumber(number), - ErrorDetails{ - "min": resultErrorFormatNumber(*currentSubSchema.minimum), - }, - ) - } - } else { - if float64Value < *currentSubSchema.minimum { - result.addError( - new(NumberGTEError), - context, - resultErrorFormatJsonNumber(number), - ErrorDetails{ - "min": resultErrorFormatNumber(*currentSubSchema.minimum), - }, - ) - } + if float64Value.Cmp(currentSubSchema.minimum) == -1 { + result.addInternalError( + new(NumberGTEError), + context, + resultErrorFormatJsonNumber(number), + ErrorDetails{ + "min": currentSubSchema.minimum, + }, + ) + } + } + if currentSubSchema.exclusiveMinimum != nil { + if float64Value.Cmp(currentSubSchema.exclusiveMinimum) <= 0 { + // if float64Value <= *currentSubSchema.minimum { + result.addInternalError( + new(NumberGTError), + context, + resultErrorFormatJsonNumber(number), + ErrorDetails{ + "min": currentSubSchema.exclusiveMinimum, + }, + ) + } + } + + // format + if currentSubSchema.format != "" { + if !FormatCheckers.IsFormat(currentSubSchema.format, float64Value) { + result.addInternalError( + new(DoesNotMatchFormatError), + context, + value, + ErrorDetails{"format": currentSubSchema.format}, + ) } }