1// Copyright 2018 johandorland ( https://github.com/johandorland )
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package gojsonschema
16
17import (
18	"bytes"
19	"errors"
20
21	"github.com/xeipuuv/gojsonreference"
22)
23
24// SchemaLoader is used to load schemas
25type SchemaLoader struct {
26	pool       *schemaPool
27	AutoDetect bool
28	Validate   bool
29	Draft      Draft
30}
31
32// NewSchemaLoader creates a new NewSchemaLoader
33func NewSchemaLoader() *SchemaLoader {
34
35	ps := &SchemaLoader{
36		pool: &schemaPool{
37			schemaPoolDocuments: make(map[string]*schemaPoolDocument),
38		},
39		AutoDetect: true,
40		Validate:   false,
41		Draft:      Hybrid,
42	}
43	ps.pool.autoDetect = &ps.AutoDetect
44
45	return ps
46}
47
48func (sl *SchemaLoader) validateMetaschema(documentNode interface{}) error {
49
50	var (
51		schema string
52		err    error
53	)
54	if sl.AutoDetect {
55		schema, _, err = parseSchemaURL(documentNode)
56		if err != nil {
57			return err
58		}
59	}
60
61	// If no explicit "$schema" is used, use the default metaschema associated with the draft used
62	if schema == "" {
63		if sl.Draft == Hybrid {
64			return nil
65		}
66		schema = drafts.GetSchemaURL(sl.Draft)
67	}
68
69	//Disable validation when loading the metaschema to prevent an infinite recursive loop
70	sl.Validate = false
71
72	metaSchema, err := sl.Compile(NewReferenceLoader(schema))
73
74	if err != nil {
75		return err
76	}
77
78	sl.Validate = true
79
80	result := metaSchema.validateDocument(documentNode)
81
82	if !result.Valid() {
83		var res bytes.Buffer
84		for _, err := range result.Errors() {
85			res.WriteString(err.String())
86			res.WriteString("\n")
87		}
88		return errors.New(res.String())
89	}
90
91	return nil
92}
93
94// AddSchemas adds an arbritrary amount of schemas to the schema cache. As this function does not require
95// an explicit URL, every schema should contain an $id, so that it can be referenced by the main schema
96func (sl *SchemaLoader) AddSchemas(loaders ...JSONLoader) error {
97	emptyRef, _ := gojsonreference.NewJsonReference("")
98
99	for _, loader := range loaders {
100		doc, err := loader.LoadJSON()
101
102		if err != nil {
103			return err
104		}
105
106		if sl.Validate {
107			if err := sl.validateMetaschema(doc); err != nil {
108				return err
109			}
110		}
111
112		// Directly use the Recursive function, so that it get only added to the schema pool by $id
113		// and not by the ref of the document as it's empty
114		if err = sl.pool.parseReferences(doc, emptyRef, false); err != nil {
115			return err
116		}
117	}
118
119	return nil
120}
121
122//AddSchema adds a schema under the provided URL to the schema cache
123func (sl *SchemaLoader) AddSchema(url string, loader JSONLoader) error {
124
125	ref, err := gojsonreference.NewJsonReference(url)
126
127	if err != nil {
128		return err
129	}
130
131	doc, err := loader.LoadJSON()
132
133	if err != nil {
134		return err
135	}
136
137	if sl.Validate {
138		if err := sl.validateMetaschema(doc); err != nil {
139			return err
140		}
141	}
142
143	return sl.pool.parseReferences(doc, ref, true)
144}
145
146// Compile loads and compiles a schema
147func (sl *SchemaLoader) Compile(rootSchema JSONLoader) (*Schema, error) {
148
149	ref, err := rootSchema.JsonReference()
150
151	if err != nil {
152		return nil, err
153	}
154
155	d := Schema{}
156	d.pool = sl.pool
157	d.pool.jsonLoaderFactory = rootSchema.LoaderFactory()
158	d.documentReference = ref
159	d.referencePool = newSchemaReferencePool()
160
161	var doc interface{}
162	if ref.String() != "" {
163		// Get document from schema pool
164		spd, err := d.pool.GetDocument(d.documentReference)
165		if err != nil {
166			return nil, err
167		}
168		doc = spd.Document
169	} else {
170		// Load JSON directly
171		doc, err = rootSchema.LoadJSON()
172		if err != nil {
173			return nil, err
174		}
175		// References need only be parsed if loading JSON directly
176		//  as pool.GetDocument already does this for us if loading by reference
177		err = sl.pool.parseReferences(doc, ref, true)
178		if err != nil {
179			return nil, err
180		}
181	}
182
183	if sl.Validate {
184		if err := sl.validateMetaschema(doc); err != nil {
185			return nil, err
186		}
187	}
188
189	draft := sl.Draft
190	if sl.AutoDetect {
191		_, detectedDraft, err := parseSchemaURL(doc)
192		if err != nil {
193			return nil, err
194		}
195		if detectedDraft != nil {
196			draft = *detectedDraft
197		}
198	}
199
200	err = d.parse(doc, draft)
201	if err != nil {
202		return nil, err
203	}
204
205	return &d, nil
206}
207