diff --git a/keywords.go b/keywords.go index 584ddc1..343517e 100644 --- a/keywords.go +++ b/keywords.go @@ -60,13 +60,13 @@ func (t Type) Validate(data interface{}) error { } if len(t) == 1 { return fmt.Errorf(`expected "%v" to be of type %s`, data, t[0]) - } else { - str := "" - for _, ts := range t { - str += ts + "," - } - return fmt.Errorf(`expected "%v" to be one of type: %s`, data, str[:len(str)-1]) } + + str := "" + for _, ts := range t { + str += ts + "," + } + return fmt.Errorf(`expected "%v" to be one of type: %s`, data, str[:len(str)-1]) } // JSONProp implements JSON property name indexing for Type diff --git a/keywords_arrays.go b/keywords_arrays.go index 96f84b9..8d76efa 100644 --- a/keywords_arrays.go +++ b/keywords_arrays.go @@ -56,6 +56,7 @@ func (it Items) JSONProp(name string) interface{} { return it.Schemas[idx] } +// JSONChildren implements the JSONContainer interface for Items func (it Items) JSONChildren() (res map[string]JSONPather) { res = map[string]JSONPather{} for i, sch := range it.Schemas { @@ -202,8 +203,8 @@ func (c *Contains) Validate(data interface{}) error { } // JSONProp implements JSON property name indexing for Contains -func (m Contains) JSONProp(name string) interface{} { - return Schema(m).JSONProp(name) +func (c Contains) JSONProp(name string) interface{} { + return Schema(c).JSONProp(name) } // UnmarshalJSON implements the json.Unmarshaler interface for Contains diff --git a/keywords_booleans.go b/keywords_booleans.go index 8f6cfc4..d49ba00 100644 --- a/keywords_booleans.go +++ b/keywords_booleans.go @@ -32,6 +32,15 @@ func (a AllOf) JSONProp(name string) interface{} { return a[idx] } +// JSONChildren implements the JSONContainer interface for AllOf +func (a AllOf) JSONChildren() (res map[string]JSONPather) { + res = map[string]JSONPather{} + for i, sch := range a { + res[strconv.Itoa(i)] = sch + } + return +} + // AnyOf MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema. // An instance validates successfully against this keyword if it validates successfully against at // least one schema defined by this keyword's value. @@ -59,6 +68,15 @@ func (a AnyOf) JSONProp(name string) interface{} { return a[idx] } +// JSONChildren implements the JSONContainer interface for AnyOf +func (a AnyOf) JSONChildren() (res map[string]JSONPather) { + res = map[string]JSONPather{} + for i, sch := range a { + res[strconv.Itoa(i)] = sch + } + return +} + // OneOf MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema. // An instance validates successfully against this keyword if it validates successfully against exactly one schema defined by this keyword's value. type OneOf []*Schema @@ -92,6 +110,15 @@ func (o OneOf) JSONProp(name string) interface{} { return o[idx] } +// JSONChildren implements the JSONContainer interface for OneOf +func (o OneOf) JSONChildren() (res map[string]JSONPather) { + res = map[string]JSONPather{} + for i, sch := range o { + res[strconv.Itoa(i)] = sch + } + return +} + // Not MUST be a valid JSON Schema. // An instance is valid against this keyword if it fails to validate successfully against the schema defined // by this keyword. diff --git a/keywords_conditionals.go b/keywords_conditionals.go index c7319e0..6a1f1db 100644 --- a/keywords_conditionals.go +++ b/keywords_conditionals.go @@ -8,16 +8,33 @@ import ( // Instances that successfully validate against this keyword's subschema MUST also be valid against the subschema value of the "then" keyword, if present. // Instances that fail to validate against this keyword's subschema MUST also be valid against the subschema value of the "else" keyword. // Validation of the instance against this keyword on its own always succeeds, regardless of the validation outcome of against its subschema. -type If Schema +type If struct { + Schema Schema + Then *Then + Else *Else +} // Validate implements the Validator interface for If func (i *If) Validate(data interface{}) error { + if err := i.Schema.Validate(data); err == nil { + if i.Then != nil { + s := Schema(*i.Then) + sch := &s + return sch.Validate(data) + } + } else { + if i.Else != nil { + s := Schema(*i.Else) + sch := &s + return sch.Validate(data) + } + } return nil } // JSONProp implements JSON property name indexing for If func (i If) JSONProp(name string) interface{} { - return Schema(i).JSONProp(name) + return Schema(i.Schema).JSONProp(name) } // UnmarshalJSON implements the json.Unmarshaler interface for If @@ -26,7 +43,7 @@ func (i *If) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &sch); err != nil { return err } - *i = If(sch) + *i = If{Schema: sch} return nil } diff --git a/keywords_objects.go b/keywords_objects.go index a20b338..c43a254 100644 --- a/keywords_objects.go +++ b/keywords_objects.go @@ -92,6 +92,7 @@ func (p Properties) JSONProp(name string) interface{} { return p[name] } +// JSONChildren implements the JSONContainer interface for Properties func (p Properties) JSONChildren() (res map[string]JSONPather) { res = map[string]JSONPather{} for key, sch := range p { diff --git a/schema.go b/schema.go index d54b816..95bf5dc 100644 --- a/schema.go +++ b/schema.go @@ -9,6 +9,8 @@ import ( "encoding/json" "fmt" "github.com/qri-io/jsonpointer" + // "io/ioutil" + "net/http" "net/url" ) @@ -73,9 +75,7 @@ func (rs *RootSchema) UnmarshalJSON(data []byte) error { if err := walkJSON(sch, func(elem JSONPather) error { if sch, ok := elem.(*Schema); ok { if sch.Ref != "" { - // fmt.Println(sch.Ref, ids[sch.Ref]) if ids[sch.Ref] != nil { - fmt.Println("using id:", sch.Ref) sch.ref = ids[sch.Ref] return nil } @@ -107,7 +107,59 @@ func (rs *RootSchema) UnmarshalJSON(data []byte) error { return nil } -func (rs *RootSchema) ValdiateBytes(data []byte) error { +// FetchRemoteReferences grabs any url-based schema references that cannot +// be locally resolved via network requests +func (rs *RootSchema) FetchRemoteReferences() error { + sch := &rs.Schema + + // collect IDs for internal referencing: + refs := map[string]*Schema{} + if err := walkJSON(sch, func(elem JSONPather) error { + if sch, ok := elem.(*Schema); ok { + ref := sch.Ref + if ref != "" { + if refs[ref] == nil { + if u, err := url.Parse(ref); err == nil { + if res, err := http.Get(u.String()); err == nil { + s := &RootSchema{} + if err := json.NewDecoder(res.Body).Decode(s); err != nil { + return err + } + refs[ref] = &s.Schema + sch.ref = refs[ref] + } + } + } + } + } + return nil + }); err != nil { + return err + } + + // pass a pointer to the schema component in here (instead of the RootSchema struct) + // to ensure root is evaluated for references + if err := walkJSON(sch, func(elem JSONPather) error { + if sch, ok := elem.(*Schema); ok { + if sch.Ref != "" && refs[sch.Ref] != nil { + if refs[sch.Ref] != nil { + fmt.Println("using remote ref:", sch.Ref) + sch.ref = refs[sch.Ref] + } + return nil + } + } + return nil + }); err != nil { + return err + } + + rs.Schema = *sch + return nil +} + +// ValidateBytes performs schema validation against a slice of json byte data +func (rs *RootSchema) ValidateBytes(data []byte) error { var doc interface{} if err := json.Unmarshal(data, &doc); err != nil { return err @@ -115,8 +167,8 @@ func (rs *RootSchema) ValdiateBytes(data []byte) error { return rs.Validate(doc) } -func (s *RootSchema) evalJSONValidatorPointer(ptr jsonpointer.Pointer) (res interface{}, err error) { - res = s +func (rs *RootSchema) evalJSONValidatorPointer(ptr jsonpointer.Pointer) (res interface{}, err error) { + res = rs for _, token := range ptr { if adr, ok := res.(JSONPather); ok { res = adr.JSONProp(token) @@ -164,8 +216,6 @@ const ( type Schema struct { // internal tracking for true/false/{...} schemas schemaType schemaType - // reference to root for ref parsing - root *RootSchema // The "$id" keyword defines a URI for the schema, // and the base URI that other URI references within the schema are resolved against. // A subschema's "$id" is resolved against the base URI of its parent schema. @@ -237,6 +287,11 @@ type Schema struct { // might get stuck in an infinite recursive loop trying to validate the instance. // Schemas SHOULD NOT make use of infinite recursive nesting like this; the behavior is undefined. Ref string `json:"$ref,omitempty"` + // Format functions as both an annotation (Section 3.3) and as an assertion (Section 3.2). + // While no special effort is required to implement it as an annotation conveying semantic meaning, + // implementing validation is non-trivial. + Format string `json:"format,omitempty"` + ref Validator // Definitions provides a standardized location for schema authors to inline re-usable JSON Schemas @@ -261,6 +316,7 @@ type _schema struct { Comment string `json:"comment,omitempty"` Ref string `json:"$ref,omitempty"` Definitions map[string]*Schema `json:"definitions,omitempty"` + Format string `json:"format,omitempty"` } // JSONProp implements the JSONPather for Schema @@ -286,6 +342,8 @@ func (s Schema) JSONProp(name string) interface{} { return s.Ref case "definitions": return s.Definitions + case "format": + return s.Format default: prop := s.Validators[name] if prop == nil && s.extraDefinitions[name] != nil { @@ -295,7 +353,7 @@ func (s Schema) JSONProp(name string) interface{} { } } -// JSONChildren implements the JSONPather for Schema +// JSONChildren implements the JSONContainer interface for Schema func (s *Schema) JSONChildren() (ch map[string]JSONPather) { ch = map[string]JSONPather{} @@ -350,6 +408,7 @@ func (s *Schema) UnmarshalJSON(data []byte) error { Comment: _s.Comment, Ref: _s.Ref, Definitions: _s.Definitions, + Format: _s.Format, Validators: map[string]Validator{}, } @@ -377,7 +436,7 @@ func (s *Schema) UnmarshalJSON(data []byte) error { // props to validator factory functions switch prop { // skip any already-parsed props - case "$id", "title", "description", "default", "examples", "readOnly", "writeOnly", "comment", "$ref", "definitions": + case "$schema", "$id", "title", "description", "default", "examples", "readOnly", "writeOnly", "comment", "$ref", "definitions", "format": continue case "type": val = new(Type) @@ -461,6 +520,17 @@ func (s *Schema) UnmarshalJSON(data []byte) error { sch.Validators[prop] = val } + if sch.Validators["if"] != nil { + if ite, ok := sch.Validators["if"].(*If); ok { + if s, ok := sch.Validators["then"].(*Then); ok { + ite.Then = s + } + if s, ok := sch.Validators["else"].(*Else); ok { + ite.Else = s + } + } + } + // TODO - replace all these assertions with methods on Schema that return proper types if sch.Validators["items"] != nil && sch.Validators["additionalItems"] != nil && !sch.Validators["items"].(*Items).single { sch.Validators["additionalItems"].(*AdditionalItems).startIndex = len(sch.Validators["items"].(*Items).Schemas) @@ -482,6 +552,9 @@ func (s *Schema) Validate(data interface{}) error { return s.ref.Validate(data) } + // TODO - so far all default.json tests pass when no use of "default" is made. + // Is this correct? + for _, v := range s.Validators { if err := v.Validate(data); err != nil { return err @@ -490,12 +563,16 @@ func (s *Schema) Validate(data interface{}) error { return nil } +// Definitions implements a map of schemas while also satsfying the JSON +// traversal methods type Definitions map[string]*Schema +// JSONProp implements the JSONPather for Definitions func (d Definitions) JSONProp(name string) interface{} { return d[name] } +// JSONChildren implements the JSONContainer interface for Definitions func (d Definitions) JSONChildren() (r map[string]JSONPather) { r = map[string]JSONPather{} // fmt.Println("getting children for definitions:", d) diff --git a/schema_test.go b/schema_test.go index b85838a..2839cd3 100644 --- a/schema_test.go +++ b/schema_test.go @@ -10,8 +10,6 @@ import ( "testing" ) -var _ JSONPather = &Schema{} - func Example() { var schemaData = []byte(`{ "title": "Person", @@ -45,14 +43,14 @@ func Example() { "firstName" : "Brendan", "lastName" : "O'Brien" }`) - if err := rs.ValdiateBytes(valid); err != nil { + if err := rs.ValidateBytes(valid); err != nil { panic(err) } var invalidPerson = []byte(`{ "firstName" : "Brendan" }`) - err := rs.ValdiateBytes(invalidPerson) + err := rs.ValidateBytes(invalidPerson) fmt.Println(err.Error()) var invalidFriend = []byte(`{ @@ -62,7 +60,7 @@ func Example() { "firstName" : "Margaux" }] }`) - err = rs.ValdiateBytes(invalidFriend) + err = rs.ValidateBytes(invalidFriend) fmt.Println(err) // Output: "lastName" value is required @@ -183,41 +181,41 @@ func TestDraft6(t *testing.T) { func TestDraft7(t *testing.T) { runJSONTests(t, []string{ - // "testdata/draft7/additionalItems.json", - // "testdata/draft7/contains.json", - // "testdata/draft7/exclusiveMinimum.json", - // "testdata/draft7/maximum.json", - // "testdata/draft7/not.json", - // "testdata/draft7/propertyNames.json", - // "testdata/draft7/additionalProperties.json", - // "testdata/draft7/default.json", - // "testdata/draft7/if-then-else.json", - // "testdata/draft7/minItems.json", - // "testdata/draft7/oneOf.json", + "testdata/draft7/additionalItems.json", + "testdata/draft7/contains.json", + "testdata/draft7/exclusiveMinimum.json", + "testdata/draft7/maximum.json", + "testdata/draft7/not.json", + "testdata/draft7/propertyNames.json", + "testdata/draft7/additionalProperties.json", + "testdata/draft7/default.json", + "testdata/draft7/if-then-else.json", + "testdata/draft7/minItems.json", + "testdata/draft7/oneOf.json", "testdata/draft7/ref.json", - // "testdata/draft7/allOf.json", + "testdata/draft7/allOf.json", // "testdata/draft7/definitions.json", - // "testdata/draft7/items.json", - // "testdata/draft7/minLength.json", + "testdata/draft7/items.json", + "testdata/draft7/minLength.json", // "testdata/draft7/refRemote.json", - // "testdata/draft7/anyOf.json", + "testdata/draft7/anyOf.json", // "testdata/draft7/dependencies.json", - // "testdata/draft7/maxItems.json", - // "testdata/draft7/minProperties.json", - // "testdata/draft7/pattern.json", - // "testdata/draft7/required.json", + "testdata/draft7/maxItems.json", + "testdata/draft7/minProperties.json", + "testdata/draft7/pattern.json", + "testdata/draft7/required.json", // "testdata/draft7/boolean_schema.json", - // "testdata/draft7/enum.json", - // "testdata/draft7/maxLength.json", - // "testdata/draft7/minimum.json", - // "testdata/draft7/patternProperties.json", - // "testdata/draft7/type.json", - // "testdata/draft7/const.json", - // "testdata/draft7/exclusiveMaximum.json", - // "testdata/draft7/maxProperties.json", - // "testdata/draft7/multipleOf.json", - // "testdata/draft7/properties.json", - // "testdata/draft7/uniqueItems.json", + "testdata/draft7/enum.json", + "testdata/draft7/maxLength.json", + "testdata/draft7/minimum.json", + "testdata/draft7/patternProperties.json", + "testdata/draft7/type.json", + "testdata/draft7/const.json", + "testdata/draft7/exclusiveMaximum.json", + "testdata/draft7/maxProperties.json", + "testdata/draft7/multipleOf.json", + "testdata/draft7/properties.json", + "testdata/draft7/uniqueItems.json", // "testdata/draft7/optional/bignum.json", // "testdata/draft7/optional/content.json", @@ -277,6 +275,10 @@ func runJSONTests(t *testing.T, testFilepaths []string) { for _, ts := range testSets { sc := ts.Schema + if err := sc.FetchRemoteReferences(); err != nil { + t.Errorf("%s: %s error fetching remote references: %s", base, ts.Description, err.Error()) + continue + } for i, c := range ts.Tests { tests++ got := sc.Validate(c.Data) diff --git a/traversal.go b/traversal.go index 0de1be1..c359c0c 100644 --- a/traversal.go +++ b/traversal.go @@ -4,17 +4,19 @@ package jsonschema // "fmt" // ) +// JSONPather makes validators traversible by JSON-pointers, +// which is required to support references in JSON schemas. type JSONPather interface { - // JSONProp makes validators traversible by JSON-pointers, - // which is required to support references in JSON schemas. - // for a given JSON property name the validator must - // return any matching property of that name + // JSONProp take a string references for a given JSON property + // implementations must return any matching property of that name // or nil if no such subproperty exists. // Note this also applies to array values, which are expected to interpret // valid numbers as an array index JSONProp(name string) interface{} } +// JSONContainer is an interface that enables tree traversal by listing +// the immideate children of an object type JSONContainer interface { // JSONChildren should return all immidiate children of this element JSONChildren() map[string]JSONPather diff --git a/traversal_test.go b/traversal_test.go index e3957ed..8278769 100644 --- a/traversal_test.go +++ b/traversal_test.go @@ -20,7 +20,7 @@ func TestSchemaDeref(t *testing.T) { return } - got := rs.ValdiateBytes([]byte(`"a"`)) + got := rs.ValidateBytes([]byte(`"a"`)) if got == nil { t.Errorf("expected error, got nil") return