1
0
mirror of https://github.com/rancher/os.git synced 2025-07-06 19:38:37 +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"},
"resize_device": {"type": "string"},
"sysctl": {"type": "object"},
"restart_services": {"type": "array"}
"restart_services": {"type": "array"},
"hypervisor_service": {"type": "boolean"}
}
},

View File

@ -10,7 +10,8 @@ import (
)
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 {
t.Fatal(err)
}

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ github.com/coreos/yaml 6b16a5714269b2f70720a45406b1babd947a17ef
github.com/davecgh/go-spew 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
github.com/docker/containerd b7a26a1c481d5ba88a2df757954a54439142ceb1 https://github.com/ibuildthecloud/containerd.git
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/go-connections v0.2.0
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/vishvananda/netlink fe3b5664d23a11b52ba59bece4ff29c52772a56b
github.com/vishvananda/netns 54f0e4339ce73702a0607f49922aaa1e749b418d
github.com/xeipuuv/gojsonpointer e0fe6f68307607d540ed8eac07a342c33fa1b54a
github.com/xeipuuv/gojsonpointer 6fe8760cad3569743d51ddbb243b26f8456742dc
github.com/xeipuuv/gojsonreference e02fc20de94c78484cd5ffb007f8af96be030a45
github.com/xeipuuv/gojsonschema ac452913faa25c08bb78810d3e6f88b8a39f8f25
github.com/xeipuuv/gojsonschema 0c8571ac0ce161a5feb57375a9cdf148c98c0f70
github.com/SvenDowideit/cpuid dfdb6dba69f48dd44c5cd831950be648f71162ca
golang.org/x/crypto 2f3083f6163ef51179ad42ed523a18c9a1141467
golang.org/x/net 991d3e32f76f19ee6d9caadb3a22eae8d23315f7 https://github.com/golang/net.git

View File

@ -52,35 +52,24 @@ type implStruct struct {
outError error
}
func NewJsonPointer(jsonPointerString string) (JsonPointer, error) {
var p JsonPointer
err := p.parse(jsonPointerString)
return p, err
}
type JsonPointer struct {
referenceTokens []string
}
// "Constructor", parses the given string JSON pointer
func (p *JsonPointer) parse(jsonPointerString string) error {
// NewJsonPointer parses the given string JSON pointer and returns an object
func NewJsonPointer(jsonPointerString string) (p JsonPointer, err error) {
var err error
if jsonPointerString != const_empty_pointer {
if !strings.HasPrefix(jsonPointerString, const_pointer_separator) {
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)
}
}
// Pointer to the root of the document
if len(jsonPointerString) == 0 {
// Keep referenceTokens nil
return
}
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
@ -119,64 +108,55 @@ func (p *JsonPointer) implementation(i *implStruct) {
for ti, token := range p.referenceTokens {
decodedToken := decodeReferenceToken(token)
isLastToken := ti == len(p.referenceTokens)-1
rValue := reflect.ValueOf(node)
kind = rValue.Kind()
switch v := node.(type) {
switch kind {
case reflect.Map:
m := node.(map[string]interface{})
if _, ok := m[decodedToken]; ok {
node = m[decodedToken]
case map[string]interface{}:
decodedToken := decodeReferenceToken(token)
if _, ok := v[decodedToken]; ok {
node = v[decodedToken]
if isLastToken && i.mode == "SET" {
m[decodedToken] = i.setInValue
v[decodedToken] = i.setInValue
}
} else {
i.outError = errors.New(fmt.Sprintf("Object has no key '%s'", token))
i.getOutKind = kind
i.outError = fmt.Errorf("Object has no key '%s'", decodedToken)
i.getOutKind = reflect.Map
i.getOutNode = nil
return
}
case reflect.Slice:
s := node.([]interface{})
case []interface{}:
tokenIndex, err := strconv.Atoi(token)
if err != nil {
i.outError = errors.New(fmt.Sprintf("Invalid array index '%s'", token))
i.getOutKind = kind
i.outError = fmt.Errorf("Invalid array index '%s'", token)
i.getOutKind = reflect.Slice
i.getOutNode = nil
return
}
sLength := len(s)
if tokenIndex < 0 || tokenIndex >= sLength {
i.outError = errors.New(fmt.Sprintf("Out of bound array[0,%d] index '%d'", sLength, tokenIndex))
i.getOutKind = kind
if tokenIndex < 0 || tokenIndex >= len(v) {
i.outError = fmt.Errorf("Out of bound array[0,%d] index '%d'", len(v), tokenIndex)
i.getOutKind = reflect.Slice
i.getOutNode = nil
return
}
node = s[tokenIndex]
node = v[tokenIndex]
if isLastToken && i.mode == "SET" {
s[tokenIndex] = i.setInValue
v[tokenIndex] = i.setInValue
}
default:
i.outError = errors.New(fmt.Sprintf("Invalid token reference '%s'", token))
i.getOutKind = kind
i.outError = fmt.Errorf("Invalid token reference '%s'", token)
i.getOutKind = reflect.ValueOf(node).Kind()
i.getOutNode = nil
return
}
}
rValue := reflect.ValueOf(node)
kind = rValue.Kind()
i.getOutNode = node
i.getOutKind = kind
i.getOutKind = reflect.ValueOf(node).Kind()
i.outError = nil
}
@ -197,21 +177,14 @@ func (p *JsonPointer) String() string {
// ~1 => /
// ... 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 {
step1 := strings.Replace(token, const_encoded_reference_token_1, const_decoded_reference_token_1, -1)
step2 := strings.Replace(step1, const_encoded_reference_token_0, const_decoded_reference_token_0, -1)
step1 := strings.Replace(token, `~1`, `/`, -1)
step2 := strings.Replace(step1, `~0`, `~`, -1)
return step2
}
func encodeReferenceToken(token string) string {
step1 := strings.Replace(token, const_decoded_reference_token_1, const_encoded_reference_token_1, -1)
step2 := strings.Replace(step1, const_decoded_reference_token_0, const_encoded_reference_token_0, -1)
step1 := strings.Replace(token, `~`, `~0`, -1)
step2 := strings.Replace(step1, `/`, `~1`, -1)
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()*
Note in most cases, the err.Details() will be used to generate replacement strings in your locales. and not used directly i.e.
Note in most cases, the err.Details() will be used to generate replacement strings in your locales, and not used directly. These strings follow the text/template format i.e.
```
%field% must be greater than or equal to %min%
{{.field}} must be greater than or equal to {{.min}}
```
The library allows you to specify custom template functions, should you require more complex error message handling.
```go
gojsonschema.ErrorTemplateFuncs = map[string]interface{}{
"allcaps": func(s string) string {
return strings.ToUpper(s)
},
}
```
Given the above definition, you can use the custom function `"allcaps"` in your localization templates:
```
{{allcaps .field}} must be greater than or equal to {{.min}}
```
The above error message would then be rendered with the `field` value in capital letters. For example:
```
"PASSWORD must be greater than or equal to 8"
```
Learn more about what types of template functions you can use in `ErrorTemplateFuncs` by referring to Go's [text/template FuncMap](https://golang.org/pkg/text/template/#FuncMap) type.
## Formats
JSON Schema allows for optional "format" property to validate strings against well-known formats. gojsonschema ships with all of the formats defined in the spec that you can use like this:
````json
{"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:

View File

@ -1,10 +1,20 @@
package gojsonschema
import (
"fmt"
"strings"
"bytes"
"sync"
"text/template"
)
var errorTemplates errorTemplate = errorTemplate{template.New("errors-new"), sync.RWMutex{}}
// template.Template is not thread-safe for writing, so some locking is done
// sync.RWMutex is used for efficiently locking when new templates are created
type errorTemplate struct {
*template.Template
sync.RWMutex
}
type (
// RequiredError. ErrorDetails: property string
RequiredError struct {
@ -227,16 +237,47 @@ func newError(err ResultError, context *jsonContext, value interface{}, locale l
err.SetValue(value)
err.SetDetails(details)
details["field"] = err.Field()
if _, exists := details["context"]; !exists && context != nil {
details["context"] = context.String()
}
err.SetDescription(formatErrorDescription(d, details))
}
// formatErrorDescription takes a string in this format: %field% is required
// and converts it to a string with replacements. The fields come from
// the ErrorDetails struct and vary for each type of error.
// 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 {
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)
}
tpl, err = tpl.Parse(s)
errorTemplates.Unlock()
if err != nil {
return err.Error()
}
}
return s
err = tpl.Execute(&descrAsBuffer, details)
if err != nil {
return err.Error()
}
return descrAsBuffer.String()
}

View File

@ -5,6 +5,7 @@ import (
"net/url"
"reflect"
"regexp"
"strings"
"time"
)
@ -51,14 +52,20 @@ type (
// http://tools.ietf.org/html/rfc3339#section-5.6
DateTimeFormatChecker struct{}
// URIFormatCheckers validates a URI with a valid Scheme per RFC3986
// URIFormatChecker validates a URI with a valid Scheme per RFC3986
URIFormatChecker struct{}
// URIReferenceFormatChecker validates a URI or relative-reference per RFC3986
URIReferenceFormatChecker struct{}
// HostnameFormatChecker validates a hostname is in the correct format
HostnameFormatChecker struct{}
// UUIDFormatChecker validates a UUID is in the correct format
UUIDFormatChecker struct{}
// RegexFormatChecker validates a regex is in the correct format
RegexFormatChecker struct{}
)
var (
@ -66,13 +73,15 @@ var (
// so library users can add custom formatters
FormatCheckers = FormatCheckerChain{
formatters: map[string]FormatChecker{
"date-time": DateTimeFormatChecker{},
"hostname": HostnameFormatChecker{},
"email": EmailFormatChecker{},
"ipv4": IPV4FormatChecker{},
"ipv6": IPV6FormatChecker{},
"uri": URIFormatChecker{},
"uuid": UUIDFormatChecker{},
"date-time": DateTimeFormatChecker{},
"hostname": HostnameFormatChecker{},
"email": EmailFormatChecker{},
"ipv4": IPV4FormatChecker{},
"ipv6": IPV6FormatChecker{},
"uri": URIFormatChecker{},
"uri-reference": URIReferenceFormatChecker{},
"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}])))\\.?$")
// 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}$")
)
@ -132,13 +141,13 @@ func (f EmailFormatChecker) IsFormat(input string) bool {
// Credit: https://github.com/asaskevich/govalidator
func (f IPV4FormatChecker) IsFormat(input string) bool {
ip := net.ParseIP(input)
return ip != nil && ip.To4() != nil
return ip != nil && strings.Contains(input, ".")
}
// Credit: https://github.com/asaskevich/govalidator
func (f IPV6FormatChecker) IsFormat(input string) bool {
ip := net.ParseIP(input)
return ip != nil && ip.To4() == nil
return ip != nil && strings.Contains(input, ":")
}
func (f DateTimeFormatChecker) IsFormat(input string) bool {
@ -168,10 +177,27 @@ func (f URIFormatChecker) IsFormat(input string) bool {
return true
}
func (f URIReferenceFormatChecker) IsFormat(input string) bool {
_, err := url.Parse(input)
return err == nil
}
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 {
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/ioutil"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/xeipuuv/gojsonreference"
)
var osFS = osFileSystem(os.Open)
// JSON loader interface
type JSONLoader interface {
jsonSource() interface{}
loadJSON() (interface{}, error)
loadSchema() (*Schema, error)
JsonSource() interface{}
LoadJSON() (interface{}, error)
JsonReference() (gojsonreference.JsonReference, error)
LoaderFactory() JSONLoaderFactory
}
type JSONLoaderFactory interface {
New(source string) JSONLoader
}
type DefaultJSONLoaderFactory struct {
}
type FileSystemJSONLoaderFactory struct {
fs http.FileSystem
}
func (d DefaultJSONLoaderFactory) New(source string) JSONLoader {
return &jsonReferenceLoader{
fs: osFS,
source: source,
}
}
func (f FileSystemJSONLoaderFactory) New(source string) JSONLoader {
return &jsonReferenceLoader{
fs: f.fs,
source: source,
}
}
// osFileSystem is a functional wrapper for os.Open that implements http.FileSystem.
type osFileSystem func(string) (*os.File, error)
func (o osFileSystem) Open(name string) (http.File, error) {
return o(name)
}
// JSON Reference loader
// references are used to load JSONs from files and HTTP
type jsonReferenceLoader struct {
fs http.FileSystem
source string
}
func (l *jsonReferenceLoader) jsonSource() interface{} {
func (l *jsonReferenceLoader) JsonSource() interface{} {
return l.source
}
func NewReferenceLoader(source string) *jsonReferenceLoader {
return &jsonReferenceLoader{source: source}
func (l *jsonReferenceLoader) JsonReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference(l.JsonSource().(string))
}
func (l *jsonReferenceLoader) loadJSON() (interface{}, error) {
func (l *jsonReferenceLoader) LoaderFactory() JSONLoaderFactory {
return &FileSystemJSONLoaderFactory{
fs: l.fs,
}
}
// NewReferenceLoader returns a JSON reference loader using the given source and the local OS file system.
func NewReferenceLoader(source string) *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
reference, err := gojsonreference.NewJsonReference(l.jsonSource().(string))
reference, err := gojsonreference.NewJsonReference(l.JsonSource().(string))
if err != nil {
return nil, err
}
@ -79,7 +139,7 @@ func (l *jsonReferenceLoader) loadJSON() (interface{}, error) {
if reference.HasFileScheme {
filename := strings.Replace(refToUrl.String(), "file://", "", -1)
filename := strings.Replace(refToUrl.GetUrl().Path, "file://", "", -1)
if runtime.GOOS == "windows" {
// on Windows, a file URL may have an extra leading slash, use slashes
// instead of backslashes, and have spaces escaped
@ -87,7 +147,6 @@ func (l *jsonReferenceLoader) loadJSON() (interface{}, error) {
filename = filename[1:]
}
filename = filepath.FromSlash(filename)
filename = strings.Replace(filename, "%20", " ", -1)
}
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) {
resp, err := http.Get(address)
@ -144,7 +176,7 @@ func (l *jsonReferenceLoader) loadFromHTTP(address string) (interface{}, error)
// must return HTTP Status 200 OK
if resp.StatusCode != http.StatusOK {
return nil, errors.New(formatErrorDescription(Locale.httpBadStatus(), ErrorDetails{"status": resp.Status}))
return nil, errors.New(formatErrorDescription(Locale.HttpBadStatus(), ErrorDetails{"status": resp.Status}))
}
bodyBuff, err := ioutil.ReadAll(resp.Body)
@ -157,8 +189,13 @@ func (l *jsonReferenceLoader) loadFromHTTP(address 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 {
return nil, err
}
@ -173,45 +210,52 @@ type jsonStringLoader struct {
source string
}
func (l *jsonStringLoader) jsonSource() interface{} {
func (l *jsonStringLoader) JsonSource() interface{} {
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 {
return &jsonStringLoader{source: source}
}
func (l *jsonStringLoader) loadJSON() (interface{}, error) {
func (l *jsonStringLoader) LoadJSON() (interface{}, error) {
return decodeJsonUsingNumber(strings.NewReader(l.jsonSource().(string)))
return decodeJsonUsingNumber(strings.NewReader(l.JsonSource().(string)))
}
func (l *jsonStringLoader) loadSchema() (*Schema, error) {
// JSON bytes loader
var err error
type jsonBytesLoader struct {
source []byte
}
document, err := l.loadJSON()
if err != nil {
return nil, err
}
func (l *jsonBytesLoader) JsonSource() interface{} {
return l.source
}
d := Schema{}
d.pool = newSchemaPool()
d.referencePool = newSchemaReferencePool()
d.documentReference, err = gojsonreference.NewJsonReference("#")
d.pool.SetStandaloneDocument(document)
if err != nil {
return nil, err
}
func (l *jsonBytesLoader) JsonReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference("#")
}
err = d.parse(document)
if err != nil {
return nil, err
}
func (l *jsonBytesLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
return &d, nil
func NewBytesLoader(source []byte) *jsonBytesLoader {
return &jsonBytesLoader{source: source}
}
func (l *jsonBytesLoader) LoadJSON() (interface{}, error) {
return decodeJsonUsingNumber(bytes.NewReader(l.JsonSource().([]byte)))
}
// JSON Go (types) loader
@ -221,19 +265,27 @@ type jsonGoLoader struct {
source interface{}
}
func (l *jsonGoLoader) jsonSource() interface{} {
func (l *jsonGoLoader) JsonSource() interface{} {
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 {
return &jsonGoLoader{source: source}
}
func (l *jsonGoLoader) loadJSON() (interface{}, error) {
func (l *jsonGoLoader) LoadJSON() (interface{}, error) {
// convert it to a compliant JSON first to avoid types "mismatches"
jsonBytes, err := json.Marshal(l.jsonSource())
jsonBytes, err := json.Marshal(l.JsonSource())
if err != nil {
return nil, err
}
@ -242,31 +294,34 @@ func (l *jsonGoLoader) loadJSON() (interface{}, error) {
}
func (l *jsonGoLoader) loadSchema() (*Schema, error) {
type jsonIOLoader struct {
buf *bytes.Buffer
}
var err error
func NewReaderLoader(source io.Reader) (*jsonIOLoader, io.Reader) {
buf := &bytes.Buffer{}
return &jsonIOLoader{buf: buf}, io.TeeReader(source, buf)
}
document, err := l.loadJSON()
if err != nil {
return nil, err
}
func NewWriterLoader(source io.Writer) (*jsonIOLoader, io.Writer) {
buf := &bytes.Buffer{}
return &jsonIOLoader{buf: buf}, io.MultiWriter(source, buf)
}
d := Schema{}
d.pool = newSchemaPool()
d.referencePool = newSchemaReferencePool()
d.documentReference, err = gojsonreference.NewJsonReference("#")
d.pool.SetStandaloneDocument(document)
if err != nil {
return nil, err
}
func (l *jsonIOLoader) JsonSource() interface{} {
return l.buf.String()
}
err = d.parse(document)
if err != nil {
return nil, err
}
func (l *jsonIOLoader) LoadJSON() (interface{}, error) {
return decodeJsonUsingNumber(l.buf)
}
return &d, nil
func (l *jsonIOLoader) JsonReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference("#")
}
func (l *jsonIOLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
func decodeJsonUsingNumber(r io.Reader) (interface{}, error) {
@ -280,7 +335,7 @@ func decodeJsonUsingNumber(r io.Reader) (interface{}, error) {
if err != nil {
return nil, err
}
return document, nil
}

View File

@ -26,7 +26,7 @@
package gojsonschema
type (
// locale is an interface for definining custom error strings
// locale is an interface for defining custom error strings
locale interface {
Required() string
InvalidType() string
@ -37,6 +37,7 @@ type (
MissingDependency() string
Internal() string
Enum() string
ArrayNotEnoughItems() string
ArrayNoAdditionalItems() string
ArrayMinItems() string
ArrayMaxItems() string
@ -72,7 +73,8 @@ type (
ReferenceMustBeCanonical() string
NotAValidType() string
Duplicated() string
httpBadStatus() string
HttpBadStatus() string
ParseError() string
// ErrorFormat
ErrorFormat() string
@ -83,11 +85,11 @@ type (
)
func (l DefaultLocale) Required() string {
return `%property% is required`
return `{{.property}} is required`
}
func (l DefaultLocale) InvalidType() string {
return `Invalid type. Expected: %expected%, given: %given%`
return `Invalid type. Expected: {{.expected}}, given: {{.given}}`
}
func (l DefaultLocale) NumberAnyOf() string {
@ -107,157 +109,166 @@ func (l DefaultLocale) NumberNot() string {
}
func (l DefaultLocale) MissingDependency() string {
return `Has a dependency on %dependency%`
return `Has a dependency on {{.dependency}}`
}
func (l DefaultLocale) Internal() string {
return `Internal Error %error%`
return `Internal Error {{.error}}`
}
func (l DefaultLocale) Enum() string {
return `%field% must be one of the following: %allowed%`
return `{{.field}} must be one of the following: {{.allowed}}`
}
func (l DefaultLocale) ArrayNoAdditionalItems() string {
return `No additional items allowed on array`
}
func (l DefaultLocale) ArrayNotEnoughItems() string {
return `Not enough items on array to match positional list of schema`
}
func (l DefaultLocale) ArrayMinItems() string {
return `Array must have at least %min% items`
return `Array must have at least {{.min}} items`
}
func (l DefaultLocale) ArrayMaxItems() string {
return `Array must have at most %max% items`
return `Array must have at most {{.max}} items`
}
func (l DefaultLocale) Unique() string {
return `%type% items must be unique`
return `{{.type}} items must be unique`
}
func (l DefaultLocale) ArrayMinProperties() string {
return `Must have at least %min% properties`
return `Must have at least {{.min}} properties`
}
func (l DefaultLocale) ArrayMaxProperties() string {
return `Must have at most %max% properties`
return `Must have at most {{.max}} properties`
}
func (l DefaultLocale) AdditionalPropertyNotAllowed() string {
return `Additional property %property% is not allowed`
return `Additional property {{.property}} is not allowed`
}
func (l DefaultLocale) InvalidPropertyPattern() string {
return `Property "%property%" does not match pattern %pattern%`
return `Property "{{.property}}" does not match pattern {{.pattern}}`
}
func (l DefaultLocale) StringGTE() string {
return `String length must be greater than or equal to %min%`
return `String length must be greater than or equal to {{.min}}`
}
func (l DefaultLocale) StringLTE() string {
return `String length must be less than or equal to %max%`
return `String length must be less than or equal to {{.max}}`
}
func (l DefaultLocale) DoesNotMatchPattern() string {
return `Does not match pattern '%pattern%'`
return `Does not match pattern '{{.pattern}}'`
}
func (l DefaultLocale) DoesNotMatchFormat() string {
return `Does not match format '%format%'`
return `Does not match format '{{.format}}'`
}
func (l DefaultLocale) MultipleOf() string {
return `Must be a multiple of %multiple%`
return `Must be a multiple of {{.multiple}}`
}
func (l DefaultLocale) NumberGTE() string {
return `Must be greater than or equal to %min%`
return `Must be greater than or equal to {{.min}}`
}
func (l DefaultLocale) NumberGT() string {
return `Must be greater than %min%`
return `Must be greater than {{.min}}`
}
func (l DefaultLocale) NumberLTE() string {
return `Must be less than or equal to %max%`
return `Must be less than or equal to {{.max}}`
}
func (l DefaultLocale) NumberLT() string {
return `Must be less than %max%`
return `Must be less than {{.max}}`
}
// Schema validators
func (l DefaultLocale) RegexPattern() string {
return `Invalid regex pattern '%pattern%'`
return `Invalid regex pattern '{{.pattern}}'`
}
func (l DefaultLocale) GreaterThanZero() string {
return `%number% must be strictly greater than 0`
return `{{.number}} must be strictly greater than 0`
}
func (l DefaultLocale) MustBeOfA() string {
return `%x% must be of a %y%`
return `{{.x}} must be of a {{.y}}`
}
func (l DefaultLocale) MustBeOfAn() string {
return `%x% must be of an %y%`
return `{{.x}} must be of an {{.y}}`
}
func (l DefaultLocale) CannotBeUsedWithout() string {
return `%x% cannot be used without %y%`
return `{{.x}} cannot be used without {{.y}}`
}
func (l DefaultLocale) CannotBeGT() string {
return `%x% cannot be greater than %y%`
return `{{.x}} cannot be greater than {{.y}}`
}
func (l DefaultLocale) MustBeOfType() string {
return `%key% must be of type %type%`
return `{{.key}} must be of type {{.type}}`
}
func (l DefaultLocale) MustBeValidRegex() string {
return `%key% must be a valid regex`
return `{{.key}} must be a valid regex`
}
func (l DefaultLocale) MustBeValidFormat() string {
return `%key% must be a valid format %given%`
return `{{.key}} must be a valid format {{.given}}`
}
func (l DefaultLocale) MustBeGTEZero() string {
return `%key% must be greater than or equal to 0`
return `{{.key}} must be greater than or equal to 0`
}
func (l DefaultLocale) KeyCannotBeGreaterThan() string {
return `%key% cannot be greater than %y%`
return `{{.key}} cannot be greater than {{.y}}`
}
func (l DefaultLocale) KeyItemsMustBeOfType() string {
return `%key% items must be %type%`
return `{{.key}} items must be {{.type}}`
}
func (l DefaultLocale) KeyItemsMustBeUnique() string {
return `%key% items must be unique`
return `{{.key}} items must be unique`
}
func (l DefaultLocale) ReferenceMustBeCanonical() string {
return `Reference %reference% must be canonical`
return `Reference {{.reference}} must be canonical`
}
func (l DefaultLocale) NotAValidType() string {
return `%type% is not a valid type -- `
return `has a primitive type that is NOT VALID -- given: {{.given}} Expected valid values are:{{.expected}}`
}
func (l DefaultLocale) Duplicated() string {
return `%type% type is duplicated`
return `{{.type}} type is duplicated`
}
func (l DefaultLocale) httpBadStatus() string {
return `Could not read schema from HTTP, response status is %status%`
func (l DefaultLocale) HttpBadStatus() string {
return `Could not read schema from HTTP, response status is {{.status}}`
}
// Replacement options: field, description, context, value
func (l DefaultLocale) ErrorFormat() string {
return `%field%: %description%`
return `{{.field}}: {{.description}}`
}
//Parse error
func (l DefaultLocale) ParseError() string {
return `Expected: %expected%, given: Invalid JSON`
}
const (

View File

@ -48,6 +48,7 @@ type (
Value() interface{}
SetDetails(ErrorDetails)
Details() ErrorDetails
String() string
}
// ResultErrorFields holds the fields for each ResultError implementation.
@ -126,7 +127,7 @@ func (v ResultErrorFields) String() string {
valueString := fmt.Sprintf("%v", v.value)
// marshal the go value value to json
if v.Value == nil {
if v.value == nil {
valueString = TYPE_NULL
} else {
if vs, err := marshalToJsonString(v.value); err == nil {

View File

@ -31,6 +31,7 @@ import (
"errors"
"reflect"
"regexp"
"text/template"
"github.com/xeipuuv/gojsonreference"
)
@ -39,10 +40,45 @@ var (
// Locale is the default locale to use
// Library users can overwrite with their own implementation
Locale locale = DefaultLocale{}
// ErrorTemplateFuncs allows you to define custom template funcs for use in localization.
ErrorTemplateFuncs template.FuncMap
)
func NewSchema(l JSONLoader) (*Schema, error) {
return l.loadSchema()
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 {
@ -71,10 +107,9 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema)
if !isKind(documentNode, reflect.Map) {
return errors.New(formatErrorDescription(
Locale.InvalidType(),
Locale.ParseError(),
ErrorDetails{
"expected": TYPE_OBJECT,
"given": STRING_SCHEMA,
"expected": STRING_SCHEMA,
},
))
}
@ -116,14 +151,27 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema)
}
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
} else {
var err error
err = d.parseReference(documentNode, currentSchema, k)
err := d.parseReference(documentNode, currentSchema, k)
if err != nil {
return err
}
@ -755,30 +803,10 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema)
return nil
}
func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSchema, reference string) (e 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()
func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSchema, reference string) error {
var refdDocumentNode interface{}
jsonPointer := currentSchema.ref.GetPointer()
standaloneDocument := d.pool.GetStandaloneDocument()
if standaloneDocument != nil {
@ -789,8 +817,6 @@ func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSche
}
} else {
var err error
dsp, err := d.pool.GetDocument(*currentSchema.ref)
if err != nil {
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
newSchemaDocument := refdDocumentNode.(map[string]interface{})
newSchema := &subSchema{property: KEY_REF, parent: currentSchema, ref: currentSchema.ref}
d.referencePool.Add(currentSchema.ref.String()+reference, newSchema)
err = d.parseSchema(newSchemaDocument, newSchema)
err := d.parseSchema(newSchemaDocument, newSchema)
if err != nil {
return err
}

View File

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

View File

@ -44,7 +44,7 @@ func (t *jsonSchemaType) IsTyped() bool {
func (t *jsonSchemaType) Add(etype string) error {
if !isStringInSlice(JSON_TYPES, etype) {
return errors.New(formatErrorDescription(Locale.NotAValidType(), ErrorDetails{"type": etype}))
return errors.New(formatErrorDescription(Locale.NotAValidType(), ErrorDetails{"given": "/" + etype + "/", "expected": JSON_TYPES}))
}
if t.Contains(etype) {

View File

@ -214,7 +214,7 @@ func (s *subSchema) PatternPropertiesString() string {
}
patternPropertiesKeySlice := []string{}
for pk, _ := range s.patternProperties {
for pk := range s.patternProperties {
patternPropertiesKeySlice = append(patternPropertiesKeySlice, `"`+pk+`"`)
}

View File

@ -34,7 +34,12 @@ import (
)
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 {
@ -77,13 +82,14 @@ func checkJsonNumber(what interface{}) (isValidFloat64 bool, isValidInt64 bool,
jsonNumber := what.(json.Number)
_, errFloat64 := jsonNumber.Float64()
_, errInt64 := jsonNumber.Int64()
f64, errFloat64 := jsonNumber.Float64()
s64 := strconv.FormatFloat(f64, 'f', -1, 64)
_, errInt64 := strconv.ParseInt(s64, 10, 64)
isValidFloat64 = errFloat64 == nil
isValidInt64 = errInt64 == nil
_, errInt32 := strconv.ParseInt(jsonNumber.String(), 10, 32)
_, errInt32 := strconv.ParseInt(s64, 10, 32)
isValidInt32 = isValidInt64 && errInt32 == nil
return

View File

@ -55,7 +55,7 @@ func (v *Schema) Validate(l JSONLoader) (*Result, error) {
// load document
root, err := l.loadJSON()
root, err := l.LoadJSON()
if err != nil {
return nil, err
}
@ -412,7 +412,7 @@ func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface
internalLog(" %v", value)
}
nbItems := len(value)
nbValues := len(value)
// TODO explain
if currentSubSchema.itemsChildrenIsSingleSchema {
@ -425,15 +425,18 @@ func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface
if currentSubSchema.itemsChildren != nil && len(currentSubSchema.itemsChildren) > 0 {
nbItems := len(currentSubSchema.itemsChildren)
nbValues := len(value)
if nbItems == nbValues {
for i := 0; i != nbItems; i++ {
subContext := newJsonContext(strconv.Itoa(i), context)
validationResult := currentSubSchema.itemsChildren[i].subValidateWithContext(value[i], subContext)
result.mergeErrors(validationResult)
}
} else if nbItems < nbValues {
// while we have both schemas and values, check them against each other
for i := 0; i != nbItems && i != nbValues; i++ {
subContext := newJsonContext(strconv.Itoa(i), context)
validationResult := currentSubSchema.itemsChildren[i].subValidateWithContext(value[i], subContext)
result.mergeErrors(validationResult)
}
if nbItems < nbValues {
// we have less schemas than elements in the instance array,
// but that might be ok if "additionalItems" is specified.
switch currentSubSchema.additionalItems.(type) {
case bool:
if !currentSubSchema.additionalItems.(bool) {
@ -453,7 +456,7 @@ func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface
// minItems & maxItems
if currentSubSchema.minItems != nil {
if nbItems < int(*currentSubSchema.minItems) {
if nbValues < int(*currentSubSchema.minItems) {
result.addError(
new(ArrayMinItemsError),
context,
@ -463,7 +466,7 @@ func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface
}
}
if currentSubSchema.maxItems != nil {
if nbItems > int(*currentSubSchema.maxItems) {
if nbValues > int(*currentSubSchema.maxItems) {
result.addError(
new(ArrayMaxItemsError),
context,
@ -565,7 +568,7 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string
result.addError(
new(AdditionalPropertyNotAllowedError),
context,
value,
value[pk],
ErrorDetails{"property": pk},
)
}
@ -576,7 +579,7 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string
result.addError(
new(AdditionalPropertyNotAllowedError),
context,
value,
value[pk],
ErrorDetails{"property": pk},
)
}
@ -628,7 +631,7 @@ func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string
result.addError(
new(InvalidPropertyPatternError),
context,
value,
value[pk],
ErrorDetails{
"property": pk,
"pattern": currentSubSchema.PatternPropertiesString(),
@ -776,22 +779,22 @@ func (v *subSchema) validateNumber(currentSubSchema *subSchema, value interface{
if currentSubSchema.exclusiveMaximum {
if float64Value >= *currentSubSchema.maximum {
result.addError(
new(NumberLTEError),
new(NumberLTError),
context,
resultErrorFormatJsonNumber(number),
ErrorDetails{
"min": resultErrorFormatNumber(*currentSubSchema.maximum),
"max": resultErrorFormatNumber(*currentSubSchema.maximum),
},
)
}
} else {
if float64Value > *currentSubSchema.maximum {
result.addError(
new(NumberLTError),
new(NumberLTEError),
context,
resultErrorFormatJsonNumber(number),
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 float64Value <= *currentSubSchema.minimum {
result.addError(
new(NumberGTEError),
new(NumberGTError),
context,
resultErrorFormatJsonNumber(number),
ErrorDetails{
"max": resultErrorFormatNumber(*currentSubSchema.minimum),
"min": resultErrorFormatNumber(*currentSubSchema.minimum),
},
)
}
} else {
if float64Value < *currentSubSchema.minimum {
result.addError(
new(NumberGTError),
new(NumberGTEError),
context,
resultErrorFormatJsonNumber(number),
ErrorDetails{
"max": resultErrorFormatNumber(*currentSubSchema.minimum),
"min": resultErrorFormatNumber(*currentSubSchema.minimum),
},
)
}