Skip to content

Commit

Permalink
refactor(jsonschema): reworking how we handle json schema parsing and…
Browse files Browse the repository at this point in the history
… validating
  • Loading branch information
Arqu committed Apr 28, 2020
1 parent 3f1bc86 commit b60b331
Show file tree
Hide file tree
Showing 30 changed files with 3,182 additions and 2,718 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.DS_Store
coverage.txt
coverage.txt
.vscode
69 changes: 69 additions & 0 deletions draft2019_09_keywords.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

func LoadDraft2019_09() {
// core keywords
// TODO: take a look at these three
// RegisterKeyword("$schema", NewSchemaURI)
// RegisterKeyword("$id", NewId)
// RegisterKeyword("default", NewDefault)
RegisterKeyword("description", NewDescription)
RegisterKeyword("title", NewTitle)
RegisterKeyword("$comment", NewComment)
RegisterKeyword("examples", NewExamples)
RegisterKeyword("readOnly", NewReadOnly)
RegisterKeyword("writeOnly", NewWriteOnly)

// standard keywords
RegisterKeyword("type", NewType)
RegisterKeyword("enum", NewEnum)
RegisterKeyword("const", NewConst)

// numeric keywords
RegisterKeyword("multipleOf", NewMultipleOf)
RegisterKeyword("maximum", NewMaximum)
RegisterKeyword("exclusiveMaximum", NewExclusiveMaximum)
RegisterKeyword("minimum", NewMinimum)
RegisterKeyword("exclusiveMinimum", NewExclusiveMinimum)

// string keywords
RegisterKeyword("maxLength", NewMaxLength)
RegisterKeyword("minLength", NewMinLength)
RegisterKeyword("pattern", NewPattern)

// boolean keywords
RegisterKeyword("allOf", NewAllOf)
RegisterKeyword("anyOf", NewAnyOf)
RegisterKeyword("oneOf", NewOneOf)
RegisterKeyword("not", NewNot)

// object keywords
RegisterKeyword("properties", NewProperties)
RegisterKeyword("patternProperties", NewPatternProperties)
RegisterKeyword("additionalProperties", NewAdditionalProperties)
RegisterKeyword("required", NewRequired)
RegisterKeyword("propertyNames", NewPropertyNames)
RegisterKeyword("maxProperties", NewMaxProperties)
RegisterKeyword("minProperties", NewMinProperties)
//Check if this is stil actual
RegisterKeyword("dependencies", NewDependencies)

SetKeywordOrder("properties", 2)
SetKeywordOrder("additionalProperties", 3)

// array keywords
RegisterKeyword("items", NewItems)
RegisterKeyword("additionalItems", NewAdditionalItems)
RegisterKeyword("maxItems", NewMaxItems)
RegisterKeyword("minItems", NewMinItems)
RegisterKeyword("uniqueItems", NewUniqueItems)
RegisterKeyword("contains", NewContains)
RegisterKeyword("maxContains", NewMaxContains)
RegisterKeyword("minContains", NewMinContains)

SetKeywordOrder("maxContains", 2)
SetKeywordOrder("minContains", 2)
SetKeywordOrder("additionalItems", 3)

//optional formats
RegisterKeyword("format", NewFormat)
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ require (
github.com/sergi/go-diff v1.0.0
github.com/stretchr/testify v1.3.0 // indirect
)

replace github.com/qri-io/jsonpointer => /Users/arqu/dev/qri/jsonpointer
189 changes: 189 additions & 0 deletions keyword.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package main

import (
"bytes"
"encoding/json"
"fmt"
jptr "github.com/qri-io/jsonpointer"
)

var notSupported = map[string]bool{
// "$schema": true,
// "$id": true,
"$anchor": true,
"$recursiveAnchor": true,
"$defs": true,
"definitions": true,
"$ref": true,
"$recursiveRef": true,
// "title": true,
// "description": true,
// "default": true,
// "examples": true,
// "readOnly": true,
// "writeOnly": true,
// "$comment": true,
"$vocabulary": true,

// array keywords
// "items": true,
// "additionalItems": true,
// "maxItems": true,
// "minItems": true,
// "uniqueItems": true,
// "contains": true,
// "maxContains": true,
// "minContains": true,
"unevaluatedItems": true,

// object keywords
"unevaluatedProperties": true,

// conditional keywords
"if": true,
"then": true,
"else": true,

// other
"contentEncoding": true,
"contentMediaType": true,
"contentSchema": true,
"dependentRequired": true,
"dependentSchemas": true,
"deprecated": true,
}

var KeywordRegistry = map[string]KeyMaker{}
var KeywordOrder = map[string]int{}

func IsKeyword(prop string) bool {
_, ok := KeywordRegistry[prop]
return ok
}

func GetKeyword(prop string) Keyword {
if !IsKeyword(prop) {
return NewVoid()
}
return KeywordRegistry[prop]()
}

func GetKeywordOrder(prop string) int {
if order, ok := KeywordOrder[prop]; ok {
return order
}
return 1
}

func SetKeywordOrder(prop string, order int) {
KeywordOrder[prop] = order
}

func IsNotSupportedKeyword(prop string) bool {
_, ok := notSupported[prop]
return ok
}

func RegisterKeyword(prop string, maker KeyMaker) {
KeywordRegistry[prop] = maker
}

// MaxValueErrStringLen sets how long a value can be before it's length is truncated
// when printing error strings
// a special value of -1 disables output trimming
var MaxKeywordErrStringLen = 20

// Validator is an interface for anything that can validate.
// JSON-Schema keywords are all examples of validators
type Keyword interface {
// Validate checks decoded JSON data and writes
// validation errors (if any) to an outparam slice of ValErrors
// propPath indicates the position of data in the json tree
Validate(propPath string, data interface{}, errs *[]KeyError)
ValidateFromContext(schCtx *SchemaContext, errs *[]KeyError)

Register(uri string, registry *SchemaRegistry)
Resolve(pointer jptr.Pointer, uri string) *Schema
}

// BaseValidator is a foundation for building a validator
type BaseKeyword struct {
path string
}

// SetPath sets base validator's path
func (b *BaseKeyword) SetPath(path string) {
b.path = path
}

// Path gives this validator's path
func (b BaseKeyword) Path() string {
return b.path
}

// AddError is a convenience method for appending a new error to an existing error slice
func (b BaseKeyword) AddError(errs *[]KeyError, propPath string, data interface{}, msg string) {
*errs = append(*errs, KeyError{
PropertyPath: propPath,
RulePath: b.Path(),
InvalidValue: data,
Message: msg,
})
}

// ValMaker is a function that generates instances of a validator.
// Calls to ValMaker will be passed directly to json.Marshal,
// so the returned value should be a pointer
type KeyMaker func() Keyword

// ValError represents a single error in an instance of a schema
// The only absolutely-required property is Message.
type KeyError struct {
// PropertyPath is a string path that leads to the
// property that produced the error
PropertyPath string `json:"propertyPath,omitempty"`
// InvalidValue is the value that returned the error
InvalidValue interface{} `json:"invalidValue,omitempty"`
// RulePath is the path to the rule that errored
RulePath string `json:"rulePath,omitempty"`
// Message is a human-readable description of the error
Message string `json:"message"`
}

// Error implements the error interface for ValError
func (v KeyError) Error() string {
// [propPath]: [value] [message]
if v.PropertyPath != "" && v.InvalidValue != nil {
return fmt.Sprintf("%s: %s %s", v.PropertyPath, InvalidValueString(v.InvalidValue), v.Message)
} else if v.PropertyPath != "" {
return fmt.Sprintf("%s: %s", v.PropertyPath, v.Message)
}
return v.Message
}

// InvalidValueString returns the errored value as a string
func InvalidValueString(data interface{}) string {
bt, err := json.Marshal(data)
if err != nil {
return ""
}
bt = bytes.Replace(bt, []byte{'\n', '\r'}, []byte{' '}, -1)
if MaxKeywordErrStringLen != -1 && len(bt) > MaxKeywordErrStringLen {
bt = append(bt[:MaxKeywordErrStringLen], []byte("...")...)
}
return string(bt)
}

func (k KeyError) String() string {
return fmt.Sprintf("for: '%s' msg:'%s'", k.InvalidValue, k.Message)
}

// AddError creates and appends a ValError to errs
func AddError(errs *[]KeyError, propPath string, data interface{}, msg string) {
*errs = append(*errs, KeyError{
PropertyPath: propPath,
InvalidValue: data,
Message: msg,
})
}

Loading

0 comments on commit b60b331

Please sign in to comment.