bump github.com/xeipuuv/gojsonschema v1.1.0

full diff: 93e72a773f...f971f3cd73

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2019-04-13 02:56:09 +02:00
parent 3e07fa728a
commit 06f34ba507
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
18 changed files with 1933 additions and 563 deletions

View File

@ -18,15 +18,19 @@ const (
type portsFormatChecker struct{} type portsFormatChecker struct{}
func (checker portsFormatChecker) IsFormat(input string) bool { func (checker portsFormatChecker) IsFormat(input interface{}) bool {
// TODO: implement this // TODO: implement this
return true return true
} }
type durationFormatChecker struct{} type durationFormatChecker struct{}
func (checker durationFormatChecker) IsFormat(input string) bool { func (checker durationFormatChecker) IsFormat(input interface{}) bool {
_, err := time.ParseDuration(input) value, ok := input.(string)
if !ok {
return false
}
_, err := time.ParseDuration(value)
return err == nil return err == nil
} }

View File

@ -78,7 +78,7 @@ github.com/tonistiigi/fsutil 7f9f9232dd24c4c9c68ab3c8030c
github.com/tonistiigi/units 6950e57a87eaf136bbe44ef2ec8e75b9e3569de2 github.com/tonistiigi/units 6950e57a87eaf136bbe44ef2ec8e75b9e3569de2
github.com/xeipuuv/gojsonpointer 4e3ac2762d5f479393488629ee9370b50873b3a6 github.com/xeipuuv/gojsonpointer 4e3ac2762d5f479393488629ee9370b50873b3a6
github.com/xeipuuv/gojsonreference bd5ef7bd5415a7ac448318e64f11a24cd21e594b 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/crypto 88737f569e3a9c7ab309cdc09a07fe7fc87233c3
golang.org/x/net f3200d17e092c607f615320ecaad13d87ad9a2b3 golang.org/x/net f3200d17e092c607f615320ecaad13d87ad9a2b3
golang.org/x/oauth2 ef147856a6ddbb60760db74283d2424e98c87bff golang.org/x/oauth2 ef147856a6ddbb60760db74283d2424e98c87bff

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) [![Build Status](https://travis-ci.org/xeipuuv/gojsonschema.svg)](https://travis-ci.org/xeipuuv/gojsonschema)
# gojsonschema # gojsonschema
## Description ## 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 : References :
@ -54,7 +55,6 @@ func main() {
fmt.Printf("- %s\n", desc) 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 ## Working with Errors
The library handles string error codes which you can customize by creating your own gojsonschema.locale and setting it The library handles string error codes which you can customize by creating your own gojsonschema.locale and setting it
@ -157,6 +238,8 @@ 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 **err.Type()**: *string* Returns the "type" of error that occurred. Note you can also type check. See below
Note: An error of RequiredType has an err.Type() return value of "required" Note: An error of RequiredType has an err.Type() return value of "required"
@ -169,15 +252,18 @@ Note: An error of RequiredType has an err.Type() return value of "required"
"number_not": NumberNotError "number_not": NumberNotError
"missing_dependency": MissingDependencyError "missing_dependency": MissingDependencyError
"internal": InternalError "internal": InternalError
"const": ConstEror
"enum": EnumError "enum": EnumError
"array_no_additional_items": ArrayNoAdditionalItemsError "array_no_additional_items": ArrayNoAdditionalItemsError
"array_min_items": ArrayMinItemsError "array_min_items": ArrayMinItemsError
"array_max_items": ArrayMaxItemsError "array_max_items": ArrayMaxItemsError
"unique": ItemsMustBeUniqueError "unique": ItemsMustBeUniqueError
"contains" : ArrayContainsError
"array_min_properties": ArrayMinPropertiesError "array_min_properties": ArrayMinPropertiesError
"array_max_properties": ArrayMaxPropertiesError "array_max_properties": ArrayMaxPropertiesError
"additional_property_not_allowed": AdditionalPropertyNotAllowedError "additional_property_not_allowed": AdditionalPropertyNotAllowedError
"invalid_property_pattern": InvalidPropertyPatternError "invalid_property_pattern": InvalidPropertyPatternError
"invalid_property_name": InvalidPropertyNameError
"string_gte": StringLengthGTEError "string_gte": StringLengthGTEError
"string_lte": StringLengthLTEError "string_lte": StringLengthLTEError
"pattern": DoesNotMatchPatternError "pattern": DoesNotMatchPatternError
@ -186,28 +272,78 @@ Note: An error of RequiredType has an err.Type() return value of "required"
"number_gt": NumberGTError "number_gt": NumberGTError
"number_lte": NumberLTEError "number_lte": NumberLTEError
"number_lt": NumberLTError "number_lt": NumberLTError
"condition_then" : ConditionThenError
"condition_else" : ConditionElseError
**err.Value()**: *interface{}* Returns the value given **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.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.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()* **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 ## 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 ````json
{"type": "string", "format": "email"} {"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: 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 {} type RoleFormatChecker struct {}
// Ensure it meets the gojsonschema.FormatChecker interface // Ensure it meets the gojsonschema.FormatChecker interface
func (f RoleFormatChecker) IsFormat(input string) bool { func (f RoleFormatChecker) IsFormat(input interface{}) bool {
return strings.HasPrefix("ROLE_", input)
asString, ok := input.(string)
if ok == false {
return false
}
return strings.HasPrefix("ROLE_", asString)
} }
// Add it to the library // Add it to the library
@ -229,6 +371,93 @@ Now to use in your json schema:
{"type": "string", "format": "role"} {"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 ## Uses
gojsonschema uses the following test suite : 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 package gojsonschema
import ( import (
"fmt" "bytes"
"strings" "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 ( type (
// RequiredError. ErrorDetails: property string // RequiredError. ErrorDetails: property string
RequiredError struct { RequiredError struct {
@ -46,6 +56,11 @@ type (
ResultErrorFields ResultErrorFields
} }
// ConstError. ErrorDetails: allowed
ConstError struct {
ResultErrorFields
}
// EnumError. ErrorDetails: allowed // EnumError. ErrorDetails: allowed
EnumError struct { EnumError struct {
ResultErrorFields ResultErrorFields
@ -66,11 +81,16 @@ type (
ResultErrorFields ResultErrorFields
} }
// ItemsMustBeUniqueError. ErrorDetails: type // ItemsMustBeUniqueError. ErrorDetails: type, i, j
ItemsMustBeUniqueError struct { ItemsMustBeUniqueError struct {
ResultErrorFields ResultErrorFields
} }
// ArrayContainsError. ErrorDetails:
ArrayContainsError struct {
ResultErrorFields
}
// ArrayMinPropertiesError. ErrorDetails: min // ArrayMinPropertiesError. ErrorDetails: min
ArrayMinPropertiesError struct { ArrayMinPropertiesError struct {
ResultErrorFields ResultErrorFields
@ -91,6 +111,11 @@ type (
ResultErrorFields ResultErrorFields
} }
// InvalidPopertyNameError. ErrorDetails: property
InvalidPropertyNameError struct {
ResultErrorFields
}
// StringLengthGTEError. ErrorDetails: min // StringLengthGTEError. ErrorDetails: min
StringLengthGTEError struct { StringLengthGTEError struct {
ResultErrorFields ResultErrorFields
@ -135,10 +160,20 @@ type (
NumberLTError struct { NumberLTError struct {
ResultErrorFields 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 // 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 t string
var d string var d string
switch err.(type) { switch err.(type) {
@ -166,6 +201,9 @@ func newError(err ResultError, context *jsonContext, value interface{}, locale l
case *InternalError: case *InternalError:
t = "internal" t = "internal"
d = locale.Internal() d = locale.Internal()
case *ConstError:
t = "const"
d = locale.Const()
case *EnumError: case *EnumError:
t = "enum" t = "enum"
d = locale.Enum() d = locale.Enum()
@ -181,6 +219,9 @@ func newError(err ResultError, context *jsonContext, value interface{}, locale l
case *ItemsMustBeUniqueError: case *ItemsMustBeUniqueError:
t = "unique" t = "unique"
d = locale.Unique() d = locale.Unique()
case *ArrayContainsError:
t = "contains"
d = locale.ArrayContains()
case *ArrayMinPropertiesError: case *ArrayMinPropertiesError:
t = "array_min_properties" t = "array_min_properties"
d = locale.ArrayMinProperties() d = locale.ArrayMinProperties()
@ -193,6 +234,9 @@ func newError(err ResultError, context *jsonContext, value interface{}, locale l
case *InvalidPropertyPatternError: case *InvalidPropertyPatternError:
t = "invalid_property_pattern" t = "invalid_property_pattern"
d = locale.InvalidPropertyPattern() d = locale.InvalidPropertyPattern()
case *InvalidPropertyNameError:
t = "invalid_property_name"
d = locale.InvalidPropertyName()
case *StringLengthGTEError: case *StringLengthGTEError:
t = "string_gte" t = "string_gte"
d = locale.StringGTE() d = locale.StringGTE()
@ -220,23 +264,61 @@ func newError(err ResultError, context *jsonContext, value interface{}, locale l
case *NumberLTError: case *NumberLTError:
t = "number_lt" t = "number_lt"
d = locale.NumberLT() d = locale.NumberLT()
case *ConditionThenError:
t = "condition_then"
d = locale.ConditionThen()
case *ConditionElseError:
t = "condition_else"
d = locale.ConditionElse()
} }
err.SetType(t) err.SetType(t)
err.SetContext(context) err.SetContext(context)
err.SetValue(value) err.SetValue(value)
err.SetDetails(details) err.SetDetails(details)
err.SetDescriptionFormat(d)
details["field"] = err.Field() details["field"] = err.Field()
err.SetDescription(formatErrorDescription(d, details))
if _, exists := details["context"]; !exists && context != nil {
details["context"] = context.String()
} }
// formatErrorDescription takes a string in this format: %field% is required err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details))
// and converts it to a string with replacements. The fields come from }
// the ErrorDetails struct and vary for each type of error.
// 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 { func formatErrorDescription(s string, details ErrorDetails) string {
for name, val := range details {
s = strings.Replace(s, "%"+strings.ToLower(name)+"%", fmt.Sprintf("%v", val), -1) 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)
} }
return s 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 ( import (
"net" "net"
"net/mail"
"net/url" "net/url"
"reflect"
"regexp" "regexp"
"strings" "strings"
"sync"
"time" "time"
) )
type ( type (
// FormatChecker is the interface all formatters added to FormatCheckerChain must implement // FormatChecker is the interface all formatters added to FormatCheckerChain must implement
FormatChecker interface { FormatChecker interface {
IsFormat(input string) bool IsFormat(input interface{}) bool
} }
// FormatCheckerChain holds the formatters // FormatCheckerChain holds the formatters
@ -52,14 +53,33 @@ type (
// http://tools.ietf.org/html/rfc3339#section-5.6 // http://tools.ietf.org/html/rfc3339#section-5.6
DateTimeFormatChecker struct{} 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{} 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 validates a hostname is in the correct format
HostnameFormatChecker struct{} HostnameFormatChecker struct{}
// UUIDFormatChecker validates a UUID is in the correct format // UUIDFormatChecker validates a UUID is in the correct format
UUIDFormatChecker struct{} 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 ( var (
@ -67,43 +87,65 @@ var (
// so library users can add custom formatters // so library users can add custom formatters
FormatCheckers = FormatCheckerChain{ FormatCheckers = FormatCheckerChain{
formatters: map[string]FormatChecker{ formatters: map[string]FormatChecker{
"date": DateFormatChecker{},
"time": TimeFormatChecker{},
"date-time": DateTimeFormatChecker{}, "date-time": DateTimeFormatChecker{},
"hostname": HostnameFormatChecker{}, "hostname": HostnameFormatChecker{},
"email": EmailFormatChecker{}, "email": EmailFormatChecker{},
"idn-email": EmailFormatChecker{},
"ipv4": IPV4FormatChecker{}, "ipv4": IPV4FormatChecker{},
"ipv6": IPV6FormatChecker{}, "ipv6": IPV6FormatChecker{},
"uri": URIFormatChecker{}, "uri": URIFormatChecker{},
"uri-reference": URIReferenceFormatChecker{},
"iri": URIFormatChecker{},
"iri-reference": URIReferenceFormatChecker{},
"uri-template": URITemplateFormatChecker{},
"uuid": UUIDFormatChecker{}, "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 // 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]))*$`) 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}$") 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 // Add adds a FormatChecker to the FormatCheckerChain
// The name used will be the value used for the format key in your json schema // 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 { func (c *FormatCheckerChain) Add(name string, f FormatChecker) *FormatCheckerChain {
lock.Lock()
c.formatters[name] = f c.formatters[name] = f
lock.Unlock()
return c return c
} }
// Remove deletes a FormatChecker from the FormatCheckerChain (if it exists) // Remove deletes a FormatChecker from the FormatCheckerChain (if it exists)
func (c *FormatCheckerChain) Remove(name string) *FormatCheckerChain { func (c *FormatCheckerChain) Remove(name string) *FormatCheckerChain {
lock.Lock()
delete(c.formatters, name) delete(c.formatters, name)
lock.Unlock()
return c return c
} }
// Has checks to see if the FormatCheckerChain holds a FormatChecker with the given name // Has checks to see if the FormatCheckerChain holds a FormatChecker with the given name
func (c *FormatCheckerChain) Has(name string) bool { func (c *FormatCheckerChain) Has(name string) bool {
lock.Lock()
_, ok := c.formatters[name] _, ok := c.formatters[name]
lock.Unlock()
return ok return ok
} }
@ -117,32 +159,52 @@ func (c *FormatCheckerChain) IsFormat(name string, input interface{}) bool {
return false 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 return false
} }
inputString := input.(string) _, err := mail.ParseAddress(asString)
return f.IsFormat(inputString) return err == nil
}
func (f EmailFormatChecker) IsFormat(input string) bool {
return rxEmail.MatchString(input)
} }
// Credit: https://github.com/asaskevich/govalidator // Credit: https://github.com/asaskevich/govalidator
func (f IPV4FormatChecker) IsFormat(input string) bool { func (f IPV4FormatChecker) IsFormat(input interface{}) bool {
ip := net.ParseIP(input)
return ip != nil && strings.Contains(input, ".") 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 // Credit: https://github.com/asaskevich/govalidator
func (f IPV6FormatChecker) IsFormat(input string) bool { func (f IPV6FormatChecker) IsFormat(input interface{}) bool {
ip := net.ParseIP(input)
return ip != nil && strings.Contains(input, ":") asString, ok := input.(string)
if ok == false {
return false
}
ip := net.ParseIP(asString)
return ip != nil && strings.Contains(asString, ":")
}
func (f DateTimeFormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if ok == false {
return false
} }
func (f DateTimeFormatChecker) IsFormat(input string) bool {
formats := []string{ formats := []string{
"15:04:05", "15:04:05",
"15:04:05Z07:00", "15:04:05Z07:00",
@ -152,7 +214,7 @@ func (f DateTimeFormatChecker) IsFormat(input string) bool {
} }
for _, format := range formats { for _, format := range formats {
if _, err := time.Parse(format, input); err == nil { if _, err := time.Parse(format, asString); err == nil {
return true return true
} }
} }
@ -160,19 +222,122 @@ func (f DateTimeFormatChecker) IsFormat(input string) bool {
return false return false
} }
func (f URIFormatChecker) IsFormat(input string) bool { func (f DateFormatChecker) IsFormat(input interface{}) bool {
u, err := url.Parse(input) 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 == "" { if err != nil || u.Scheme == "" {
return false 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 return true
} }
func (f HostnameFormatChecker) IsFormat(input string) bool { func (f JSONPointerFormatChecker) IsFormat(input interface{}) bool {
return rxHostname.MatchString(input) && len(input) < 256 asString, ok := input.(string)
if ok == false {
return false
} }
func (f UUIDFormatChecker) IsFormat(input string) bool { return rxJSONPointer.MatchString(asString)
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" import "bytes"
// jsonContext implements a persistent linked-list of strings // JsonContext implements a persistent linked-list of strings
type jsonContext struct { type JsonContext struct {
head string head string
tail *jsonContext tail *JsonContext
} }
func newJsonContext(head string, tail *jsonContext) *jsonContext { func NewJsonContext(head string, tail *JsonContext) *JsonContext {
return &jsonContext{head, tail} return &JsonContext{head, tail}
} }
// String displays the context in reverse. // String displays the context in reverse.
// This plays well with the data structure's persistent nature with // This plays well with the data structure's persistent nature with
// Cons and a json document's tree structure. // 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()) byteArr := make([]byte, 0, c.stringLen())
buf := bytes.NewBuffer(byteArr) buf := bytes.NewBuffer(byteArr)
c.writeStringToBuffer(buf, del) c.writeStringToBuffer(buf, del)
@ -47,7 +47,7 @@ func (c *jsonContext) String(del ...string) string {
return buf.String() return buf.String()
} }
func (c *jsonContext) stringLen() int { func (c *JsonContext) stringLen() int {
length := 0 length := 0
if c.tail != nil { if c.tail != nil {
length = c.tail.stringLen() + 1 // add 1 for "." length = c.tail.stringLen() + 1 // add 1 for "."
@ -57,7 +57,7 @@ func (c *jsonContext) stringLen() int {
return length return length
} }
func (c *jsonContext) writeStringToBuffer(buf *bytes.Buffer, del []string) { func (c *JsonContext) writeStringToBuffer(buf *bytes.Buffer, del []string) {
if c.tail != nil { if c.tail != nil {
c.tail.writeStringToBuffer(buf, del) c.tail.writeStringToBuffer(buf, del)

View File

@ -33,6 +33,7 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
@ -40,34 +41,92 @@ import (
"github.com/xeipuuv/gojsonreference" "github.com/xeipuuv/gojsonreference"
) )
var osFS = osFileSystem(os.Open)
// JSON loader interface // JSON loader interface
type JSONLoader interface { type JSONLoader interface {
jsonSource() interface{} JsonSource() interface{}
loadJSON() (interface{}, error) LoadJSON() (interface{}, error)
loadSchema() (*Schema, 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 // JSON Reference loader
// references are used to load JSONs from files and HTTP // references are used to load JSONs from files and HTTP
type jsonReferenceLoader struct { type jsonReferenceLoader struct {
fs http.FileSystem
source string source string
} }
func (l *jsonReferenceLoader) jsonSource() interface{} { func (l *jsonReferenceLoader) JsonSource() interface{} {
return l.source return l.source
} }
func NewReferenceLoader(source string) *jsonReferenceLoader { func (l *jsonReferenceLoader) JsonReference() (gojsonreference.JsonReference, error) {
return &jsonReferenceLoader{source: source} 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 var err error
reference, err := gojsonreference.NewJsonReference(l.jsonSource().(string)) reference, err := gojsonreference.NewJsonReference(l.JsonSource().(string))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -79,15 +138,12 @@ func (l *jsonReferenceLoader) loadJSON() (interface{}, error) {
if reference.HasFileScheme { if reference.HasFileScheme {
filename := strings.Replace(refToUrl.String(), "file://", "", -1) filename := strings.TrimPrefix(refToUrl.String(), "file://")
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
// on Windows, a file URL may have an extra leading slash, use slashes // on Windows, a file URL may have an extra leading slash, use slashes
// instead of backslashes, and have spaces escaped // instead of backslashes, and have spaces escaped
if strings.HasPrefix(filename, "/") { filename = strings.TrimPrefix(filename, "/")
filename = filename[1:]
}
filename = filepath.FromSlash(filename) filename = filepath.FromSlash(filename)
filename = strings.Replace(filename, "%20", " ", -1)
} }
document, err = l.loadFromFile(filename) 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) { 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) resp, err := http.Get(address)
if err != nil { if err != nil {
return nil, err return nil, err
@ -144,7 +179,7 @@ func (l *jsonReferenceLoader) loadFromHTTP(address string) (interface{}, error)
// must return HTTP Status 200 OK // must return HTTP Status 200 OK
if resp.StatusCode != http.StatusOK { 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) bodyBuff, err := ioutil.ReadAll(resp.Body)
@ -153,12 +188,16 @@ func (l *jsonReferenceLoader) loadFromHTTP(address string) (interface{}, error)
} }
return decodeJsonUsingNumber(bytes.NewReader(bodyBuff)) return decodeJsonUsingNumber(bytes.NewReader(bodyBuff))
} }
func (l *jsonReferenceLoader) loadFromFile(path string) (interface{}, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -173,45 +212,52 @@ type jsonStringLoader struct {
source string source string
} }
func (l *jsonStringLoader) jsonSource() interface{} { func (l *jsonStringLoader) JsonSource() interface{} {
return l.source 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} 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
} }
d := Schema{} func (l *jsonBytesLoader) JsonSource() interface{} {
d.pool = newSchemaPool() return l.source
d.referencePool = newSchemaReferencePool()
d.documentReference, err = gojsonreference.NewJsonReference("#")
d.pool.SetStandaloneDocument(document)
if err != nil {
return nil, err
} }
err = d.parse(document) func (l *jsonBytesLoader) JsonReference() (gojsonreference.JsonReference, error) {
if err != nil { return gojsonreference.NewJsonReference("#")
return nil, err
} }
return &d, nil func (l *jsonBytesLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
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 // JSON Go (types) loader
@ -221,19 +267,27 @@ type jsonGoLoader struct {
source interface{} source interface{}
} }
func (l *jsonGoLoader) jsonSource() interface{} { func (l *jsonGoLoader) JsonSource() interface{} {
return l.source 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} 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" // 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 { if err != nil {
return nil, err 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
document, err := l.loadJSON()
if err != nil {
return nil, err
} }
d := Schema{} func NewReaderLoader(source io.Reader) (JSONLoader, io.Reader) {
d.pool = newSchemaPool() buf := &bytes.Buffer{}
d.referencePool = newSchemaReferencePool() return &jsonIOLoader{buf: buf}, io.TeeReader(source, buf)
d.documentReference, err = gojsonreference.NewJsonReference("#")
d.pool.SetStandaloneDocument(document)
if err != nil {
return nil, err
} }
err = d.parse(document) func NewWriterLoader(source io.Writer) (JSONLoader, io.Writer) {
if err != nil { buf := &bytes.Buffer{}
return nil, err return &jsonIOLoader{buf: buf}, io.MultiWriter(source, buf)
} }
return &d, nil func (l *jsonIOLoader) JsonSource() interface{} {
return l.buf.String()
}
func (l *jsonIOLoader) LoadJSON() (interface{}, error) {
return decodeJsonUsingNumber(l.buf)
}
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) { func decodeJsonUsingNumber(r io.Reader) (interface{}, error) {

View File

@ -26,7 +26,7 @@
package gojsonschema package gojsonschema
type ( type (
// locale is an interface for definining custom error strings // locale is an interface for defining custom error strings
locale interface { locale interface {
Required() string Required() string
InvalidType() string InvalidType() string
@ -36,15 +36,19 @@ type (
NumberNot() string NumberNot() string
MissingDependency() string MissingDependency() string
Internal() string Internal() string
Const() string
Enum() string Enum() string
ArrayNotEnoughItems() string
ArrayNoAdditionalItems() string ArrayNoAdditionalItems() string
ArrayMinItems() string ArrayMinItems() string
ArrayMaxItems() string ArrayMaxItems() string
Unique() string Unique() string
ArrayContains() string
ArrayMinProperties() string ArrayMinProperties() string
ArrayMaxProperties() string ArrayMaxProperties() string
AdditionalPropertyNotAllowed() string AdditionalPropertyNotAllowed() string
InvalidPropertyPattern() string InvalidPropertyPattern() string
InvalidPropertyName() string
StringGTE() string StringGTE() string
StringLTE() string StringLTE() string
DoesNotMatchPattern() string DoesNotMatchPattern() string
@ -72,7 +76,11 @@ type (
ReferenceMustBeCanonical() string ReferenceMustBeCanonical() string
NotAValidType() string NotAValidType() string
Duplicated() string Duplicated() string
httpBadStatus() string HttpBadStatus() string
ParseError() string
ConditionThen() string
ConditionElse() string
// ErrorFormat // ErrorFormat
ErrorFormat() string ErrorFormat() string
@ -83,11 +91,11 @@ type (
) )
func (l DefaultLocale) Required() string { func (l DefaultLocale) Required() string {
return `%property% is required` return `{{.property}} is required`
} }
func (l DefaultLocale) InvalidType() string { func (l DefaultLocale) InvalidType() string {
return `Invalid type. Expected: %expected%, given: %given%` return `Invalid type. Expected: {{.expected}}, given: {{.given}}`
} }
func (l DefaultLocale) NumberAnyOf() string { func (l DefaultLocale) NumberAnyOf() string {
@ -107,164 +115,194 @@ func (l DefaultLocale) NumberNot() string {
} }
func (l DefaultLocale) MissingDependency() string { func (l DefaultLocale) MissingDependency() string {
return `Has a dependency on %dependency%` return `Has a dependency on {{.dependency}}`
} }
func (l DefaultLocale) Internal() string { 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 { 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 { func (l DefaultLocale) ArrayNoAdditionalItems() string {
return `No additional items allowed on array` 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 { 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 { 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 { 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 { func (l DefaultLocale) ArrayMinProperties() string {
return `Must have at least %min% properties` return `Must have at least {{.min}} properties`
} }
func (l DefaultLocale) ArrayMaxProperties() string { func (l DefaultLocale) ArrayMaxProperties() string {
return `Must have at most %max% properties` return `Must have at most {{.max}} properties`
} }
func (l DefaultLocale) AdditionalPropertyNotAllowed() string { func (l DefaultLocale) AdditionalPropertyNotAllowed() string {
return `Additional property %property% is not allowed` return `Additional property {{.property}} is not allowed`
} }
func (l DefaultLocale) InvalidPropertyPattern() string { 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 { 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 { 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 { func (l DefaultLocale) DoesNotMatchPattern() string {
return `Does not match pattern '%pattern%'` return `Does not match pattern '{{.pattern}}'`
} }
func (l DefaultLocale) DoesNotMatchFormat() string { func (l DefaultLocale) DoesNotMatchFormat() string {
return `Does not match format '%format%'` return `Does not match format '{{.format}}'`
} }
func (l DefaultLocale) MultipleOf() string { func (l DefaultLocale) MultipleOf() string {
return `Must be a multiple of %multiple%` return `Must be a multiple of {{.multiple}}`
} }
func (l DefaultLocale) NumberGTE() string { 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 { func (l DefaultLocale) NumberGT() string {
return `Must be greater than %min%` return `Must be greater than {{.min}}`
} }
func (l DefaultLocale) NumberLTE() string { 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 { func (l DefaultLocale) NumberLT() string {
return `Must be less than %max%` return `Must be less than {{.max}}`
} }
// Schema validators // Schema validators
func (l DefaultLocale) RegexPattern() string { func (l DefaultLocale) RegexPattern() string {
return `Invalid regex pattern '%pattern%'` return `Invalid regex pattern '{{.pattern}}'`
} }
func (l DefaultLocale) GreaterThanZero() string { 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 { func (l DefaultLocale) MustBeOfA() string {
return `%x% must be of a %y%` return `{{.x}} must be of a {{.y}}`
} }
func (l DefaultLocale) MustBeOfAn() string { func (l DefaultLocale) MustBeOfAn() string {
return `%x% must be of an %y%` return `{{.x}} must be of an {{.y}}`
} }
func (l DefaultLocale) CannotBeUsedWithout() string { func (l DefaultLocale) CannotBeUsedWithout() string {
return `%x% cannot be used without %y%` return `{{.x}} cannot be used without {{.y}}`
} }
func (l DefaultLocale) CannotBeGT() string { func (l DefaultLocale) CannotBeGT() string {
return `%x% cannot be greater than %y%` return `{{.x}} cannot be greater than {{.y}}`
} }
func (l DefaultLocale) MustBeOfType() string { func (l DefaultLocale) MustBeOfType() string {
return `%key% must be of type %type%` return `{{.key}} must be of type {{.type}}`
} }
func (l DefaultLocale) MustBeValidRegex() string { func (l DefaultLocale) MustBeValidRegex() string {
return `%key% must be a valid regex` return `{{.key}} must be a valid regex`
} }
func (l DefaultLocale) MustBeValidFormat() string { 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 { 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 { func (l DefaultLocale) KeyCannotBeGreaterThan() string {
return `%key% cannot be greater than %y%` return `{{.key}} cannot be greater than {{.y}}`
} }
func (l DefaultLocale) KeyItemsMustBeOfType() string { func (l DefaultLocale) KeyItemsMustBeOfType() string {
return `%key% items must be %type%` return `{{.key}} items must be {{.type}}`
} }
func (l DefaultLocale) KeyItemsMustBeUnique() string { func (l DefaultLocale) KeyItemsMustBeUnique() string {
return `%key% items must be unique` return `{{.key}} items must be unique`
} }
func (l DefaultLocale) ReferenceMustBeCanonical() string { func (l DefaultLocale) ReferenceMustBeCanonical() string {
return `Reference %reference% must be canonical` return `Reference {{.reference}} must be canonical`
} }
func (l DefaultLocale) NotAValidType() string { 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 { func (l DefaultLocale) Duplicated() string {
return `%type% type is duplicated` return `{{.type}} type is duplicated`
} }
func (l DefaultLocale) httpBadStatus() string { func (l DefaultLocale) HttpBadStatus() string {
return `Could not read schema from HTTP, response status is %status%` return `Could not read schema from HTTP, response status is {{.status}}`
} }
// Replacement options: field, description, context, value // Replacement options: field, description, context, value
func (l DefaultLocale) ErrorFormat() string { 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 ( const (
STRING_NUMBER = "number" STRING_NUMBER = "number"
STRING_ARRAY_OF_STRINGS = "array of strings" STRING_ARRAY_OF_STRINGS = "array of strings"
STRING_ARRAY_OF_SCHEMAS = "array of schemas" 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_SCHEMA_OR_ARRAY_OF_STRINGS = "schema or array of strings"
STRING_PROPERTIES = "properties" STRING_PROPERTIES = "properties"
STRING_DEPENDENCY = "dependency" STRING_DEPENDENCY = "dependency"

View File

@ -40,14 +40,17 @@ type (
Field() string Field() string
SetType(string) SetType(string)
Type() string Type() string
SetContext(*jsonContext) SetContext(*JsonContext)
Context() *jsonContext Context() *JsonContext
SetDescription(string) SetDescription(string)
Description() string Description() string
SetDescriptionFormat(string)
DescriptionFormat() string
SetValue(interface{}) SetValue(interface{})
Value() interface{} Value() interface{}
SetDetails(ErrorDetails) SetDetails(ErrorDetails)
Details() ErrorDetails Details() ErrorDetails
String() string
} }
// ResultErrorFields holds the fields for each ResultError implementation. // ResultErrorFields holds the fields for each ResultError implementation.
@ -55,8 +58,9 @@ type (
// can be defined by just embedding this type // can be defined by just embedding this type
ResultErrorFields struct { ResultErrorFields struct {
errorType string // A string with the type of error (i.e. invalid_type) 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 ... context *JsonContext // Tree like notation of the part that failed the validation. ex (root).a.b ...
description string // A human readable error message 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 value interface{} // Value given by the JSON file that is the source of the error
details ErrorDetails details ErrorDetails
} }
@ -72,12 +76,6 @@ type (
// Field outputs the field name without the root context // Field outputs the field name without the root context
// i.e. firstName or person.firstName instead of (root).firstName or (root).person.firstName // i.e. firstName or person.firstName instead of (root).firstName or (root).person.firstName
func (v *ResultErrorFields) Field() string { 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+".") return strings.TrimPrefix(v.context.String(), STRING_ROOT_SCHEMA_PROPERTY+".")
} }
@ -89,11 +87,11 @@ func (v *ResultErrorFields) Type() string {
return v.errorType return v.errorType
} }
func (v *ResultErrorFields) SetContext(context *jsonContext) { func (v *ResultErrorFields) SetContext(context *JsonContext) {
v.context = context v.context = context
} }
func (v *ResultErrorFields) Context() *jsonContext { func (v *ResultErrorFields) Context() *JsonContext {
return v.context return v.context
} }
@ -105,6 +103,14 @@ func (v *ResultErrorFields) Description() string {
return v.description 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{}) { func (v *ResultErrorFields) SetValue(value interface{}) {
v.value = value v.value = value
} }
@ -154,7 +160,19 @@ func (v *Result) Errors() []ResultError {
return v.errors 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) newError(err, context, value, Locale, details)
v.errors = append(v.errors, err) 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 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 package gojsonschema
import ( import (
// "encoding/json"
"errors" "errors"
"math/big"
"reflect" "reflect"
"regexp" "regexp"
"text/template"
"github.com/xeipuuv/gojsonreference" "github.com/xeipuuv/gojsonreference"
) )
@ -39,10 +40,13 @@ var (
// Locale is the default locale to use // Locale is the default locale to use
// Library users can overwrite with their own implementation // Library users can overwrite with their own implementation
Locale locale = DefaultLocale{} Locale locale = DefaultLocale{}
// ErrorTemplateFuncs allows you to define custom template funcs for use in localization.
ErrorTemplateFuncs template.FuncMap
) )
func NewSchema(l JSONLoader) (*Schema, error) { func NewSchema(l JSONLoader) (*Schema, error) {
return l.loadSchema() return NewSchemaLoader().Compile(l)
} }
type Schema struct { type Schema struct {
@ -52,8 +56,8 @@ type Schema struct {
referencePool *schemaReferencePool referencePool *schemaReferencePool
} }
func (d *Schema) parse(document interface{}) error { func (d *Schema) parse(document interface{}, draft Draft) error {
d.rootSchema = &subSchema{property: STRING_ROOT_SCHEMA_PROPERTY} d.rootSchema = &subSchema{property: STRING_ROOT_SCHEMA_PROPERTY, draft: &draft}
return d.parseSchema(document, d.rootSchema) 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 { 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) { if !isKind(documentNode, reflect.Map) {
return errors.New(formatErrorDescription( return errors.New(formatErrorDescription(
Locale.InvalidType(), Locale.ParseError(),
ErrorDetails{ ErrorDetails{
"expected": TYPE_OBJECT, "expected": STRING_SCHEMA,
"given": STRING_SCHEMA,
}, },
)) ))
} }
m := documentNode.(map[string]interface{}) m := documentNode.(map[string]interface{})
if currentSchema == d.rootSchema { if currentSchema.parent == nil {
currentSchema.ref = &d.documentReference currentSchema.ref = &d.documentReference
currentSchema.id = &d.documentReference
} }
// $subSchema if currentSchema.id == nil && currentSchema.parent != nil {
if existsMapKey(m, KEY_SCHEMA) { currentSchema.id = currentSchema.parent.id
if !isKind(m[KEY_SCHEMA], 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( return errors.New(formatErrorDescription(
Locale.InvalidType(), Locale.InvalidType(),
ErrorDetails{ ErrorDetails{
"expected": TYPE_STRING, "expected": TYPE_STRING,
"given": KEY_SCHEMA, "given": keyID,
}, },
)) ))
} }
schemaRef := m[KEY_SCHEMA].(string) if k, ok := m[keyID].(string); ok {
schemaReference, err := gojsonreference.NewJsonReference(schemaRef) jsonReference, err := gojsonreference.NewJsonReference(k)
currentSchema.subSchema = &schemaReference
if err != nil { if err != nil {
return err return err
} }
} if currentSchema == d.rootSchema {
currentSchema.id = &jsonReference
// $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 {
if sch, ok := d.referencePool.Get(currentSchema.ref.String() + k); ok {
currentSchema.refSchema = sch
} else { } else {
ref, err := currentSchema.parent.id.Inherits(jsonReference)
var err error
err = d.parseReference(documentNode, currentSchema, k)
if err != nil { if err != nil {
return err return err
} }
currentSchema.id = ref
return nil
} }
} }
// definitions // definitions
if existsMapKey(m, KEY_DEFINITIONS) { if existsMapKey(m, KEY_DEFINITIONS) {
if isKind(m[KEY_DEFINITIONS], reflect.Map) { if isKind(m[KEY_DEFINITIONS], reflect.Map, reflect.Bool) {
currentSchema.definitions = make(map[string]*subSchema) for _, dv := range m[KEY_DEFINITIONS].(map[string]interface{}) {
for dk, dv := range m[KEY_DEFINITIONS].(map[string]interface{}) { if isKind(dv, reflect.Map, reflect.Bool) {
if isKind(dv, reflect.Map) {
newSchema := &subSchema{property: KEY_DEFINITIONS, parent: currentSchema, ref: currentSchema.ref} newSchema := &subSchema{property: KEY_DEFINITIONS, parent: currentSchema}
currentSchema.definitions[dk] = newSchema
err := d.parseSchema(dv, newSchema) err := d.parseSchema(dv, newSchema)
if err != nil { if err != nil {
return errors.New(err.Error()) return err
} }
} else { } else {
return errors.New(formatErrorDescription( 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 // title
if existsMapKey(m, KEY_TITLE) && !isKind(m[KEY_TITLE], reflect.String) { if existsMapKey(m, KEY_TITLE) && !isKind(m[KEY_TITLE], reflect.String) {
return errors.New(formatErrorDescription( return errors.New(formatErrorDescription(
@ -208,6 +213,39 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema)
currentSchema.description = &k 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 // type
if existsMapKey(m, KEY_TYPE) { if existsMapKey(m, KEY_TYPE) {
if isKind(m[KEY_TYPE], reflect.String) { 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 // dependencies
if existsMapKey(m, KEY_DEPENDENCIES) { if existsMapKey(m, KEY_DEPENDENCIES) {
err := d.parseDependencies(m[KEY_DEPENDENCIES], currentSchema) 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 existsMapKey(m, KEY_ITEMS) {
if isKind(m[KEY_ITEMS], reflect.Slice) { if isKind(m[KEY_ITEMS], reflect.Slice) {
for _, itemElement := range m[KEY_ITEMS].([]interface{}) { 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 := &subSchema{parent: currentSchema, property: KEY_ITEMS}
newSchema.ref = currentSchema.ref newSchema.ref = currentSchema.ref
currentSchema.AddItemsChild(newSchema) currentSchema.AddItemsChild(newSchema)
@ -340,7 +398,7 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema)
} }
currentSchema.itemsChildrenIsSingleSchema = false 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 := &subSchema{parent: currentSchema, property: KEY_ITEMS}
newSchema.ref = currentSchema.ref newSchema.ref = currentSchema.ref
currentSchema.AddItemsChild(newSchema) 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( return errors.New(formatErrorDescription(
Locale.GreaterThanZero(), Locale.GreaterThanZero(),
ErrorDetails{"number": KEY_MULTIPLE_OF}, ErrorDetails{"number": KEY_MULTIPLE_OF},
@ -416,6 +474,28 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema)
} }
if existsMapKey(m, KEY_EXCLUSIVE_MINIMUM) { if existsMapKey(m, KEY_EXCLUSIVE_MINIMUM) {
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},
))
}
if m[KEY_EXCLUSIVE_MINIMUM].(bool) {
currentSchema.exclusiveMinimum = currentSchema.minimum
currentSchema.minimum = nil
}
case Hybrid:
if isKind(m[KEY_EXCLUSIVE_MINIMUM], reflect.Bool) { if isKind(m[KEY_EXCLUSIVE_MINIMUM], reflect.Bool) {
if currentSchema.minimum == nil { if currentSchema.minimum == nil {
return errors.New(formatErrorDescription( return errors.New(formatErrorDescription(
@ -423,14 +503,34 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema)
ErrorDetails{"x": KEY_EXCLUSIVE_MINIMUM, "y": KEY_MINIMUM}, ErrorDetails{"x": KEY_EXCLUSIVE_MINIMUM, "y": KEY_MINIMUM},
)) ))
} }
exclusiveMinimumValue := m[KEY_EXCLUSIVE_MINIMUM].(bool) if m[KEY_EXCLUSIVE_MINIMUM].(bool) {
currentSchema.exclusiveMinimum = exclusiveMinimumValue currentSchema.exclusiveMinimum = currentSchema.minimum
currentSchema.minimum = nil
}
} else if isJsonNumber(m[KEY_EXCLUSIVE_MINIMUM]) {
currentSchema.exclusiveMinimum = mustBeNumber(m[KEY_EXCLUSIVE_MINIMUM])
} else { } else {
return errors.New(formatErrorDescription( return errors.New(formatErrorDescription(
Locale.MustBeOfA(), Locale.InvalidType(),
ErrorDetails{"x": KEY_EXCLUSIVE_MINIMUM, "y": TYPE_BOOLEAN}, 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,
},
))
}
}
} }
if existsMapKey(m, KEY_MAXIMUM) { if existsMapKey(m, KEY_MAXIMUM) {
@ -445,6 +545,28 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema)
} }
if existsMapKey(m, KEY_EXCLUSIVE_MAXIMUM) { if existsMapKey(m, KEY_EXCLUSIVE_MAXIMUM) {
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},
))
}
if m[KEY_EXCLUSIVE_MAXIMUM].(bool) {
currentSchema.exclusiveMaximum = currentSchema.maximum
currentSchema.maximum = nil
}
case Hybrid:
if isKind(m[KEY_EXCLUSIVE_MAXIMUM], reflect.Bool) { if isKind(m[KEY_EXCLUSIVE_MAXIMUM], reflect.Bool) {
if currentSchema.maximum == nil { if currentSchema.maximum == nil {
return errors.New(formatErrorDescription( return errors.New(formatErrorDescription(
@ -452,24 +574,35 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema)
ErrorDetails{"x": KEY_EXCLUSIVE_MAXIMUM, "y": KEY_MAXIMUM}, ErrorDetails{"x": KEY_EXCLUSIVE_MAXIMUM, "y": KEY_MAXIMUM},
)) ))
} }
exclusiveMaximumValue := m[KEY_EXCLUSIVE_MAXIMUM].(bool) if m[KEY_EXCLUSIVE_MAXIMUM].(bool) {
currentSchema.exclusiveMaximum = exclusiveMaximumValue currentSchema.exclusiveMaximum = currentSchema.maximum
currentSchema.maximum = nil
}
} else if isJsonNumber(m[KEY_EXCLUSIVE_MAXIMUM]) {
currentSchema.exclusiveMaximum = mustBeNumber(m[KEY_EXCLUSIVE_MAXIMUM])
} else { } else {
return errors.New(formatErrorDescription( return errors.New(formatErrorDescription(
Locale.MustBeOfA(), Locale.InvalidType(),
ErrorDetails{"x": KEY_EXCLUSIVE_MAXIMUM, "y": STRING_NUMBER}, ErrorDetails{
"expected": TYPE_BOOLEAN + "/" + TYPE_NUMBER,
"given": KEY_EXCLUSIVE_MAXIMUM,
},
)) ))
} }
} default:
if isJsonNumber(m[KEY_EXCLUSIVE_MAXIMUM]) {
if currentSchema.minimum != nil && currentSchema.maximum != nil { currentSchema.exclusiveMaximum = mustBeNumber(m[KEY_EXCLUSIVE_MAXIMUM])
if *currentSchema.minimum > *currentSchema.maximum { } else {
return errors.New(formatErrorDescription( return errors.New(formatErrorDescription(
Locale.CannotBeGT(), Locale.InvalidType(),
ErrorDetails{"x": KEY_MINIMUM, "y": KEY_MAXIMUM}, ErrorDetails{
"expected": TYPE_NUMBER,
"given": KEY_EXCLUSIVE_MAXIMUM,
},
)) ))
} }
} }
}
// validation : string // validation : string
@ -538,11 +671,6 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema)
formatString, ok := m[KEY_FORMAT].(string) formatString, ok := m[KEY_FORMAT].(string)
if ok && FormatCheckers.Has(formatString) { if ok && FormatCheckers.Has(formatString) {
currentSchema.format = 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 // 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 existsMapKey(m, KEY_ENUM) {
if isKind(m[KEY_ENUM], reflect.Slice) { if isKind(m[KEY_ENUM], reflect.Slice) {
for _, v := range m[KEY_ENUM].([]interface{}) { 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 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} newSchema := &subSchema{property: KEY_NOT, parent: currentSchema, ref: currentSchema.ref}
currentSchema.SetNot(newSchema) currentSchema.SetNot(newSchema)
err := d.parseSchema(m[KEY_NOT], 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 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 { if err != nil {
return err return err
} }
standaloneDocument := d.pool.GetStandaloneDocument() if !isKind(refdDocumentNode, reflect.Map, reflect.Bool) {
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) {
return errors.New(formatErrorDescription( return errors.New(formatErrorDescription(
Locale.MustBeOfType(), Locale.MustBeOfType(),
ErrorDetails{"key": STRING_SCHEMA, "type": TYPE_OBJECT}, ErrorDetails{"key": STRING_SCHEMA, "type": TYPE_OBJECT},
)) ))
} }
// returns the loaded referenced subSchema for the caller to update its current subSchema err = d.parseSchema(refdDocumentNode, newSchema)
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)
if err != nil { if err != nil {
return err return err
} }
@ -884,7 +1048,7 @@ func (d *Schema) parseDependencies(documentNode interface{}, currentSchema *subS
currentSchema.dependencies[k] = valuesToRegister currentSchema.dependencies[k] = valuesToRegister
} }
case reflect.Map: case reflect.Map, reflect.Bool:
depSchema := &subSchema{property: k, parent: currentSchema, ref: currentSchema.ref} depSchema := &subSchema{property: k, parent: currentSchema, ref: currentSchema.ref}
err := d.parseSchema(m[k], depSchema) err := d.parseSchema(m[k], depSchema)
if err != nil { 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 ( import (
"errors" "errors"
"fmt"
"reflect"
"github.com/xeipuuv/gojsonreference" "github.com/xeipuuv/gojsonreference"
) )
type schemaPoolDocument struct { type schemaPoolDocument struct {
Document interface{} Document interface{}
Draft *Draft
} }
type schemaPool struct { type schemaPool struct {
schemaPoolDocuments map[string]*schemaPoolDocument 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{} var (
p.schemaPoolDocuments = make(map[string]*schemaPoolDocument) draft *Draft
p.standaloneDocument = nil err error
reference = ref.String()
return p )
// 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)
} }
func (p *schemaPool) SetStandaloneDocument(document interface{}) { if *p.autoDetect {
p.standaloneDocument = document _, draft, err = parseSchemaURL(document)
if err != nil {
return err
}
} }
func (p *schemaPool) GetStandaloneDocument() (document interface{}) { err = p.parseReferencesRecursive(document, ref, draft)
return p.standaloneDocument
if pooled {
p.schemaPoolDocuments[reference] = &schemaPoolDocument{Document: document, Draft: draft}
}
return err
}
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
// 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) { func (p *schemaPool) GetDocument(reference gojsonreference.JsonReference) (*schemaPoolDocument, error) {
var (
spd *schemaPoolDocument
draft *Draft
ok bool
err error
)
if internalLogEnabled { if internalLogEnabled {
internalLog("Get Document ( %s )", reference.String()) 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... // First check if the given fragment is a location independent identifier
if !reference.IsCanonical() { // http://json-schema.org/latest/json-schema-core.html#rfc.section.8.2.3
return nil, errors.New(formatErrorDescription(
Locale.ReferenceMustBeCanonical(),
ErrorDetails{"reference": reference},
))
}
refToUrl := reference if spd, ok = p.schemaPoolDocuments[refToUrl.String()]; ok {
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 internalLogEnabled { if internalLogEnabled {
internalLog(" From pool") internalLog(" From pool")
} }
return spd, nil return spd, nil
} }
jsonReferenceLoader := NewReferenceLoader(reference.String()) // If the given reference is not a location independent identifier,
document, err := jsonReferenceLoader.loadJSON() // 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 { if err != nil {
return nil, err return nil, err
} }
spd = &schemaPoolDocument{Document: document} if internalLogEnabled {
// add the document to the pool for potential later use internalLog(" From pool")
p.schemaPoolDocuments[refToUrl.String()] = spd }
spd = &schemaPoolDocument{Document: document, Draft: cachedSpd.Draft}
p.schemaPoolDocuments[reference.String()] = spd
return spd, nil 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
}
// add the whole document to the pool for potential re-use
p.parseReferences(document, refToUrl, true)
_, 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 { if internalLogEnabled {
internalLog(fmt.Sprintf("Add Schema Reference %s to pool", ref)) internalLog(fmt.Sprintf("Add Schema Reference %s to pool", ref))
} }
if _, ok := p.documents[ref]; !ok {
p.documents[ref] = sch p.documents[ref] = sch
} }
}

View File

@ -44,7 +44,7 @@ func (t *jsonSchemaType) IsTyped() bool {
func (t *jsonSchemaType) Add(etype string) error { func (t *jsonSchemaType) Add(etype string) error {
if !isStringInSlice(JSON_TYPES, etype) { 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) { if t.Contains(etype) {

View File

@ -28,6 +28,7 @@ package gojsonschema
import ( import (
"errors" "errors"
"math/big"
"regexp" "regexp"
"strings" "strings"
@ -35,8 +36,9 @@ import (
) )
const ( const (
KEY_SCHEMA = "$subSchema" KEY_SCHEMA = "$schema"
KEY_ID = "$id" KEY_ID = "id"
KEY_ID_NEW = "$id"
KEY_REF = "$ref" KEY_REF = "$ref"
KEY_TITLE = "title" KEY_TITLE = "title"
KEY_DESCRIPTION = "description" KEY_DESCRIPTION = "description"
@ -46,6 +48,7 @@ const (
KEY_PROPERTIES = "properties" KEY_PROPERTIES = "properties"
KEY_PATTERN_PROPERTIES = "patternProperties" KEY_PATTERN_PROPERTIES = "patternProperties"
KEY_ADDITIONAL_PROPERTIES = "additionalProperties" KEY_ADDITIONAL_PROPERTIES = "additionalProperties"
KEY_PROPERTY_NAMES = "propertyNames"
KEY_DEFINITIONS = "definitions" KEY_DEFINITIONS = "definitions"
KEY_MULTIPLE_OF = "multipleOf" KEY_MULTIPLE_OF = "multipleOf"
KEY_MINIMUM = "minimum" KEY_MINIMUM = "minimum"
@ -63,17 +66,23 @@ const (
KEY_MIN_ITEMS = "minItems" KEY_MIN_ITEMS = "minItems"
KEY_MAX_ITEMS = "maxItems" KEY_MAX_ITEMS = "maxItems"
KEY_UNIQUE_ITEMS = "uniqueItems" KEY_UNIQUE_ITEMS = "uniqueItems"
KEY_CONTAINS = "contains"
KEY_CONST = "const"
KEY_ENUM = "enum" KEY_ENUM = "enum"
KEY_ONE_OF = "oneOf" KEY_ONE_OF = "oneOf"
KEY_ANY_OF = "anyOf" KEY_ANY_OF = "anyOf"
KEY_ALL_OF = "allOf" KEY_ALL_OF = "allOf"
KEY_NOT = "not" KEY_NOT = "not"
KEY_IF = "if"
KEY_THEN = "then"
KEY_ELSE = "else"
) )
type subSchema struct { type subSchema struct {
draft *Draft
// basic subSchema meta properties // basic subSchema meta properties
id *string id *gojsonreference.JsonReference
title *string title *string
description *string description *string
@ -86,23 +95,19 @@ type subSchema struct {
ref *gojsonreference.JsonReference ref *gojsonreference.JsonReference
// Schema referenced // Schema referenced
refSchema *subSchema refSchema *subSchema
// Json reference
subSchema *gojsonreference.JsonReference
// hierarchy // hierarchy
parent *subSchema parent *subSchema
definitions map[string]*subSchema
definitionsChildren []*subSchema
itemsChildren []*subSchema itemsChildren []*subSchema
itemsChildrenIsSingleSchema bool itemsChildrenIsSingleSchema bool
propertiesChildren []*subSchema propertiesChildren []*subSchema
// validation : number / integer // validation : number / integer
multipleOf *float64 multipleOf *big.Rat
maximum *float64 maximum *big.Rat
exclusiveMaximum bool exclusiveMaximum *big.Rat
minimum *float64 minimum *big.Rat
exclusiveMinimum bool exclusiveMinimum *big.Rat
// validation : string // validation : string
minLength *int minLength *int
@ -118,15 +123,18 @@ type subSchema struct {
dependencies map[string]interface{} dependencies map[string]interface{}
additionalProperties interface{} additionalProperties interface{}
patternProperties map[string]*subSchema patternProperties map[string]*subSchema
propertyNames *subSchema
// validation : array // validation : array
minItems *int minItems *int
maxItems *int maxItems *int
uniqueItems bool uniqueItems bool
contains *subSchema
additionalItems interface{} additionalItems interface{}
// validation : all // validation : all
_const *string //const is a golang keyword
enum []string enum []string
// validation : subSchema // validation : subSchema
@ -134,11 +142,24 @@ type subSchema struct {
anyOf []*subSchema anyOf []*subSchema
allOf []*subSchema allOf []*subSchema
not *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 { func (s *subSchema) AddEnum(i interface{}) error {
is, err := marshalToJsonString(i) is, err := marshalWithoutNumber(i)
if err != nil { if err != nil {
return err return err
} }
@ -157,7 +178,7 @@ func (s *subSchema) AddEnum(i interface{}) error {
func (s *subSchema) ContainsEnum(i interface{}) (bool, error) { func (s *subSchema) ContainsEnum(i interface{}) (bool, error) {
is, err := marshalToJsonString(i) is, err := marshalWithoutNumber(i)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -181,6 +202,18 @@ func (s *subSchema) SetNot(subSchema *subSchema) {
s.not = 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 { func (s *subSchema) AddRequired(value string) error {
if isStringInSlice(s.required, value) { if isStringInSlice(s.required, value) {
@ -195,10 +228,6 @@ func (s *subSchema) AddRequired(value string) error {
return nil return nil
} }
func (s *subSchema) AddDefinitionChild(child *subSchema) {
s.definitionsChildren = append(s.definitionsChildren, child)
}
func (s *subSchema) AddItemsChild(child *subSchema) { func (s *subSchema) AddItemsChild(child *subSchema) {
s.itemsChildren = append(s.itemsChildren, child) s.itemsChildren = append(s.itemsChildren, child)
} }
@ -214,7 +243,7 @@ func (s *subSchema) PatternPropertiesString() string {
} }
patternPropertiesKeySlice := []string{} patternPropertiesKeySlice := []string{}
for pk, _ := range s.patternProperties { for pk := range s.patternProperties {
patternPropertiesKeySlice = append(patternPropertiesKeySlice, `"`+pk+`"`) patternPropertiesKeySlice = append(patternPropertiesKeySlice, `"`+pk+`"`)
} }

View File

@ -29,12 +29,23 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"math" "math"
"math/big"
"reflect" "reflect"
"strconv"
) )
func isKind(what interface{}, kind reflect.Kind) bool { func isKind(what interface{}, kinds ...reflect.Kind) bool {
return reflect.ValueOf(what).Kind() == kind 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 { func existsMapKey(m map[string]interface{}, k string) bool {
@ -51,6 +62,16 @@ func isStringInSlice(s []string, what string) bool {
return false 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) { func marshalToJsonString(value interface{}) (*string, error) {
mBytes, err := json.Marshal(value) mBytes, err := json.Marshal(value)
@ -62,6 +83,28 @@ func marshalToJsonString(value interface{}) (*string, error) {
return &sBytes, nil 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 { func isJsonNumber(what interface{}) bool {
switch what.(type) { switch what.(type) {
@ -73,20 +116,13 @@ func isJsonNumber(what interface{}) bool {
return false return false
} }
func checkJsonNumber(what interface{}) (isValidFloat64 bool, isValidInt64 bool, isValidInt32 bool) { func checkJsonInteger(what interface{}) (isInt bool) {
jsonNumber := what.(json.Number) jsonNumber := what.(json.Number)
_, errFloat64 := jsonNumber.Float64() bigFloat, isValidNumber := new(big.Rat).SetString(string(jsonNumber))
_, errInt64 := jsonNumber.Int64()
isValidFloat64 = errFloat64 == nil return isValidNumber && bigFloat.IsInt()
isValidInt64 = errInt64 == nil
_, errInt32 := strconv.ParseInt(jsonNumber.String(), 10, 32)
isValidInt32 = isValidInt64 && errInt32 == nil
return
} }
@ -111,9 +147,9 @@ func mustBeInteger(what interface{}) *int {
number := what.(json.Number) number := what.(json.Number)
_, _, isValidInt32 := checkJsonNumber(number) isInt := checkJsonInteger(number)
if isValidInt32 { if isInt {
int64Value, err := number.Int64() int64Value, err := number.Int64()
if err != nil { if err != nil {
@ -132,15 +168,13 @@ func mustBeInteger(what interface{}) *int {
return nil return nil
} }
func mustBeNumber(what interface{}) *float64 { func mustBeNumber(what interface{}) *big.Rat {
if isJsonNumber(what) { if isJsonNumber(what) {
number := what.(json.Number) number := what.(json.Number)
float64Value, err := number.Float64() float64Value, success := new(big.Rat).SetString(string(number))
if success {
if err == nil { return float64Value
return &float64Value
} else { } else {
return nil return nil
} }

View File

@ -27,6 +27,7 @@ package gojsonschema
import ( import (
"encoding/json" "encoding/json"
"math/big"
"reflect" "reflect"
"regexp" "regexp"
"strconv" "strconv"
@ -55,29 +56,32 @@ func (v *Schema) Validate(l JSONLoader) (*Result, error) {
// load document // load document
root, err := l.loadJSON() root, err := l.LoadJSON()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return v.validateDocument(root), nil
}
func (v *Schema) validateDocument(root interface{}) *Result {
// begin validation // begin validation
result := &Result{} result := &Result{}
context := newJsonContext(STRING_CONTEXT_ROOT, nil) context := NewJsonContext(STRING_CONTEXT_ROOT, nil)
v.rootSchema.validateRecursive(v.rootSchema, root, result, context) 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{} result := &Result{}
v.validateRecursive(v, document, result, context) v.validateRecursive(v, document, result, context)
return result return result
} }
// Walker function to validate the json recursively against the subSchema // 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 { if internalLogEnabled {
internalLog("validateRecursive %s", context.String()) internalLog("validateRecursive %s", context.String())
@ -93,7 +97,7 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i
// Check for null value // Check for null value
if currentNode == nil { if currentNode == nil {
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_NULL) { if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_NULL) {
result.addError( result.addInternalError(
new(InvalidTypeError), new(InvalidTypeError),
context, context,
currentNode, currentNode,
@ -114,18 +118,18 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i
value := currentNode.(json.Number) 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 { if currentSubSchema.types.IsTyped() && !validType {
givenType := TYPE_INTEGER givenType := TYPE_INTEGER
if !isValidInt64 { if !isInt {
givenType = TYPE_NUMBER givenType = TYPE_NUMBER
} }
result.addError( result.addInternalError(
new(InvalidTypeError), new(InvalidTypeError),
context, context,
currentNode, currentNode,
@ -154,7 +158,7 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i
case reflect.Slice: case reflect.Slice:
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_ARRAY) { if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_ARRAY) {
result.addError( result.addInternalError(
new(InvalidTypeError), new(InvalidTypeError),
context, context,
currentNode, currentNode,
@ -177,7 +181,7 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i
case reflect.Map: case reflect.Map:
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_OBJECT) { if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_OBJECT) {
result.addError( result.addInternalError(
new(InvalidTypeError), new(InvalidTypeError),
context, context,
currentNode, currentNode,
@ -202,7 +206,7 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i
for _, pSchema := range currentSubSchema.propertiesChildren { for _, pSchema := range currentSubSchema.propertiesChildren {
nextNode, ok := castCurrentNode[pSchema.property] nextNode, ok := castCurrentNode[pSchema.property]
if ok { if ok {
subContext := newJsonContext(pSchema.property, context) subContext := NewJsonContext(pSchema.property, context)
v.validateRecursive(pSchema, nextNode, result, subContext) v.validateRecursive(pSchema, nextNode, result, subContext)
} }
} }
@ -212,7 +216,7 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i
case reflect.Bool: case reflect.Bool:
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_BOOLEAN) { if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_BOOLEAN) {
result.addError( result.addInternalError(
new(InvalidTypeError), new(InvalidTypeError),
context, context,
currentNode, currentNode,
@ -234,7 +238,7 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i
case reflect.String: case reflect.String:
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_STRING) { if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_STRING) {
result.addError( result.addInternalError(
new(InvalidTypeError), new(InvalidTypeError),
context, context,
currentNode, currentNode,
@ -263,7 +267,7 @@ func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode i
} }
// Different kinds of validation there, subSchema / common / array / object / string... // 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 { if internalLogEnabled {
internalLog("validateSchema %s", context.String()) internalLog("validateSchema %s", context.String())
@ -287,7 +291,7 @@ func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode inte
} }
if !validatedAnyOf { if !validatedAnyOf {
result.addError(new(NumberAnyOfError), context, currentNode, ErrorDetails{}) result.addInternalError(new(NumberAnyOfError), context, currentNode, ErrorDetails{})
if bestValidationResult != nil { if bestValidationResult != nil {
// add error messages of closest matching subSchema as // add error messages of closest matching subSchema as
@ -313,7 +317,7 @@ func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode inte
if nbValidated != 1 { if nbValidated != 1 {
result.addError(new(NumberOneOfError), context, currentNode, ErrorDetails{}) result.addInternalError(new(NumberOneOfError), context, currentNode, ErrorDetails{})
if nbValidated == 0 { if nbValidated == 0 {
// add error messages of closest matching subSchema as // 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) { if nbValidated != len(currentSubSchema.allOf) {
result.addError(new(NumberAllOfError), context, currentNode, ErrorDetails{}) result.addInternalError(new(NumberAllOfError), context, currentNode, ErrorDetails{})
} }
} }
if currentSubSchema.not != nil { if currentSubSchema.not != nil {
validationResult := currentSubSchema.not.subValidateWithContext(currentNode, context) validationResult := currentSubSchema.not.subValidateWithContext(currentNode, context)
if validationResult.Valid() { 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: case []string:
for _, dependOnKey := range dependency { for _, dependOnKey := range dependency {
if _, dependencyResolved := currentNode.(map[string]interface{})[dependOnKey]; !dependencyResolved { if _, dependencyResolved := currentNode.(map[string]interface{})[dependOnKey]; !dependencyResolved {
result.addError( result.addInternalError(
new(MissingDependencyError), new(MissingDependencyError),
context, context,
currentNode, currentNode,
@ -367,9 +371,26 @@ func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode inte
case *subSchema: case *subSchema:
dependency.validateRecursive(dependency, currentNode, result, context) 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)
} }
} }
} }
@ -377,21 +398,38 @@ func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode inte
result.incrementScore() 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 { if internalLogEnabled {
internalLog("validateCommon %s", context.String()) internalLog("validateCommon %s", context.String())
internalLog(" %v", value) 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: // enum:
if len(currentSubSchema.enum) > 0 { if len(currentSubSchema.enum) > 0 {
has, err := currentSubSchema.ContainsEnum(value) has, err := currentSubSchema.ContainsEnum(value)
if err != nil { if err != nil {
result.addError(new(InternalError), context, value, ErrorDetails{"error": err}) result.addInternalError(new(InternalError), context, value, ErrorDetails{"error": err})
} }
if !has { if !has {
result.addError( result.addInternalError(
new(EnumError), new(EnumError),
context, context,
value, value,
@ -405,19 +443,19 @@ func (v *subSchema) validateCommon(currentSubSchema *subSchema, value interface{
result.incrementScore() 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 { if internalLogEnabled {
internalLog("validateArray %s", context.String()) internalLog("validateArray %s", context.String())
internalLog(" %v", value) internalLog(" %v", value)
} }
nbItems := len(value) nbValues := len(value)
// TODO explain // TODO explain
if currentSubSchema.itemsChildrenIsSingleSchema { if currentSubSchema.itemsChildrenIsSingleSchema {
for i := range value { for i := range value {
subContext := newJsonContext(strconv.Itoa(i), context) subContext := NewJsonContext(strconv.Itoa(i), context)
validationResult := currentSubSchema.itemsChildren[0].subValidateWithContext(value[i], subContext) validationResult := currentSubSchema.itemsChildren[0].subValidateWithContext(value[i], subContext)
result.mergeErrors(validationResult) result.mergeErrors(validationResult)
} }
@ -425,24 +463,27 @@ func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface
if currentSubSchema.itemsChildren != nil && len(currentSubSchema.itemsChildren) > 0 { if currentSubSchema.itemsChildren != nil && len(currentSubSchema.itemsChildren) > 0 {
nbItems := len(currentSubSchema.itemsChildren) nbItems := len(currentSubSchema.itemsChildren)
nbValues := len(value)
if nbItems == nbValues { // while we have both schemas and values, check them against each other
for i := 0; i != nbItems; i++ { for i := 0; i != nbItems && i != nbValues; i++ {
subContext := newJsonContext(strconv.Itoa(i), context) subContext := NewJsonContext(strconv.Itoa(i), context)
validationResult := currentSubSchema.itemsChildren[i].subValidateWithContext(value[i], subContext) validationResult := currentSubSchema.itemsChildren[i].subValidateWithContext(value[i], subContext)
result.mergeErrors(validationResult) result.mergeErrors(validationResult)
} }
} else if nbItems < nbValues {
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) { switch currentSubSchema.additionalItems.(type) {
case bool: case bool:
if !currentSubSchema.additionalItems.(bool) { if !currentSubSchema.additionalItems.(bool) {
result.addError(new(ArrayNoAdditionalItemsError), context, value, ErrorDetails{}) result.addInternalError(new(ArrayNoAdditionalItemsError), context, value, ErrorDetails{})
} }
case *subSchema: case *subSchema:
additionalItemSchema := currentSubSchema.additionalItems.(*subSchema) additionalItemSchema := currentSubSchema.additionalItems.(*subSchema)
for i := nbItems; i != nbValues; i++ { for i := nbItems; i != nbValues; i++ {
subContext := newJsonContext(strconv.Itoa(i), context) subContext := NewJsonContext(strconv.Itoa(i), context)
validationResult := additionalItemSchema.subValidateWithContext(value[i], subContext) validationResult := additionalItemSchema.subValidateWithContext(value[i], subContext)
result.mergeErrors(validationResult) result.mergeErrors(validationResult)
} }
@ -453,8 +494,8 @@ func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface
// minItems & maxItems // minItems & maxItems
if currentSubSchema.minItems != nil { if currentSubSchema.minItems != nil {
if nbItems < int(*currentSubSchema.minItems) { if nbValues < int(*currentSubSchema.minItems) {
result.addError( result.addInternalError(
new(ArrayMinItemsError), new(ArrayMinItemsError),
context, context,
value, value,
@ -463,8 +504,8 @@ func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface
} }
} }
if currentSubSchema.maxItems != nil { if currentSubSchema.maxItems != nil {
if nbItems > int(*currentSubSchema.maxItems) { if nbValues > int(*currentSubSchema.maxItems) {
result.addError( result.addInternalError(
new(ArrayMaxItemsError), new(ArrayMaxItemsError),
context, context,
value, value,
@ -476,27 +517,59 @@ func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface
// uniqueItems: // uniqueItems:
if currentSubSchema.uniqueItems { if currentSubSchema.uniqueItems {
var stringifiedItems []string var stringifiedItems []string
for _, v := range value { for j, v := range value {
vString, err := marshalToJsonString(v) vString, err := marshalWithoutNumber(v)
if err != nil { 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) { if i := indexStringInSlice(stringifiedItems, *vString); i > -1 {
result.addError( result.addInternalError(
new(ItemsMustBeUniqueError), new(ItemsMustBeUniqueError),
context, context,
value, value,
ErrorDetails{"type": TYPE_ARRAY}, ErrorDetails{"type": TYPE_ARRAY, "i": i, "j": j},
) )
} }
stringifiedItems = append(stringifiedItems, *vString) 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() 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 { if internalLogEnabled {
internalLog("validateObject %s", context.String()) internalLog("validateObject %s", context.String())
@ -506,7 +579,7 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string
// minProperties & maxProperties: // minProperties & maxProperties:
if currentSubSchema.minProperties != nil { if currentSubSchema.minProperties != nil {
if len(value) < int(*currentSubSchema.minProperties) { if len(value) < int(*currentSubSchema.minProperties) {
result.addError( result.addInternalError(
new(ArrayMinPropertiesError), new(ArrayMinPropertiesError),
context, context,
value, value,
@ -516,7 +589,7 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string
} }
if currentSubSchema.maxProperties != nil { if currentSubSchema.maxProperties != nil {
if len(value) > int(*currentSubSchema.maxProperties) { if len(value) > int(*currentSubSchema.maxProperties) {
result.addError( result.addInternalError(
new(ArrayMaxPropertiesError), new(ArrayMaxPropertiesError),
context, context,
value, value,
@ -531,7 +604,7 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string
if ok { if ok {
result.incrementScore() result.incrementScore()
} else { } else {
result.addError( result.addInternalError(
new(RequiredError), new(RequiredError),
context, context,
value, value,
@ -562,10 +635,10 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string
if found { if found {
if pp_has && !pp_match { if pp_has && !pp_match {
result.addError( result.addInternalError(
new(AdditionalPropertyNotAllowedError), new(AdditionalPropertyNotAllowedError),
context, context,
value, value[pk],
ErrorDetails{"property": pk}, ErrorDetails{"property": pk},
) )
} }
@ -573,10 +646,10 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string
} else { } else {
if !pp_has || !pp_match { if !pp_has || !pp_match {
result.addError( result.addInternalError(
new(AdditionalPropertyNotAllowedError), new(AdditionalPropertyNotAllowedError),
context, context,
value, value[pk],
ErrorDetails{"property": pk}, ErrorDetails{"property": pk},
) )
} }
@ -625,10 +698,10 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string
if pp_has && !pp_match { if pp_has && !pp_match {
result.addError( result.addInternalError(
new(InvalidPropertyPatternError), new(InvalidPropertyPatternError),
context, context,
value, value[pk],
ErrorDetails{ ErrorDetails{
"property": pk, "property": pk,
"pattern": currentSubSchema.PatternPropertiesString(), "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() 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 { if internalLogEnabled {
internalLog("validatePatternProperty %s", context.String()) internalLog("validatePatternProperty %s", context.String())
@ -656,14 +744,12 @@ func (v *subSchema) validatePatternProperty(currentSubSchema *subSchema, key str
for pk, pv := range currentSubSchema.patternProperties { for pk, pv := range currentSubSchema.patternProperties {
if matches, _ := regexp.MatchString(pk, key); matches { if matches, _ := regexp.MatchString(pk, key); matches {
has = true has = true
subContext := newJsonContext(key, context) subContext := NewJsonContext(key, context)
validationResult := pv.subValidateWithContext(value, subContext) validationResult := pv.subValidateWithContext(value, subContext)
result.mergeErrors(validationResult) result.mergeErrors(validationResult)
if validationResult.Valid() {
validatedkey = true validatedkey = true
} }
} }
}
if !validatedkey { if !validatedkey {
return has, false return has, false
@ -674,7 +760,7 @@ func (v *subSchema) validatePatternProperty(currentSubSchema *subSchema, key str
return has, true 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 // Ignore JSON numbers
if isJsonNumber(value) { if isJsonNumber(value) {
@ -696,7 +782,7 @@ func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{
// minLength & maxLength: // minLength & maxLength:
if currentSubSchema.minLength != nil { if currentSubSchema.minLength != nil {
if utf8.RuneCount([]byte(stringValue)) < int(*currentSubSchema.minLength) { if utf8.RuneCount([]byte(stringValue)) < int(*currentSubSchema.minLength) {
result.addError( result.addInternalError(
new(StringLengthGTEError), new(StringLengthGTEError),
context, context,
value, value,
@ -706,7 +792,7 @@ func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{
} }
if currentSubSchema.maxLength != nil { if currentSubSchema.maxLength != nil {
if utf8.RuneCount([]byte(stringValue)) > int(*currentSubSchema.maxLength) { if utf8.RuneCount([]byte(stringValue)) > int(*currentSubSchema.maxLength) {
result.addError( result.addInternalError(
new(StringLengthLTEError), new(StringLengthLTEError),
context, context,
value, value,
@ -718,7 +804,7 @@ func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{
// pattern: // pattern:
if currentSubSchema.pattern != nil { if currentSubSchema.pattern != nil {
if !currentSubSchema.pattern.MatchString(stringValue) { if !currentSubSchema.pattern.MatchString(stringValue) {
result.addError( result.addInternalError(
new(DoesNotMatchPatternError), new(DoesNotMatchPatternError),
context, context,
value, value,
@ -731,7 +817,7 @@ func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{
// format // format
if currentSubSchema.format != "" { if currentSubSchema.format != "" {
if !FormatCheckers.IsFormat(currentSubSchema.format, stringValue) { if !FormatCheckers.IsFormat(currentSubSchema.format, stringValue) {
result.addError( result.addInternalError(
new(DoesNotMatchFormatError), new(DoesNotMatchFormatError),
context, context,
value, value,
@ -743,7 +829,7 @@ func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{
result.incrementScore() 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 // Ignore non numbers
if !isJsonNumber(value) { if !isJsonNumber(value) {
@ -756,73 +842,83 @@ func (v *subSchema) validateNumber(currentSubSchema *subSchema, value interface{
} }
number := value.(json.Number) number := value.(json.Number)
float64Value, _ := number.Float64() float64Value, _ := new(big.Rat).SetString(string(number))
// multipleOf: // multipleOf:
if currentSubSchema.multipleOf != nil { if currentSubSchema.multipleOf != nil {
if q := new(big.Rat).Quo(float64Value, currentSubSchema.multipleOf); !q.IsInt() {
if !isFloat64AnInteger(float64Value / *currentSubSchema.multipleOf) { result.addInternalError(
result.addError(
new(MultipleOfError), new(MultipleOfError),
context, context,
resultErrorFormatJsonNumber(number), resultErrorFormatJsonNumber(number),
ErrorDetails{"multiple": *currentSubSchema.multipleOf}, ErrorDetails{"multiple": new(big.Float).SetRat(currentSubSchema.multipleOf)},
) )
} }
} }
//maximum & exclusiveMaximum: //maximum & exclusiveMaximum:
if currentSubSchema.maximum != nil { if currentSubSchema.maximum != nil {
if currentSubSchema.exclusiveMaximum { if float64Value.Cmp(currentSubSchema.maximum) == 1 {
if float64Value >= *currentSubSchema.maximum { result.addInternalError(
result.addError(
new(NumberLTError),
context,
resultErrorFormatJsonNumber(number),
ErrorDetails{
"max": resultErrorFormatNumber(*currentSubSchema.maximum),
},
)
}
} else {
if float64Value > *currentSubSchema.maximum {
result.addError(
new(NumberLTEError), new(NumberLTEError),
context, context,
resultErrorFormatJsonNumber(number), resultErrorFormatJsonNumber(number),
ErrorDetails{ ErrorDetails{
"max": resultErrorFormatNumber(*currentSubSchema.maximum), "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: //minimum & exclusiveMinimum:
if currentSubSchema.minimum != nil { if currentSubSchema.minimum != nil {
if currentSubSchema.exclusiveMinimum { if float64Value.Cmp(currentSubSchema.minimum) == -1 {
if float64Value <= *currentSubSchema.minimum { result.addInternalError(
result.addError(
new(NumberGTError),
context,
resultErrorFormatJsonNumber(number),
ErrorDetails{
"min": resultErrorFormatNumber(*currentSubSchema.minimum),
},
)
}
} else {
if float64Value < *currentSubSchema.minimum {
result.addError(
new(NumberGTEError), new(NumberGTEError),
context, context,
resultErrorFormatJsonNumber(number), resultErrorFormatJsonNumber(number),
ErrorDetails{ ErrorDetails{
"min": resultErrorFormatNumber(*currentSubSchema.minimum), "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},
)
}
} }
result.incrementScore() result.incrementScore()