Merge pull request #1827 from thaJeztah/bump_go_json_schema_1.1.0

bump github.com/xeipuuv/gojsonschema v1.1.0
This commit is contained in:
Kirill Kolyshkin 2019-10-10 17:16:49 -07:00 committed by GitHub
commit 83d0c5df4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1939 additions and 569 deletions

View File

@ -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
}

View File

@ -76,9 +76,9 @@ github.com/syndtr/gocapability d98352740cb2c55f81556b63d4a1
github.com/theupdateframework/notary d6e1431feb32348e0650bf7551ac5cffd01d857b # v0.6.1
github.com/tonistiigi/fsutil 7f9f9232dd24c4c9c68ab3c8030c4edcaeac1c32
github.com/tonistiigi/units 6950e57a87eaf136bbe44ef2ec8e75b9e3569de2
github.com/xeipuuv/gojsonpointer 4e3ac2762d5f479393488629ee9370b50873b3a6
github.com/xeipuuv/gojsonpointer 02993c407bfbf5f6dae44c4f4b1cf6a39b5fc5bb
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

View File

@ -35,7 +35,7 @@ An implementation of JSON Pointer - Go language
## References
http://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-07
https://tools.ietf.org/html/rfc6901
### Note
The 4.Evaluation part of the previous reference, starting with 'If the currently referenced value is a JSON array, the reference token MUST contain either...' is not implemented.

View File

@ -130,10 +130,10 @@ func (p *JsonPointer) implementation(i *implStruct) {
node = v[decodedToken]
if isLastToken && i.mode == "SET" {
v[decodedToken] = i.setInValue
} else if isLastToken && i.mode =="DEL" {
delete(v,decodedToken)
} else if isLastToken && i.mode == "DEL" {
delete(v, decodedToken)
}
} else if (isLastToken && i.mode == "SET") {
} else if isLastToken && i.mode == "SET" {
v[decodedToken] = i.setInValue
} else {
i.outError = fmt.Errorf("Object has no key '%s'", decodedToken)
@ -160,7 +160,7 @@ func (p *JsonPointer) implementation(i *implStruct) {
node = v[tokenIndex]
if isLastToken && i.mode == "SET" {
v[tokenIndex] = i.setInValue
} else if isLastToken && i.mode =="DEL" {
} else if isLastToken && i.mode == "DEL" {
v[tokenIndex] = v[len(v)-1]
v[len(v)-1] = nil
v = v[:len(v)-1]

View File

@ -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 :

118
vendor/github.com/xeipuuv/gojsonschema/draft.go generated vendored Normal file
View File

@ -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
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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) {

View File

@ -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"

View File

@ -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

View File

@ -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 {

203
vendor/github.com/xeipuuv/gojsonschema/schemaLoader.go generated vendored Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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+`"`)
}

View File

@ -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
}

View File

@ -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},
)
}
}