From 83a7f6369d552cba07cdeea7dc622f1161a07deb Mon Sep 17 00:00:00 2001 From: Johan Dorland Date: Thu, 21 Dec 2017 15:03:42 +0100 Subject: [PATCH] Implement id parsing in conjuction with $ref fixes --- schema.go | 108 +++++++++++++++++++++++++++-------------- schemaPool.go | 20 +++----- schemaReferencePool.go | 5 +- schema_test.go | 9 ++-- subSchema.go | 4 +- 5 files changed, 89 insertions(+), 57 deletions(-) diff --git a/schema.go b/schema.go index 2cac71e..55cb1cf 100644 --- a/schema.go +++ b/schema.go @@ -27,7 +27,6 @@ package gojsonschema import ( - // "encoding/json" "errors" "reflect" "regexp" @@ -56,10 +55,11 @@ func NewSchema(l JSONLoader) (*Schema, error) { d.documentReference = ref d.referencePool = newSchemaReferencePool() + var spd *schemaPoolDocument var doc interface{} if ref.String() != "" { // Get document from schema pool - spd, err := d.pool.GetDocument(d.documentReference) + spd, err = d.pool.GetDocument(d.documentReference) if err != nil { return nil, err } @@ -70,8 +70,8 @@ func NewSchema(l JSONLoader) (*Schema, error) { if err != nil { return nil, err } - d.pool.SetStandaloneDocument(doc) } + d.pool.SetStandaloneDocument(doc) err = d.parse(doc) if err != nil { @@ -113,12 +113,48 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) }, )) } + if currentSchema.parent == nil { + currentSchema.ref = &d.documentReference + currentSchema.id = &d.documentReference + } + + if currentSchema.id == nil && currentSchema.parent != nil { + currentSchema.id = currentSchema.parent.id + } m := documentNode.(map[string]interface{}) - if currentSchema == d.rootSchema { - currentSchema.ref = &d.documentReference + // id + if existsMapKey(m, KEY_ID) && !isKind(m[KEY_ID], reflect.String) { + return errors.New(formatErrorDescription( + Locale.InvalidType(), + ErrorDetails{ + "expected": TYPE_STRING, + "given": KEY_ID, + }, + )) } + if k, ok := m[KEY_ID].(string); ok { + jsonReference, err := gojsonreference.NewJsonReference(k) + 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 + } + } + + // Add schema to document cache. The same id is passed down to subsequent + // subschemas, but as only the first and top one is used it will always reference + // the correct schema. Doing it once here prevents having + // to do this same step at every corner case. + d.referencePool.Add(currentSchema.id.String(), currentSchema) // $subSchema if existsMapKey(m, KEY_SCHEMA) { @@ -159,19 +195,17 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) if jsonReference.HasFullUrl { currentSchema.ref = &jsonReference } else { - inheritedReference, err := currentSchema.ref.Inherits(jsonReference) + inheritedReference, err := currentSchema.id.Inherits(jsonReference) if err != nil { return err } - currentSchema.ref = inheritedReference } - - if sch, ok := d.referencePool.Get(currentSchema.ref.String() + k); ok { + if sch, ok := d.referencePool.Get(currentSchema.ref.String()); ok { currentSchema.refSchema = sch - } else { - err := d.parseReference(documentNode, currentSchema, k) + err := d.parseReference(documentNode, currentSchema) + if err != nil { return err } @@ -186,11 +220,23 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) currentSchema.definitions = make(map[string]*subSchema) for dk, dv := range m[KEY_DEFINITIONS].(map[string]interface{}) { if isKind(dv, reflect.Map) { - newSchema := &subSchema{property: KEY_DEFINITIONS, parent: currentSchema, ref: currentSchema.ref} + + ref, err := gojsonreference.NewJsonReference("#/" + KEY_DEFINITIONS + "/" + dk) + if err != nil { + return err + } + + newSchemaID, err := currentSchema.id.Inherits(ref) + if err != nil { + return err + } + newSchema := &subSchema{property: KEY_DEFINITIONS, parent: currentSchema, id: newSchemaID} currentSchema.definitions[dk] = newSchema - err := d.parseSchema(dv, newSchema) + + err = d.parseSchema(dv, newSchema) + if err != nil { - return errors.New(err.Error()) + return err } } else { return errors.New(formatErrorDescription( @@ -214,20 +260,6 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) } - // id - if existsMapKey(m, KEY_ID) && !isKind(m[KEY_ID], reflect.String) { - return errors.New(formatErrorDescription( - Locale.InvalidType(), - ErrorDetails{ - "expected": TYPE_STRING, - "given": KEY_ID, - }, - )) - } - if k, ok := m[KEY_ID].(string); ok { - currentSchema.id = &k - } - // title if existsMapKey(m, KEY_TITLE) && !isKind(m[KEY_TITLE], reflect.String) { return errors.New(formatErrorDescription( @@ -798,26 +830,32 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *subSchema) return nil } -func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSchema, reference string) error { - var refdDocumentNode interface{} +func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSchema) error { + var ( + refdDocumentNode interface{} + dsp *schemaPoolDocument + err error + ) jsonPointer := currentSchema.ref.GetPointer() standaloneDocument := d.pool.GetStandaloneDocument() - if standaloneDocument != nil { + newSchema := &subSchema{property: KEY_REF, parent: currentSchema, ref: currentSchema.ref} - var err error + if currentSchema.ref.HasFragmentOnly { refdDocumentNode, _, err = jsonPointer.Get(standaloneDocument) if err != nil { return err } } else { - dsp, err := d.pool.GetDocument(*currentSchema.ref) + dsp, err = d.pool.GetDocument(*currentSchema.ref) if err != nil { return err } + newSchema.id = currentSchema.ref refdDocumentNode, _, err = jsonPointer.Get(dsp.Document) + if err != nil { return err } @@ -833,10 +871,8 @@ 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 } diff --git a/schemaPool.go b/schemaPool.go index f2ad641..ff9715f 100644 --- a/schemaPool.go +++ b/schemaPool.go @@ -62,12 +62,16 @@ func (p *schemaPool) GetStandaloneDocument() (document interface{}) { func (p *schemaPool) GetDocument(reference gojsonreference.JsonReference) (*schemaPoolDocument, error) { + var ( + spd *schemaPoolDocument + ok bool + err error + ) + if internalLogEnabled { internalLog("Get Document ( %s )", reference.String()) } - var err error - // It is not possible to load anything that is not canonical... if !reference.IsCanonical() { return nil, errors.New(formatErrorDescription( @@ -75,20 +79,10 @@ func (p *schemaPool) GetDocument(reference gojsonreference.JsonReference) (*sche ErrorDetails{"reference": reference}, )) } - refToUrl := reference refToUrl.GetUrl().Fragment = "" - var spd *schemaPoolDocument - - // Try to find the requested document in the pool - for k := range p.schemaPoolDocuments { - if k == refToUrl.String() { - spd = p.schemaPoolDocuments[k] - } - } - - if spd != nil { + if spd, ok = p.schemaPoolDocuments[refToUrl.String()]; ok { if internalLogEnabled { internalLog(" From pool") } diff --git a/schemaReferencePool.go b/schemaReferencePool.go index 294e36a..6e5e1b5 100644 --- a/schemaReferencePool.go +++ b/schemaReferencePool.go @@ -62,6 +62,7 @@ func (p *schemaReferencePool) Add(ref string, sch *subSchema) { if internalLogEnabled { internalLog(fmt.Sprintf("Add Schema Reference %s to pool", ref)) } - - p.documents[ref] = sch + if _, ok := p.documents[ref]; !ok { + p.documents[ref] = sch + } } diff --git a/schema_test.go b/schema_test.go index 453dfb8..9f47e4b 100644 --- a/schema_test.go +++ b/schema_test.go @@ -360,12 +360,10 @@ func TestJsonSchemaTestSuite(t *testing.T) { {"phase": "format validation", "test": "uri format is invalid", "schema": "format/schema_6.json", "data": "format/data_13.json", "valid": "false", "errors": "format"}, {"phase": "format validation", "test": "number format is valid", "schema": "format/schema_7.json", "data": "format/data_29.json", "valid": "true"}, {"phase": "format validation", "test": "number format is valid", "schema": "format/schema_7.json", "data": "format/data_30.json", "valid": "false", "errors": "format"}, + {"phase": "change resolution scope", "test": "changed scope ref valid", "schema": "refRemote/schema_3.json", "data": "refRemote/data_30.json", "valid": "true"}, + {"phase": "change resolution scope", "test": "changed scope ref invalid", "schema": "refRemote/schema_3.json", "data": "refRemote/data_31.json", "valid": "false", "errors": "invalid_type"}, } - //TODO Pass failed tests : id(s) as scope for references is not implemented yet - //map[string]string{"phase": "change resolution scope", "test": "changed scope ref valid", "schema": "refRemote/schema_3.json", "data": "refRemote/data_30.json", "valid": "true"}, - //map[string]string{"phase": "change resolution scope", "test": "changed scope ref invalid", "schema": "refRemote/schema_3.json", "data": "refRemote/data_31.json", "valid": "false"}} - // Setup a small http server on localhost:1234 for testing purposes wd, err := os.Getwd() @@ -416,6 +414,9 @@ func TestJsonSchemaTestSuite(t *testing.T) { expectedValid, _ := strconv.ParseBool(testJson["valid"]) if givenValid != expectedValid { t.Errorf("Test failed : %s :: %s, expects %t, given %t\n", testJson["phase"], testJson["test"], expectedValid, givenValid) + for _, e := range result.Errors() { + fmt.Println("Error: " + e.Type()) + } } if !givenValid && testJson["errors"] != "" { diff --git a/subSchema.go b/subSchema.go index 9ddbb5f..9961d92 100644 --- a/subSchema.go +++ b/subSchema.go @@ -36,7 +36,7 @@ import ( const ( KEY_SCHEMA = "$subSchema" - KEY_ID = "$id" + KEY_ID = "id" KEY_REF = "$ref" KEY_TITLE = "title" KEY_DESCRIPTION = "description" @@ -73,7 +73,7 @@ const ( type subSchema struct { // basic subSchema meta properties - id *string + id *gojsonreference.JsonReference title *string description *string