1
0
mirror of https://github.com/rancher/os.git synced 2025-07-07 11:58:38 +00:00

Merge pull request #1989 from SvenDowideit/fix-validation-tests

Fix validation tests, update deps and use the rancher/docker version …
This commit is contained in:
Sven Dowideit 2017-07-17 23:04:24 +10:00 committed by GitHub
commit 2cd3cb2442
20 changed files with 463 additions and 286 deletions

View File

@ -53,7 +53,7 @@ var schema = `{
"defaults": {"$ref": "#/definitions/defaults_config"}, "defaults": {"$ref": "#/definitions/defaults_config"},
"resize_device": {"type": "string"}, "resize_device": {"type": "string"},
"sysctl": {"type": "object"}, "sysctl": {"type": "object"},
"restart_services": {"type": "array"} "restart_services": {"type": "array"},
"hypervisor_service": {"type": "boolean"} "hypervisor_service": {"type": "boolean"}
} }
}, },

View File

@ -10,7 +10,8 @@ import (
) )
func testValidate(t *testing.T, cfg []byte, contains string) { func testValidate(t *testing.T, cfg []byte, contains string) {
validationErrors, err := Validate(cfg) fmt.Printf("Testing %s, contains %s", string(cfg), contains)
validationErrors, err := ValidateBytes(cfg)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
set -e set -ex
cd $(dirname $0)/.. cd $(dirname $0)/..

View File

@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
set -e set -ex
cd $(dirname $0)/.. cd $(dirname $0)/..
source ./scripts/version source ./scripts/version

View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# help: Run go unit tests # help: Run go unit tests
set -e set -ex
cd $(dirname $0)/.. cd $(dirname $0)/..

View File

@ -12,7 +12,7 @@ github.com/coreos/yaml 6b16a5714269b2f70720a45406b1babd947a17ef
github.com/davecgh/go-spew 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d github.com/davecgh/go-spew 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
github.com/docker/containerd b7a26a1c481d5ba88a2df757954a54439142ceb1 https://github.com/ibuildthecloud/containerd.git github.com/docker/containerd b7a26a1c481d5ba88a2df757954a54439142ceb1 https://github.com/ibuildthecloud/containerd.git
github.com/docker/distribution 467fc068d88aa6610691b7f1a677271a3fac4aac github.com/docker/distribution 467fc068d88aa6610691b7f1a677271a3fac4aac
github.com/docker/docker bf16bd9dcfc3c9fafb7eb7b39ae7ef7abf1ae7f1 https://github.com/rancher/docker.git github.com/docker/docker b40c87254f587af7cad8e8128f061a2a7f367343 https://github.com/rancher/docker.git
github.com/docker/engine-api v0.3.3 github.com/docker/engine-api v0.3.3
github.com/docker/go-connections v0.2.0 github.com/docker/go-connections v0.2.0
github.com/docker/go-units 651fc226e7441360384da338d0fd37f2440ffbe3 github.com/docker/go-units 651fc226e7441360384da338d0fd37f2440ffbe3
@ -50,9 +50,9 @@ github.com/tchap/go-patricia v2.1.0
github.com/vbatts/tar-split v0.9.11 github.com/vbatts/tar-split v0.9.11
github.com/vishvananda/netlink fe3b5664d23a11b52ba59bece4ff29c52772a56b github.com/vishvananda/netlink fe3b5664d23a11b52ba59bece4ff29c52772a56b
github.com/vishvananda/netns 54f0e4339ce73702a0607f49922aaa1e749b418d github.com/vishvananda/netns 54f0e4339ce73702a0607f49922aaa1e749b418d
github.com/xeipuuv/gojsonpointer e0fe6f68307607d540ed8eac07a342c33fa1b54a github.com/xeipuuv/gojsonpointer 6fe8760cad3569743d51ddbb243b26f8456742dc
github.com/xeipuuv/gojsonreference e02fc20de94c78484cd5ffb007f8af96be030a45 github.com/xeipuuv/gojsonreference e02fc20de94c78484cd5ffb007f8af96be030a45
github.com/xeipuuv/gojsonschema ac452913faa25c08bb78810d3e6f88b8a39f8f25 github.com/xeipuuv/gojsonschema 0c8571ac0ce161a5feb57375a9cdf148c98c0f70
github.com/SvenDowideit/cpuid dfdb6dba69f48dd44c5cd831950be648f71162ca github.com/SvenDowideit/cpuid dfdb6dba69f48dd44c5cd831950be648f71162ca
golang.org/x/crypto 2f3083f6163ef51179ad42ed523a18c9a1141467 golang.org/x/crypto 2f3083f6163ef51179ad42ed523a18c9a1141467
golang.org/x/net 991d3e32f76f19ee6d9caadb3a22eae8d23315f7 https://github.com/golang/net.git golang.org/x/net 991d3e32f76f19ee6d9caadb3a22eae8d23315f7 https://github.com/golang/net.git

View File

@ -52,35 +52,24 @@ type implStruct struct {
outError error outError error
} }
func NewJsonPointer(jsonPointerString string) (JsonPointer, error) {
var p JsonPointer
err := p.parse(jsonPointerString)
return p, err
}
type JsonPointer struct { type JsonPointer struct {
referenceTokens []string referenceTokens []string
} }
// "Constructor", parses the given string JSON pointer // NewJsonPointer parses the given string JSON pointer and returns an object
func (p *JsonPointer) parse(jsonPointerString string) error { func NewJsonPointer(jsonPointerString string) (p JsonPointer, err error) {
var err error // Pointer to the root of the document
if len(jsonPointerString) == 0 {
if jsonPointerString != const_empty_pointer { // Keep referenceTokens nil
if !strings.HasPrefix(jsonPointerString, const_pointer_separator) { return
err = errors.New(const_invalid_start)
} else {
referenceTokens := strings.Split(jsonPointerString, const_pointer_separator)
for _, referenceToken := range referenceTokens[1:] {
p.referenceTokens = append(p.referenceTokens, referenceToken)
}
} }
if jsonPointerString[0] != '/' {
return p, errors.New(const_invalid_start)
} }
return err p.referenceTokens = strings.Split(jsonPointerString[1:], const_pointer_separator)
return
} }
// Uses the pointer to retrieve a value from a JSON document // Uses the pointer to retrieve a value from a JSON document
@ -119,64 +108,55 @@ func (p *JsonPointer) implementation(i *implStruct) {
for ti, token := range p.referenceTokens { for ti, token := range p.referenceTokens {
decodedToken := decodeReferenceToken(token)
isLastToken := ti == len(p.referenceTokens)-1 isLastToken := ti == len(p.referenceTokens)-1
rValue := reflect.ValueOf(node) switch v := node.(type) {
kind = rValue.Kind()
switch kind { case map[string]interface{}:
decodedToken := decodeReferenceToken(token)
case reflect.Map: if _, ok := v[decodedToken]; ok {
m := node.(map[string]interface{}) node = v[decodedToken]
if _, ok := m[decodedToken]; ok {
node = m[decodedToken]
if isLastToken && i.mode == "SET" { if isLastToken && i.mode == "SET" {
m[decodedToken] = i.setInValue v[decodedToken] = i.setInValue
} }
} else { } else {
i.outError = errors.New(fmt.Sprintf("Object has no key '%s'", token)) i.outError = fmt.Errorf("Object has no key '%s'", decodedToken)
i.getOutKind = kind i.getOutKind = reflect.Map
i.getOutNode = nil i.getOutNode = nil
return return
} }
case reflect.Slice: case []interface{}:
s := node.([]interface{})
tokenIndex, err := strconv.Atoi(token) tokenIndex, err := strconv.Atoi(token)
if err != nil { if err != nil {
i.outError = errors.New(fmt.Sprintf("Invalid array index '%s'", token)) i.outError = fmt.Errorf("Invalid array index '%s'", token)
i.getOutKind = kind i.getOutKind = reflect.Slice
i.getOutNode = nil i.getOutNode = nil
return return
} }
sLength := len(s) if tokenIndex < 0 || tokenIndex >= len(v) {
if tokenIndex < 0 || tokenIndex >= sLength { i.outError = fmt.Errorf("Out of bound array[0,%d] index '%d'", len(v), tokenIndex)
i.outError = errors.New(fmt.Sprintf("Out of bound array[0,%d] index '%d'", sLength, tokenIndex)) i.getOutKind = reflect.Slice
i.getOutKind = kind
i.getOutNode = nil i.getOutNode = nil
return return
} }
node = s[tokenIndex] node = v[tokenIndex]
if isLastToken && i.mode == "SET" { if isLastToken && i.mode == "SET" {
s[tokenIndex] = i.setInValue v[tokenIndex] = i.setInValue
} }
default: default:
i.outError = errors.New(fmt.Sprintf("Invalid token reference '%s'", token)) i.outError = fmt.Errorf("Invalid token reference '%s'", token)
i.getOutKind = kind i.getOutKind = reflect.ValueOf(node).Kind()
i.getOutNode = nil i.getOutNode = nil
return return
} }
} }
rValue := reflect.ValueOf(node)
kind = rValue.Kind()
i.getOutNode = node i.getOutNode = node
i.getOutKind = kind i.getOutKind = reflect.ValueOf(node).Kind()
i.outError = nil i.outError = nil
} }
@ -197,21 +177,14 @@ func (p *JsonPointer) String() string {
// ~1 => / // ~1 => /
// ... and vice versa // ... and vice versa
const (
const_encoded_reference_token_0 = `~0`
const_encoded_reference_token_1 = `~1`
const_decoded_reference_token_0 = `~`
const_decoded_reference_token_1 = `/`
)
func decodeReferenceToken(token string) string { func decodeReferenceToken(token string) string {
step1 := strings.Replace(token, const_encoded_reference_token_1, const_decoded_reference_token_1, -1) step1 := strings.Replace(token, `~1`, `/`, -1)
step2 := strings.Replace(step1, const_encoded_reference_token_0, const_decoded_reference_token_0, -1) step2 := strings.Replace(step1, `~0`, `~`, -1)
return step2 return step2
} }
func encodeReferenceToken(token string) string { func encodeReferenceToken(token string) string {
step1 := strings.Replace(token, const_decoded_reference_token_1, const_encoded_reference_token_1, -1) step1 := strings.Replace(token, `~`, `~0`, -1)
step2 := strings.Replace(step1, const_decoded_reference_token_0, const_encoded_reference_token_0, -1) step2 := strings.Replace(step1, `/`, `~1`, -1)
return step2 return step2
} }

View File

@ -197,17 +197,38 @@ Note: An error of RequiredType has an err.Type() return value of "required"
**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 strings 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. Available formats: date-time, hostname, email, ipv4, ipv6, uri, uri-reference.
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:

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 {
@ -227,16 +237,47 @@ func newError(err ResultError, context *jsonContext, value interface{}, locale l
err.SetValue(value) err.SetValue(value)
err.SetDetails(details) err.SetDetails(details)
details["field"] = err.Field() details["field"] = err.Field()
if _, exists := details["context"]; !exists && context != nil {
details["context"] = context.String()
}
err.SetDescription(formatErrorDescription(d, details)) err.SetDescription(formatErrorDescription(d, details))
} }
// formatErrorDescription takes a string in this format: %field% is required // formatErrorDescription takes a string in the default text/template
// and converts it to a string with replacements. The fields come from // format and converts it to a string with replacements. The fields come
// the ErrorDetails struct and vary for each type of error. // 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

@ -5,6 +5,7 @@ import (
"net/url" "net/url"
"reflect" "reflect"
"regexp" "regexp"
"strings"
"time" "time"
) )
@ -51,14 +52,20 @@ 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 // 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{}
// 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{}
) )
var ( var (
@ -72,7 +79,9 @@ var (
"ipv4": IPV4FormatChecker{}, "ipv4": IPV4FormatChecker{},
"ipv6": IPV6FormatChecker{}, "ipv6": IPV6FormatChecker{},
"uri": URIFormatChecker{}, "uri": URIFormatChecker{},
"uri-reference": URIReferenceFormatChecker{},
"uuid": UUIDFormatChecker{}, "uuid": UUIDFormatChecker{},
"regex": RegexFormatChecker{},
}, },
} }
@ -80,7 +89,7 @@ var (
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}])))\\.?$") 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\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[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]))*$`)
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}$")
) )
@ -132,13 +141,13 @@ func (f EmailFormatChecker) IsFormat(input string) bool {
// Credit: https://github.com/asaskevich/govalidator // Credit: https://github.com/asaskevich/govalidator
func (f IPV4FormatChecker) IsFormat(input string) bool { func (f IPV4FormatChecker) IsFormat(input string) bool {
ip := net.ParseIP(input) ip := net.ParseIP(input)
return ip != nil && ip.To4() != nil return ip != nil && strings.Contains(input, ".")
} }
// Credit: https://github.com/asaskevich/govalidator // Credit: https://github.com/asaskevich/govalidator
func (f IPV6FormatChecker) IsFormat(input string) bool { func (f IPV6FormatChecker) IsFormat(input string) bool {
ip := net.ParseIP(input) ip := net.ParseIP(input)
return ip != nil && ip.To4() == nil return ip != nil && strings.Contains(input, ":")
} }
func (f DateTimeFormatChecker) IsFormat(input string) bool { func (f DateTimeFormatChecker) IsFormat(input string) bool {
@ -168,10 +177,27 @@ func (f URIFormatChecker) IsFormat(input string) bool {
return true return true
} }
func (f URIReferenceFormatChecker) IsFormat(input string) bool {
_, err := url.Parse(input)
return err == nil
}
func (f HostnameFormatChecker) IsFormat(input string) bool { func (f HostnameFormatChecker) IsFormat(input string) bool {
return rxHostname.MatchString(input) return rxHostname.MatchString(input) && len(input) < 256
} }
func (f UUIDFormatChecker) IsFormat(input string) bool { func (f UUIDFormatChecker) IsFormat(input string) bool {
return rxUUID.MatchString(input) return rxUUID.MatchString(input)
} }
// IsFormat implements FormatChecker interface.
func (f RegexFormatChecker) IsFormat(input string) bool {
if input == "" {
return true
}
_, err := regexp.Compile(input)
if err != nil {
return false
}
return true
}

12
vendor/github.com/xeipuuv/gojsonschema/glide.yaml generated vendored Normal file
View File

@ -0,0 +1,12 @@
package: github.com/xeipuuv/gojsonschema
license: Apache 2.0
import:
- package: github.com/xeipuuv/gojsonschema
- package: github.com/xeipuuv/gojsonpointer
- package: github.com/xeipuuv/gojsonreference
- package: github.com/stretchr/testify/assert
version: ^1.1.3

View File

@ -33,41 +33,101 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"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) *jsonReferenceLoader {
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) *jsonReferenceLoader {
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,7 +139,7 @@ func (l *jsonReferenceLoader) loadJSON() (interface{}, error) {
if reference.HasFileScheme { if reference.HasFileScheme {
filename := strings.Replace(refToUrl.String(), "file://", "", -1) filename := strings.Replace(refToUrl.GetUrl().Path, "file://", "", -1)
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
@ -87,7 +147,6 @@ func (l *jsonReferenceLoader) loadJSON() (interface{}, error) {
filename = filename[1:] 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,33 +167,6 @@ 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) {
resp, err := http.Get(address) resp, err := http.Get(address)
@ -144,7 +176,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)
@ -157,8 +189,13 @@ func (l *jsonReferenceLoader) loadFromHTTP(address string) (interface{}, error)
} }
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 +210,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 (l *jsonStringLoader) JsonReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference("#")
}
func (l *jsonStringLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
func NewStringLoader(source string) *jsonStringLoader { func NewStringLoader(source string) *jsonStringLoader {
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) *jsonBytesLoader {
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 +265,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 (l *jsonGoLoader) JsonReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference("#")
}
func (l *jsonGoLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
func NewGoLoader(source interface{}) *jsonGoLoader { func NewGoLoader(source interface{}) *jsonGoLoader {
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 +294,34 @@ 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) (*jsonIOLoader, 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) (*jsonIOLoader, 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{}
} }
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
@ -37,6 +37,7 @@ type (
MissingDependency() string MissingDependency() string
Internal() string Internal() string
Enum() string Enum() string
ArrayNotEnoughItems() string
ArrayNoAdditionalItems() string ArrayNoAdditionalItems() string
ArrayMinItems() string ArrayMinItems() string
ArrayMaxItems() string ArrayMaxItems() string
@ -72,7 +73,8 @@ type (
ReferenceMustBeCanonical() string ReferenceMustBeCanonical() string
NotAValidType() string NotAValidType() string
Duplicated() string Duplicated() string
httpBadStatus() string HttpBadStatus() string
ParseError() string
// ErrorFormat // ErrorFormat
ErrorFormat() string ErrorFormat() string
@ -83,11 +85,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,157 +109,166 @@ 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) 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 must be unique`
} }
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) 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`
} }
const ( const (

View File

@ -48,6 +48,7 @@ type (
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.
@ -126,7 +127,7 @@ func (v ResultErrorFields) String() string {
valueString := fmt.Sprintf("%v", v.value) valueString := fmt.Sprintf("%v", v.value)
// marshal the go value value to json // marshal the go value value to json
if v.Value == nil { if v.value == nil {
valueString = TYPE_NULL valueString = TYPE_NULL
} else { } else {
if vs, err := marshalToJsonString(v.value); err == nil { if vs, err := marshalToJsonString(v.value); err == nil {

View File

@ -31,6 +31,7 @@ import (
"errors" "errors"
"reflect" "reflect"
"regexp" "regexp"
"text/template"
"github.com/xeipuuv/gojsonreference" "github.com/xeipuuv/gojsonreference"
) )
@ -39,10 +40,45 @@ 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() ref, err := l.JsonReference()
if err != nil {
return nil, err
}
d := Schema{}
d.pool = newSchemaPool(l.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 = l.LoadJSON()
if err != nil {
return nil, err
}
d.pool.SetStandaloneDocument(doc)
}
err = d.parse(doc)
if err != nil {
return nil, err
}
return &d, nil
} }
type Schema struct { type Schema struct {
@ -71,10 +107,9 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema)
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,
}, },
)) ))
} }
@ -116,14 +151,27 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema)
} }
if k, ok := m[KEY_REF].(string); ok { if k, ok := m[KEY_REF].(string); ok {
if sch, ok := d.referencePool.Get(currentSchema.ref.String() + k); ok { jsonReference, err := gojsonreference.NewJsonReference(k)
if err != nil {
return err
}
if jsonReference.HasFullUrl {
currentSchema.ref = &jsonReference
} else {
inheritedReference, err := currentSchema.ref.Inherits(jsonReference)
if err != nil {
return err
}
currentSchema.ref = inheritedReference
}
if sch, ok := d.referencePool.Get(currentSchema.ref.String() + k); ok {
currentSchema.refSchema = sch currentSchema.refSchema = sch
} else { } else {
err := d.parseReference(documentNode, currentSchema, k)
var err error
err = d.parseReference(documentNode, currentSchema, k)
if err != nil { if err != nil {
return err return err
} }
@ -755,30 +803,10 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema)
return nil return nil
} }
func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSchema, reference string) (e error) { func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSchema, reference string) error {
var err error
jsonReference, err := gojsonreference.NewJsonReference(reference)
if err != nil {
return err
}
standaloneDocument := d.pool.GetStandaloneDocument()
if jsonReference.HasFullUrl {
currentSchema.ref = &jsonReference
} else {
inheritedReference, err := currentSchema.ref.Inherits(jsonReference)
if err != nil {
return err
}
currentSchema.ref = inheritedReference
}
jsonPointer := currentSchema.ref.GetPointer()
var refdDocumentNode interface{} var refdDocumentNode interface{}
jsonPointer := currentSchema.ref.GetPointer()
standaloneDocument := d.pool.GetStandaloneDocument()
if standaloneDocument != nil { if standaloneDocument != nil {
@ -789,8 +817,6 @@ func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSche
} }
} else { } else {
var err error
dsp, err := d.pool.GetDocument(*currentSchema.ref) dsp, err := d.pool.GetDocument(*currentSchema.ref)
if err != nil { if err != nil {
return err return err
@ -812,11 +838,10 @@ func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSche
// returns the loaded referenced subSchema for the caller to update its current subSchema // returns the loaded referenced subSchema for the caller to update its current subSchema
newSchemaDocument := refdDocumentNode.(map[string]interface{}) newSchemaDocument := refdDocumentNode.(map[string]interface{})
newSchema := &subSchema{property: KEY_REF, parent: currentSchema, ref: currentSchema.ref} newSchema := &subSchema{property: KEY_REF, parent: currentSchema, ref: currentSchema.ref}
d.referencePool.Add(currentSchema.ref.String()+reference, newSchema) d.referencePool.Add(currentSchema.ref.String()+reference, newSchema)
err = d.parseSchema(newSchemaDocument, newSchema) err := d.parseSchema(newSchemaDocument, newSchema)
if err != nil { if err != nil {
return err return err
} }

View File

@ -39,13 +39,15 @@ type schemaPoolDocument struct {
type schemaPool struct { type schemaPool struct {
schemaPoolDocuments map[string]*schemaPoolDocument schemaPoolDocuments map[string]*schemaPoolDocument
standaloneDocument interface{} standaloneDocument interface{}
jsonLoaderFactory JSONLoaderFactory
} }
func newSchemaPool() *schemaPool { func newSchemaPool(f JSONLoaderFactory) *schemaPool {
p := &schemaPool{} p := &schemaPool{}
p.schemaPoolDocuments = make(map[string]*schemaPoolDocument) p.schemaPoolDocuments = make(map[string]*schemaPoolDocument)
p.standaloneDocument = nil p.standaloneDocument = nil
p.jsonLoaderFactory = f
return p return p
} }
@ -93,8 +95,8 @@ func (p *schemaPool) GetDocument(reference gojsonreference.JsonReference) (*sche
return spd, nil return spd, nil
} }
jsonReferenceLoader := NewReferenceLoader(reference.String()) jsonReferenceLoader := p.jsonLoaderFactory.New(reference.String())
document, err := jsonReferenceLoader.loadJSON() document, err := jsonReferenceLoader.LoadJSON()
if err != nil { if err != nil {
return nil, err return nil, err
} }

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

@ -214,7 +214,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

@ -34,7 +34,12 @@ import (
) )
func isKind(what interface{}, kind reflect.Kind) bool { func isKind(what interface{}, kind reflect.Kind) bool {
return reflect.ValueOf(what).Kind() == kind target := what
if isJsonNumber(what) {
// JSON Numbers are strings!
target = *mustBeNumber(what)
}
return reflect.ValueOf(target).Kind() == kind
} }
func existsMapKey(m map[string]interface{}, k string) bool { func existsMapKey(m map[string]interface{}, k string) bool {
@ -77,13 +82,14 @@ func checkJsonNumber(what interface{}) (isValidFloat64 bool, isValidInt64 bool,
jsonNumber := what.(json.Number) jsonNumber := what.(json.Number)
_, errFloat64 := jsonNumber.Float64() f64, errFloat64 := jsonNumber.Float64()
_, errInt64 := jsonNumber.Int64() s64 := strconv.FormatFloat(f64, 'f', -1, 64)
_, errInt64 := strconv.ParseInt(s64, 10, 64)
isValidFloat64 = errFloat64 == nil isValidFloat64 = errFloat64 == nil
isValidInt64 = errInt64 == nil isValidInt64 = errInt64 == nil
_, errInt32 := strconv.ParseInt(jsonNumber.String(), 10, 32) _, errInt32 := strconv.ParseInt(s64, 10, 32)
isValidInt32 = isValidInt64 && errInt32 == nil isValidInt32 = isValidInt64 && errInt32 == nil
return return

View File

@ -55,7 +55,7 @@ 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
} }
@ -412,7 +412,7 @@ func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface
internalLog(" %v", value) internalLog(" %v", value)
} }
nbItems := len(value) nbValues := len(value)
// TODO explain // TODO explain
if currentSubSchema.itemsChildrenIsSingleSchema { if currentSubSchema.itemsChildrenIsSingleSchema {
@ -425,15 +425,18 @@ 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) {
@ -453,7 +456,7 @@ 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.addError(
new(ArrayMinItemsError), new(ArrayMinItemsError),
context, context,
@ -463,7 +466,7 @@ 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.addError(
new(ArrayMaxItemsError), new(ArrayMaxItemsError),
context, context,
@ -565,7 +568,7 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string
result.addError( result.addError(
new(AdditionalPropertyNotAllowedError), new(AdditionalPropertyNotAllowedError),
context, context,
value, value[pk],
ErrorDetails{"property": pk}, ErrorDetails{"property": pk},
) )
} }
@ -576,7 +579,7 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string
result.addError( result.addError(
new(AdditionalPropertyNotAllowedError), new(AdditionalPropertyNotAllowedError),
context, context,
value, value[pk],
ErrorDetails{"property": pk}, ErrorDetails{"property": pk},
) )
} }
@ -628,7 +631,7 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string
result.addError( result.addError(
new(InvalidPropertyPatternError), new(InvalidPropertyPatternError),
context, context,
value, value[pk],
ErrorDetails{ ErrorDetails{
"property": pk, "property": pk,
"pattern": currentSubSchema.PatternPropertiesString(), "pattern": currentSubSchema.PatternPropertiesString(),
@ -776,22 +779,22 @@ func (v *subSchema) validateNumber(currentSubSchema *subSchema, value interface{
if currentSubSchema.exclusiveMaximum { if currentSubSchema.exclusiveMaximum {
if float64Value >= *currentSubSchema.maximum { if float64Value >= *currentSubSchema.maximum {
result.addError( result.addError(
new(NumberLTEError), new(NumberLTError),
context, context,
resultErrorFormatJsonNumber(number), resultErrorFormatJsonNumber(number),
ErrorDetails{ ErrorDetails{
"min": resultErrorFormatNumber(*currentSubSchema.maximum), "max": resultErrorFormatNumber(*currentSubSchema.maximum),
}, },
) )
} }
} else { } else {
if float64Value > *currentSubSchema.maximum { if float64Value > *currentSubSchema.maximum {
result.addError( result.addError(
new(NumberLTError), new(NumberLTEError),
context, context,
resultErrorFormatJsonNumber(number), resultErrorFormatJsonNumber(number),
ErrorDetails{ ErrorDetails{
"min": resultErrorFormatNumber(*currentSubSchema.maximum), "max": resultErrorFormatNumber(*currentSubSchema.maximum),
}, },
) )
} }
@ -803,22 +806,22 @@ func (v *subSchema) validateNumber(currentSubSchema *subSchema, value interface{
if currentSubSchema.exclusiveMinimum { if currentSubSchema.exclusiveMinimum {
if float64Value <= *currentSubSchema.minimum { if float64Value <= *currentSubSchema.minimum {
result.addError( result.addError(
new(NumberGTEError), new(NumberGTError),
context, context,
resultErrorFormatJsonNumber(number), resultErrorFormatJsonNumber(number),
ErrorDetails{ ErrorDetails{
"max": resultErrorFormatNumber(*currentSubSchema.minimum), "min": resultErrorFormatNumber(*currentSubSchema.minimum),
}, },
) )
} }
} else { } else {
if float64Value < *currentSubSchema.minimum { if float64Value < *currentSubSchema.minimum {
result.addError( result.addError(
new(NumberGTError), new(NumberGTEError),
context, context,
resultErrorFormatJsonNumber(number), resultErrorFormatJsonNumber(number),
ErrorDetails{ ErrorDetails{
"max": resultErrorFormatNumber(*currentSubSchema.minimum), "min": resultErrorFormatNumber(*currentSubSchema.minimum),
}, },
) )
} }