958 lines
25 KiB
Go
958 lines
25 KiB
Go
// Copyright 2015 xeipuuv ( https://github.com/xeipuuv )
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// author xeipuuv
|
|
// author-github https://github.com/xeipuuv
|
|
// author-mail xeipuuv@gmail.com
|
|
//
|
|
// repository-name gojsonschema
|
|
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
|
|
//
|
|
// description Defines Schema, the main entry to every SubSchema.
|
|
// Contains the parsing logic and error checking.
|
|
//
|
|
// created 26-02-2013
|
|
|
|
package gojsonschema
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"math/big"
|
|
"regexp"
|
|
"text/template"
|
|
|
|
"github.com/xeipuuv/gojsonreference"
|
|
)
|
|
|
|
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
|
|
)
|
|
|
|
// NewSchema instances a schema using the given JSONLoader
|
|
func NewSchema(l JSONLoader) (*Schema, error) {
|
|
return NewSchemaLoader().Compile(l)
|
|
}
|
|
|
|
// Schema holds a schema
|
|
type Schema struct {
|
|
DocumentReference gojsonreference.JsonReference
|
|
RootSchema *SubSchema
|
|
Pool *schemaPool
|
|
ReferencePool *schemaReferencePool
|
|
}
|
|
|
|
func (d *Schema) parse(document interface{}, draft Draft) error {
|
|
d.RootSchema = &SubSchema{Property: StringRootSchemaProperty, Draft: &draft}
|
|
return d.parseSchema(document, d.RootSchema)
|
|
}
|
|
|
|
// SetRootSchemaName sets the root-schema name
|
|
func (d *Schema) SetRootSchemaName(name string) {
|
|
d.RootSchema.Property = name
|
|
}
|
|
|
|
// Parses a SubSchema
|
|
//
|
|
// Pretty long function ( sorry :) )... but pretty straight forward, repetitive and boring
|
|
// Not much magic involved here, most of the job is to validate the key names and their values,
|
|
// then the values are copied into SubSchema struct
|
|
func (d *Schema) parseSchema(documentNode interface{}, currentSchema *SubSchema) error {
|
|
|
|
if currentSchema.Draft == nil {
|
|
if currentSchema.Parent == nil {
|
|
return errors.New("Draft not set")
|
|
}
|
|
currentSchema.Draft = currentSchema.Parent.Draft
|
|
}
|
|
|
|
// As of draft 6 "true" is equivalent to an empty schema "{}" and false equals "{"not":{}}"
|
|
if *currentSchema.Draft >= Draft6 {
|
|
if b, isBool := documentNode.(bool); isBool {
|
|
currentSchema.pass = &b
|
|
return nil
|
|
}
|
|
}
|
|
|
|
m, isMap := documentNode.(map[string]interface{})
|
|
if !isMap {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.ParseError(),
|
|
ErrorDetails{
|
|
"expected": StringSchema,
|
|
},
|
|
))
|
|
}
|
|
|
|
if currentSchema.Parent == nil {
|
|
currentSchema.Ref = &d.DocumentReference
|
|
currentSchema.ID = &d.DocumentReference
|
|
}
|
|
|
|
if currentSchema.ID == nil && currentSchema.Parent != nil {
|
|
currentSchema.ID = currentSchema.Parent.ID
|
|
}
|
|
|
|
// In draft 6 the id keyword was renamed to $id
|
|
// Hybrid mode uses the old id by default
|
|
var keyID string
|
|
|
|
switch *currentSchema.Draft {
|
|
case Draft4:
|
|
keyID = KeyID
|
|
case Hybrid:
|
|
keyID = KeyIDNew
|
|
if _, found := m[KeyID]; found {
|
|
keyID = KeyID
|
|
}
|
|
default:
|
|
keyID = KeyIDNew
|
|
}
|
|
|
|
if id, err := getString(m, keyID); err != nil {
|
|
return err
|
|
} else if id != nil {
|
|
jsonReference, err := gojsonreference.NewJsonReference(*id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if currentSchema == d.RootSchema {
|
|
currentSchema.ID = &jsonReference
|
|
} else {
|
|
ref, err := currentSchema.Parent.ID.Inherits(jsonReference)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
currentSchema.ID = ref
|
|
}
|
|
}
|
|
|
|
// definitions
|
|
if v, ok := m[KeyDefinitions]; ok {
|
|
switch mt := v.(type) {
|
|
case map[string]interface{}:
|
|
for _, dv := range mt {
|
|
switch dv.(type) {
|
|
case bool, map[string]interface{}:
|
|
newSchema := &SubSchema{Property: KeyDefinitions, Parent: currentSchema}
|
|
err := d.parseSchema(dv, newSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return invalidType(StringArrayOfSchemas, KeyDefinitions)
|
|
}
|
|
}
|
|
default:
|
|
return invalidType(StringArrayOfSchemas, KeyDefinitions)
|
|
}
|
|
}
|
|
|
|
// title
|
|
var err error
|
|
currentSchema.title, err = getString(m, KeyTitle)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// description
|
|
currentSchema.description, err = getString(m, KeyDescription)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// $ref
|
|
if ref, err := getString(m, KeyRef); err != nil {
|
|
return err
|
|
} else if ref != nil {
|
|
jsonReference, err := gojsonreference.NewJsonReference(*ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
currentSchema.Ref = &jsonReference
|
|
|
|
if sch, ok := d.ReferencePool.Get(currentSchema.Ref.String()); ok {
|
|
currentSchema.RefSchema = sch
|
|
} else {
|
|
return d.parseReference(documentNode, currentSchema)
|
|
}
|
|
}
|
|
|
|
// type
|
|
if typ, found := m[KeyType]; found {
|
|
switch t := typ.(type) {
|
|
case string:
|
|
err := currentSchema.Types.Add(t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case []interface{}:
|
|
for _, typeInArray := range t {
|
|
s, isString := typeInArray.(string)
|
|
if !isString {
|
|
return invalidType(KeyType, TypeString+"/"+StringArrayOfStrings)
|
|
}
|
|
if err := currentSchema.Types.Add(s); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
default:
|
|
return invalidType(KeyType, TypeString+"/"+StringArrayOfStrings)
|
|
}
|
|
}
|
|
|
|
// properties
|
|
if properties, found := m[KeyProperties]; found {
|
|
err := d.parseProperties(properties, currentSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// additionalProperties
|
|
if additionalProperties, found := m[KeyAdditionalProperties]; found {
|
|
switch v := additionalProperties.(type) {
|
|
case bool:
|
|
currentSchema.additionalProperties = v
|
|
case map[string]interface{}:
|
|
newSchema := &SubSchema{Property: KeyAdditionalProperties, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
currentSchema.additionalProperties = newSchema
|
|
err := d.parseSchema(v, newSchema)
|
|
if err != nil {
|
|
return errors.New(err.Error())
|
|
}
|
|
default:
|
|
return invalidType(TypeBoolean+"/"+StringSchema, KeyAdditionalProperties)
|
|
}
|
|
}
|
|
|
|
// patternProperties
|
|
if patternProperties, err := getMap(m, KeyPatternProperties); err != nil {
|
|
return err
|
|
} else if patternProperties != nil {
|
|
if len(patternProperties) > 0 {
|
|
currentSchema.patternProperties = make(map[string]*SubSchema)
|
|
for k, v := range patternProperties {
|
|
_, err := regexp.MatchString(k, "")
|
|
if err != nil {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.RegexPattern(),
|
|
ErrorDetails{"pattern": k},
|
|
))
|
|
}
|
|
newSchema := &SubSchema{Property: k, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
err = d.parseSchema(v, newSchema)
|
|
if err != nil {
|
|
return errors.New(err.Error())
|
|
}
|
|
currentSchema.patternProperties[k] = newSchema
|
|
}
|
|
}
|
|
}
|
|
|
|
// propertyNames
|
|
if propertyNames, found := m[KeyPropertyNames]; found && *currentSchema.Draft >= Draft6 {
|
|
switch propertyNames.(type) {
|
|
case bool, map[string]interface{}:
|
|
newSchema := &SubSchema{Property: KeyPropertyNames, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
currentSchema.propertyNames = newSchema
|
|
err := d.parseSchema(propertyNames, newSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return errors.New(formatErrorDescription(
|
|
Locale.InvalidType(),
|
|
ErrorDetails{
|
|
"expected": StringSchema,
|
|
"given": KeyPatternProperties,
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
// dependencies
|
|
if dependencies, found := m[KeyDependencies]; found {
|
|
err := d.parseDependencies(dependencies, currentSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// items
|
|
if items, found := m[KeyItems]; found {
|
|
switch i := items.(type) {
|
|
case []interface{}:
|
|
for _, itemElement := range i {
|
|
switch itemElement.(type) {
|
|
case map[string]interface{}, bool:
|
|
newSchema := &SubSchema{Parent: currentSchema, Property: KeyItems}
|
|
newSchema.Ref = currentSchema.Ref
|
|
currentSchema.ItemsChildren = append(currentSchema.ItemsChildren, newSchema)
|
|
err := d.parseSchema(itemElement, newSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return invalidType(StringSchema+"/"+StringArrayOfSchemas, KeyItems)
|
|
}
|
|
currentSchema.ItemsChildrenIsSingleSchema = false
|
|
}
|
|
case map[string]interface{}, bool:
|
|
newSchema := &SubSchema{Parent: currentSchema, Property: KeyItems}
|
|
newSchema.Ref = currentSchema.Ref
|
|
currentSchema.ItemsChildren = append(currentSchema.ItemsChildren, newSchema)
|
|
err := d.parseSchema(items, newSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
currentSchema.ItemsChildrenIsSingleSchema = true
|
|
default:
|
|
return invalidType(StringSchema+"/"+StringArrayOfSchemas, KeyItems)
|
|
}
|
|
}
|
|
|
|
// additionalItems
|
|
if additionalItems, found := m[KeyAdditionalItems]; found {
|
|
switch i := additionalItems.(type) {
|
|
case bool:
|
|
currentSchema.additionalItems = i
|
|
case map[string]interface{}:
|
|
newSchema := &SubSchema{Property: KeyAdditionalItems, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
currentSchema.additionalItems = newSchema
|
|
err := d.parseSchema(additionalItems, newSchema)
|
|
if err != nil {
|
|
return errors.New(err.Error())
|
|
}
|
|
default:
|
|
return invalidType(TypeBoolean+"/"+StringSchema, KeyAdditionalItems)
|
|
}
|
|
}
|
|
|
|
// validation : number / integer
|
|
if multipleOf, found := m[KeyMultipleOf]; found {
|
|
multipleOfValue := mustBeNumber(multipleOf)
|
|
if multipleOfValue == nil {
|
|
return invalidType(StringNumber, KeyMultipleOf)
|
|
}
|
|
if multipleOfValue.Cmp(big.NewRat(0, 1)) <= 0 {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.GreaterThanZero(),
|
|
ErrorDetails{"number": KeyMultipleOf},
|
|
))
|
|
}
|
|
currentSchema.multipleOf = multipleOfValue
|
|
}
|
|
|
|
if minimum, found := m[KeyMinimum]; found {
|
|
minimumValue := mustBeNumber(minimum)
|
|
if minimumValue == nil {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfA(),
|
|
ErrorDetails{"x": KeyMinimum, "y": StringNumber},
|
|
))
|
|
}
|
|
currentSchema.minimum = minimumValue
|
|
}
|
|
|
|
if exclusiveMinimum, found := m[KeyExclusiveMinimum]; found {
|
|
switch *currentSchema.Draft {
|
|
case Draft4:
|
|
boolExclusiveMinimum, isBool := exclusiveMinimum.(bool)
|
|
if !isBool {
|
|
return invalidType(TypeBoolean, KeyExclusiveMinimum)
|
|
}
|
|
if currentSchema.minimum == nil {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.CannotBeUsedWithout(),
|
|
ErrorDetails{"x": KeyExclusiveMinimum, "y": KeyMinimum},
|
|
))
|
|
}
|
|
if boolExclusiveMinimum {
|
|
currentSchema.exclusiveMinimum = currentSchema.minimum
|
|
currentSchema.minimum = nil
|
|
}
|
|
case Hybrid:
|
|
switch b := exclusiveMinimum.(type) {
|
|
case bool:
|
|
if currentSchema.minimum == nil {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.CannotBeUsedWithout(),
|
|
ErrorDetails{"x": KeyExclusiveMinimum, "y": KeyMinimum},
|
|
))
|
|
}
|
|
if b {
|
|
currentSchema.exclusiveMinimum = currentSchema.minimum
|
|
currentSchema.minimum = nil
|
|
}
|
|
case json.Number:
|
|
currentSchema.exclusiveMinimum = mustBeNumber(m[KeyExclusiveMinimum])
|
|
default:
|
|
return invalidType(TypeBoolean+"/"+TypeNumber, KeyExclusiveMinimum)
|
|
}
|
|
default:
|
|
if isJSONNumber(exclusiveMinimum) {
|
|
currentSchema.exclusiveMinimum = mustBeNumber(exclusiveMinimum)
|
|
} else {
|
|
return invalidType(TypeNumber, KeyExclusiveMinimum)
|
|
}
|
|
}
|
|
}
|
|
|
|
if maximum, found := m[KeyMaximum]; found {
|
|
maximumValue := mustBeNumber(maximum)
|
|
if maximumValue == nil {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfA(),
|
|
ErrorDetails{"x": KeyMaximum, "y": StringNumber},
|
|
))
|
|
}
|
|
currentSchema.maximum = maximumValue
|
|
}
|
|
|
|
if exclusiveMaximum, found := m[KeyExclusiveMaximum]; found {
|
|
switch *currentSchema.Draft {
|
|
case Draft4:
|
|
boolExclusiveMaximum, isBool := exclusiveMaximum.(bool)
|
|
if !isBool {
|
|
return invalidType(TypeBoolean, KeyExclusiveMaximum)
|
|
}
|
|
if currentSchema.maximum == nil {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.CannotBeUsedWithout(),
|
|
ErrorDetails{"x": KeyExclusiveMaximum, "y": KeyMaximum},
|
|
))
|
|
}
|
|
if boolExclusiveMaximum {
|
|
currentSchema.exclusiveMaximum = currentSchema.maximum
|
|
currentSchema.maximum = nil
|
|
}
|
|
case Hybrid:
|
|
switch b := exclusiveMaximum.(type) {
|
|
case bool:
|
|
if currentSchema.maximum == nil {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.CannotBeUsedWithout(),
|
|
ErrorDetails{"x": KeyExclusiveMaximum, "y": KeyMaximum},
|
|
))
|
|
}
|
|
if b {
|
|
currentSchema.exclusiveMaximum = currentSchema.maximum
|
|
currentSchema.maximum = nil
|
|
}
|
|
case json.Number:
|
|
currentSchema.exclusiveMaximum = mustBeNumber(exclusiveMaximum)
|
|
default:
|
|
return invalidType(TypeBoolean+"/"+TypeNumber, KeyExclusiveMaximum)
|
|
}
|
|
default:
|
|
if isJSONNumber(exclusiveMaximum) {
|
|
currentSchema.exclusiveMaximum = mustBeNumber(exclusiveMaximum)
|
|
} else {
|
|
return invalidType(TypeNumber, KeyExclusiveMaximum)
|
|
}
|
|
}
|
|
}
|
|
|
|
// validation : string
|
|
|
|
if minLength, found := m[KeyMinLength]; found {
|
|
minLengthIntegerValue := mustBeInteger(minLength)
|
|
if minLengthIntegerValue == nil {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfAn(),
|
|
ErrorDetails{"x": KeyMinLength, "y": TypeInteger},
|
|
))
|
|
}
|
|
if *minLengthIntegerValue < 0 {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeGTEZero(),
|
|
ErrorDetails{"key": KeyMinLength},
|
|
))
|
|
}
|
|
currentSchema.minLength = minLengthIntegerValue
|
|
}
|
|
|
|
if maxLength, found := m[KeyMaxLength]; found {
|
|
maxLengthIntegerValue := mustBeInteger(maxLength)
|
|
if maxLengthIntegerValue == nil {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfAn(),
|
|
ErrorDetails{"x": KeyMaxLength, "y": TypeInteger},
|
|
))
|
|
}
|
|
if *maxLengthIntegerValue < 0 {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeGTEZero(),
|
|
ErrorDetails{"key": KeyMaxLength},
|
|
))
|
|
}
|
|
currentSchema.maxLength = maxLengthIntegerValue
|
|
}
|
|
|
|
if currentSchema.minLength != nil && currentSchema.maxLength != nil {
|
|
if *currentSchema.minLength > *currentSchema.maxLength {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.CannotBeGT(),
|
|
ErrorDetails{"x": KeyMinLength, "y": KeyMaxLength},
|
|
))
|
|
}
|
|
}
|
|
|
|
// NOTE: Regex compilation step removed as we don't use "pattern" attribute for
|
|
// type checking, and this would cause schemas to fail if they included patterns
|
|
// that were valid ECMA regex dialect but not known to Go (i.e. the regexp.Compile
|
|
// function), such as patterns with negative lookahead
|
|
if _, err := getString(m, KeyPattern); err != nil {
|
|
return err
|
|
}
|
|
|
|
if format, err := getString(m, KeyFormat); err != nil {
|
|
return err
|
|
} else if format != nil {
|
|
currentSchema.format = *format
|
|
}
|
|
|
|
// validation : object
|
|
|
|
if minProperties, found := m[KeyMinProperties]; found {
|
|
minPropertiesIntegerValue := mustBeInteger(minProperties)
|
|
if minPropertiesIntegerValue == nil {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfAn(),
|
|
ErrorDetails{"x": KeyMinProperties, "y": TypeInteger},
|
|
))
|
|
}
|
|
if *minPropertiesIntegerValue < 0 {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeGTEZero(),
|
|
ErrorDetails{"key": KeyMinProperties},
|
|
))
|
|
}
|
|
currentSchema.minProperties = minPropertiesIntegerValue
|
|
}
|
|
|
|
if maxProperties, found := m[KeyMaxProperties]; found {
|
|
maxPropertiesIntegerValue := mustBeInteger(maxProperties)
|
|
if maxPropertiesIntegerValue == nil {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfAn(),
|
|
ErrorDetails{"x": KeyMaxProperties, "y": TypeInteger},
|
|
))
|
|
}
|
|
if *maxPropertiesIntegerValue < 0 {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeGTEZero(),
|
|
ErrorDetails{"key": KeyMaxProperties},
|
|
))
|
|
}
|
|
currentSchema.maxProperties = maxPropertiesIntegerValue
|
|
}
|
|
|
|
if currentSchema.minProperties != nil && currentSchema.maxProperties != nil {
|
|
if *currentSchema.minProperties > *currentSchema.maxProperties {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.KeyCannotBeGreaterThan(),
|
|
ErrorDetails{"key": KeyMinProperties, "y": KeyMaxProperties},
|
|
))
|
|
}
|
|
}
|
|
|
|
required, err := getSlice(m, KeyRequired)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, requiredValue := range required {
|
|
s, isString := requiredValue.(string)
|
|
if !isString {
|
|
return invalidType(TypeString, KeyRequired)
|
|
} else if isStringInSlice(currentSchema.required, s) {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.KeyItemsMustBeUnique(),
|
|
ErrorDetails{"key": KeyRequired},
|
|
))
|
|
}
|
|
currentSchema.required = append(currentSchema.required, s)
|
|
}
|
|
|
|
// validation : array
|
|
|
|
if minItems, found := m[KeyMinItems]; found {
|
|
minItemsIntegerValue := mustBeInteger(minItems)
|
|
if minItemsIntegerValue == nil {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfAn(),
|
|
ErrorDetails{"x": KeyMinItems, "y": TypeInteger},
|
|
))
|
|
}
|
|
if *minItemsIntegerValue < 0 {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeGTEZero(),
|
|
ErrorDetails{"key": KeyMinItems},
|
|
))
|
|
}
|
|
currentSchema.minItems = minItemsIntegerValue
|
|
}
|
|
|
|
if maxItems, found := m[KeyMaxItems]; found {
|
|
maxItemsIntegerValue := mustBeInteger(maxItems)
|
|
if maxItemsIntegerValue == nil {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfAn(),
|
|
ErrorDetails{"x": KeyMaxItems, "y": TypeInteger},
|
|
))
|
|
}
|
|
if *maxItemsIntegerValue < 0 {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeGTEZero(),
|
|
ErrorDetails{"key": KeyMaxItems},
|
|
))
|
|
}
|
|
currentSchema.maxItems = maxItemsIntegerValue
|
|
}
|
|
|
|
if uniqueItems, found := m[KeyUniqueItems]; found {
|
|
bUniqueItems, isBool := uniqueItems.(bool)
|
|
if !isBool {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfA(),
|
|
ErrorDetails{"x": KeyUniqueItems, "y": TypeBoolean},
|
|
))
|
|
}
|
|
currentSchema.uniqueItems = bUniqueItems
|
|
}
|
|
|
|
if contains, found := m[KeyContains]; found && *currentSchema.Draft >= Draft6 {
|
|
newSchema := &SubSchema{Property: KeyContains, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
currentSchema.contains = newSchema
|
|
err := d.parseSchema(contains, newSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// validation : all
|
|
if vConst, found := m[KeyConst]; found && *currentSchema.Draft >= Draft6 {
|
|
is, err := marshalWithoutNumber(vConst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
currentSchema._const = is
|
|
}
|
|
|
|
enum, err := getSlice(m, KeyEnum)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, v := range enum {
|
|
is, err := marshalWithoutNumber(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isStringInSlice(currentSchema.enum, *is) {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.KeyItemsMustBeUnique(),
|
|
ErrorDetails{"key": KeyEnum},
|
|
))
|
|
}
|
|
currentSchema.enum = append(currentSchema.enum, *is)
|
|
}
|
|
|
|
// validation : SubSchema
|
|
oneOf, err := getSlice(m, KeyOneOf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, v := range oneOf {
|
|
newSchema := &SubSchema{Property: KeyOneOf, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
currentSchema.oneOf = append(currentSchema.oneOf, newSchema)
|
|
err := d.parseSchema(v, newSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
anyOf, err := getSlice(m, KeyAnyOf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, v := range anyOf {
|
|
newSchema := &SubSchema{Property: KeyAnyOf, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
currentSchema.AnyOf = append(currentSchema.AnyOf, newSchema)
|
|
err := d.parseSchema(v, newSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
allOf, err := getSlice(m, KeyAllOf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, v := range allOf {
|
|
newSchema := &SubSchema{Property: KeyAllOf, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
currentSchema.AllOf = append(currentSchema.AllOf, newSchema)
|
|
err := d.parseSchema(v, newSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if vNot, found := m[KeyNot]; found {
|
|
switch vNot.(type) {
|
|
case bool, map[string]interface{}:
|
|
newSchema := &SubSchema{Property: KeyNot, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
currentSchema.not = newSchema
|
|
err := d.parseSchema(vNot, newSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfAn(),
|
|
ErrorDetails{"x": KeyNot, "y": TypeObject},
|
|
))
|
|
}
|
|
}
|
|
|
|
if *currentSchema.Draft >= Draft7 {
|
|
if vIf, found := m[KeyIf]; found {
|
|
switch vIf.(type) {
|
|
case bool, map[string]interface{}:
|
|
newSchema := &SubSchema{Property: KeyIf, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
currentSchema._if = newSchema
|
|
err := d.parseSchema(vIf, newSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfAn(),
|
|
ErrorDetails{"x": KeyIf, "y": TypeObject},
|
|
))
|
|
}
|
|
}
|
|
|
|
if then, found := m[KeyThen]; found {
|
|
switch then.(type) {
|
|
case bool, map[string]interface{}:
|
|
newSchema := &SubSchema{Property: KeyThen, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
currentSchema._then = newSchema
|
|
err := d.parseSchema(then, newSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfAn(),
|
|
ErrorDetails{"x": KeyThen, "y": TypeObject},
|
|
))
|
|
}
|
|
}
|
|
|
|
if vElse, found := m[KeyElse]; found {
|
|
switch vElse.(type) {
|
|
case bool, map[string]interface{}:
|
|
newSchema := &SubSchema{Property: KeyElse, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
currentSchema._else = newSchema
|
|
err := d.parseSchema(vElse, newSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfAn(),
|
|
ErrorDetails{"x": KeyElse, "y": TypeObject},
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Schema) parseReference(_ interface{}, currentSchema *SubSchema) error {
|
|
var (
|
|
refdDocumentNode interface{}
|
|
dsp *schemaPoolDocument
|
|
err error
|
|
)
|
|
|
|
newSchema := &SubSchema{Property: KeyRef, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
|
|
d.ReferencePool.Add(currentSchema.Ref.String(), newSchema)
|
|
|
|
dsp, err = d.Pool.GetDocument(*currentSchema.Ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newSchema.ID = currentSchema.Ref
|
|
|
|
refdDocumentNode = dsp.Document
|
|
newSchema.Draft = dsp.Draft
|
|
|
|
switch refdDocumentNode.(type) {
|
|
case bool, map[string]interface{}:
|
|
// expected
|
|
default:
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfType(),
|
|
ErrorDetails{"key": StringSchema, "type": TypeObject},
|
|
))
|
|
}
|
|
|
|
err = d.parseSchema(refdDocumentNode, newSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
currentSchema.RefSchema = newSchema
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
func (d *Schema) parseProperties(documentNode interface{}, currentSchema *SubSchema) error {
|
|
m, isMap := documentNode.(map[string]interface{})
|
|
if !isMap {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfType(),
|
|
ErrorDetails{"key": StringProperties, "type": TypeObject},
|
|
))
|
|
}
|
|
|
|
for k := range m {
|
|
schemaProperty := k
|
|
newSchema := &SubSchema{Property: schemaProperty, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
currentSchema.PropertiesChildren = append(currentSchema.PropertiesChildren, newSchema)
|
|
err := d.parseSchema(m[k], newSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Schema) parseDependencies(documentNode interface{}, currentSchema *SubSchema) error {
|
|
m, isMap := documentNode.(map[string]interface{})
|
|
if !isMap {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfType(),
|
|
ErrorDetails{"key": KeyDependencies, "type": TypeObject},
|
|
))
|
|
}
|
|
currentSchema.dependencies = make(map[string]interface{})
|
|
|
|
for k := range m {
|
|
switch values := m[k].(type) {
|
|
case []interface{}:
|
|
var valuesToRegister []string
|
|
for _, value := range values {
|
|
str, isString := value.(string)
|
|
if !isString {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfType(),
|
|
ErrorDetails{
|
|
"key": StringDependency,
|
|
"type": StringSchemaOrArrayOfStrings,
|
|
},
|
|
))
|
|
}
|
|
valuesToRegister = append(valuesToRegister, str)
|
|
currentSchema.dependencies[k] = valuesToRegister
|
|
}
|
|
|
|
case bool, map[string]interface{}:
|
|
depSchema := &SubSchema{Property: k, Parent: currentSchema, Ref: currentSchema.Ref}
|
|
err := d.parseSchema(m[k], depSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
currentSchema.dependencies[k] = depSchema
|
|
|
|
default:
|
|
return errors.New(formatErrorDescription(
|
|
Locale.MustBeOfType(),
|
|
ErrorDetails{
|
|
"key": StringDependency,
|
|
"type": StringSchemaOrArrayOfStrings,
|
|
},
|
|
))
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func invalidType(expected, given string) error {
|
|
return errors.New(formatErrorDescription(
|
|
Locale.InvalidType(),
|
|
ErrorDetails{
|
|
"expected": expected,
|
|
"given": given,
|
|
},
|
|
))
|
|
}
|
|
|
|
func getString(m map[string]interface{}, key string) (*string, error) {
|
|
v, found := m[key]
|
|
if !found {
|
|
// not found
|
|
return nil, nil
|
|
}
|
|
s, isString := v.(string)
|
|
if !isString {
|
|
// wrong type
|
|
return nil, invalidType(TypeString, key)
|
|
}
|
|
return &s, nil
|
|
}
|
|
|
|
func getMap(m map[string]interface{}, key string) (map[string]interface{}, error) {
|
|
v, found := m[key]
|
|
if !found {
|
|
// not found
|
|
return nil, nil
|
|
}
|
|
s, isMap := v.(map[string]interface{})
|
|
if !isMap {
|
|
// wrong type
|
|
return nil, invalidType(StringSchema, key)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func getSlice(m map[string]interface{}, key string) ([]interface{}, error) {
|
|
v, found := m[key]
|
|
if !found {
|
|
return nil, nil
|
|
}
|
|
s, isArray := v.([]interface{})
|
|
if !isArray {
|
|
return nil, errors.New(formatErrorDescription(
|
|
Locale.MustBeOfAn(),
|
|
ErrorDetails{"x": key, "y": TypeArray},
|
|
))
|
|
}
|
|
return s, nil
|
|
}
|