// 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 resources pooling. // Eases referencing and avoids downloading the same resource twice. // // created 26-02-2013 package gojsonschema import ( "errors" "fmt" "reflect" "github.com/xeipuuv/gojsonreference" ) type schemaPoolDocument struct { Document interface{} Draft *Draft } type schemaPool struct { schemaPoolDocuments map[string]*schemaPoolDocument jsonLoaderFactory JSONLoaderFactory autoDetect *bool } func (p *schemaPool) parseReferences(document interface{}, ref gojsonreference.JsonReference, pooled bool) error { var ( draft *Draft err error reference = ref.String() ) // Only the root document should be added to the schema pool if pooled is true if _, ok := p.schemaPoolDocuments[reference]; pooled && ok { return fmt.Errorf("Reference already exists: \"%s\"", reference) } if *p.autoDetect { _, draft, err = parseSchemaURL(document) if err != nil { return err } } err = p.parseReferencesRecursive(document, ref, draft) if pooled { p.schemaPoolDocuments[reference] = &schemaPoolDocument{Document: document, Draft: draft} } return err } func (p *schemaPool) parseReferencesRecursive(document interface{}, ref gojsonreference.JsonReference, draft *Draft) error { // parseReferencesRecursive parses a JSON document and resolves all $id and $ref references. // For $ref references it takes into account the $id scope it is in and replaces // the reference by the absolute resolved reference // When encountering errors it fails silently. Error handling is done when the schema // is syntactically parsed and any error encountered here should also come up there. switch m := document.(type) { case []interface{}: for _, v := range m { p.parseReferencesRecursive(v, ref, draft) } case map[string]interface{}: localRef := &ref keyID := KEY_ID_NEW if existsMapKey(m, KEY_ID) { keyID = KEY_ID } if existsMapKey(m, keyID) && isKind(m[keyID], reflect.String) { jsonReference, err := gojsonreference.NewJsonReference(m[keyID].(string)) if err == nil { localRef, err = ref.Inherits(jsonReference) if err == nil { if _, ok := p.schemaPoolDocuments[localRef.String()]; ok { return fmt.Errorf("Reference already exists: \"%s\"", localRef.String()) } p.schemaPoolDocuments[localRef.String()] = &schemaPoolDocument{Document: document, Draft: draft} } } } if existsMapKey(m, KEY_REF) && isKind(m[KEY_REF], reflect.String) { jsonReference, err := gojsonreference.NewJsonReference(m[KEY_REF].(string)) if err == nil { absoluteRef, err := localRef.Inherits(jsonReference) if err == nil { m[KEY_REF] = absoluteRef.String() } } } for k, v := range m { // const and enums should be interpreted literally, so ignore them if k == KEY_CONST || k == KEY_ENUM { continue } // Something like a property or a dependency is not a valid schema, as it might describe properties named "$ref", "$id" or "const", etc // Therefore don't treat it like a schema. if k == KEY_PROPERTIES || k == KEY_DEPENDENCIES || k == KEY_PATTERN_PROPERTIES { if child, ok := v.(map[string]interface{}); ok { for _, v := range child { p.parseReferencesRecursive(v, *localRef, draft) } } } else { p.parseReferencesRecursive(v, *localRef, draft) } } } return nil } func (p *schemaPool) GetDocument(reference gojsonreference.JsonReference) (*schemaPoolDocument, error) { var ( spd *schemaPoolDocument draft *Draft ok bool err error ) if internalLogEnabled { internalLog("Get Document ( %s )", reference.String()) } // Create a deep copy, so we can remove the fragment part later on without altering the original refToUrl, _ := gojsonreference.NewJsonReference(reference.String()) // First check if the given fragment is a location independent identifier // http://json-schema.org/latest/json-schema-core.html#rfc.section.8.2.3 if spd, ok = p.schemaPoolDocuments[refToUrl.String()]; ok { if internalLogEnabled { internalLog(" From pool") } return spd, nil } // If the given reference is not a location independent identifier, // strip the fragment and look for a document with it's base URI refToUrl.GetUrl().Fragment = "" if cachedSpd, ok := p.schemaPoolDocuments[refToUrl.String()]; ok { document, _, err := reference.GetPointer().Get(cachedSpd.Document) if err != nil { return nil, err } if internalLogEnabled { internalLog(" From pool") } spd = &schemaPoolDocument{Document: document, Draft: cachedSpd.Draft} p.schemaPoolDocuments[reference.String()] = spd return spd, nil } // It is not possible to load anything remotely that is not canonical... if !reference.IsCanonical() { return nil, errors.New(formatErrorDescription( Locale.ReferenceMustBeCanonical(), ErrorDetails{"reference": reference.String()}, )) } jsonReferenceLoader := p.jsonLoaderFactory.New(reference.String()) document, err := jsonReferenceLoader.LoadJSON() if err != nil { return nil, err } // add the whole document to the pool for potential re-use p.parseReferences(document, refToUrl, true) _, draft, _ = parseSchemaURL(document) // resolve the potential fragment and also cache it document, _, err = reference.GetPointer().Get(document) if err != nil { return nil, err } return &schemaPoolDocument{Document: document, Draft: draft}, nil }