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
24type SchemaLoader struct {
25	pool       *schemaPool
26	AutoDetect bool
27	Validate   bool
28	Draft      Draft
29}
30
31func NewSchemaLoader() *SchemaLoader {
32
33	ps := &SchemaLoader{
34		pool: &schemaPool{
35			schemaPoolDocuments: make(map[string]*schemaPoolDocument),
36		},
37		AutoDetect: true,
38		Validate:   false,
39		Draft:      Hybrid,
40	}
41	ps.pool.autoDetect = &ps.AutoDetect
42
43	return ps
44}
45
46func (sl *SchemaLoader) validateMetaschema(documentNode interface{}) error {
47
48	var (
49		schema string
50		err    error
51	)
52	if sl.AutoDetect {
53		schema, _, err = parseSchemaURL(documentNode)
54		if err != nil {
55			return err
56		}
57	}
58
59	// If no explicit "$schema" is used, use the default metaschema associated with the draft used
60	if schema == "" {
61		if sl.Draft == Hybrid {
62			return nil
63		}
64		schema = drafts.GetSchemaURL(sl.Draft)
65	}
66
67	//Disable validation when loading the metaschema to prevent an infinite recursive loop
68	sl.Validate = false
69
70	metaSchema, err := sl.Compile(NewReferenceLoader(schema))
71
72	if err != nil {
73		return err
74	}
75
76	sl.Validate = true
77
78	result := metaSchema.validateDocument(documentNode)
79
80	if !result.Valid() {
81		var res bytes.Buffer
82		for _, err := range result.Errors() {
83			res.WriteString(err.String())
84			res.WriteString("\n")
85		}
86		return errors.New(res.String())
87	}
88
89	return nil
90}
91
92// AddSchemas adds an arbritrary amount of schemas to the schema cache. As this function does not require
93// an explicit URL, every schema should contain an $id, so that it can be referenced by the main schema
94func (sl *SchemaLoader) AddSchemas(loaders ...JSONLoader) error {
95	emptyRef, _ := gojsonreference.NewJsonReference("")
96
97	for _, loader := range loaders {
98		doc, err := loader.LoadJSON()
99
100		if err != nil {
101			return err
102		}
103
104		if sl.Validate {
105			if err := sl.validateMetaschema(doc); err != nil {
106				return err
107			}
108		}
109
110		// Directly use the Recursive function, so that it get only added to the schema pool by $id
111		// and not by the ref of the document as it's empty
112		if err = sl.pool.parseReferences(doc, emptyRef, false); err != nil {
113			return err
114		}
115	}
116
117	return nil
118}
119
120//AddSchema adds a schema under the provided URL to the schema cache
121func (sl *SchemaLoader) AddSchema(url string, loader JSONLoader) error {
122
123	ref, err := gojsonreference.NewJsonReference(url)
124
125	if err != nil {
126		return err
127	}
128
129	doc, err := loader.LoadJSON()
130
131	if err != nil {
132		return err
133	}
134
135	if sl.Validate {
136		if err := sl.validateMetaschema(doc); err != nil {
137			return err
138		}
139	}
140
141	return sl.pool.parseReferences(doc, ref, true)
142}
143
144func (sl *SchemaLoader) Compile(rootSchema JSONLoader) (*Schema, error) {
145
146	ref, err := rootSchema.JsonReference()
147
148	if err != nil {
149		return nil, err
150	}
151
152	d := Schema{}
153	d.pool = sl.pool
154	d.pool.jsonLoaderFactory = rootSchema.LoaderFactory()
155	d.documentReference = ref
156	d.referencePool = newSchemaReferencePool()
157
158	var doc interface{}
159	if ref.String() != "" {
160		// Get document from schema pool
161		spd, err := d.pool.GetDocument(d.documentReference)
162		if err != nil {
163			return nil, err
164		}
165		doc = spd.Document
166	} else {
167		// Load JSON directly
168		doc, err = rootSchema.LoadJSON()
169		if err != nil {
170			return nil, err
171		}
172		// References need only be parsed if loading JSON directly
173		//  as pool.GetDocument already does this for us if loading by reference
174		err = sl.pool.parseReferences(doc, ref, true)
175		if err != nil {
176			return nil, err
177		}
178	}
179
180	if sl.Validate {
181		if err := sl.validateMetaschema(doc); err != nil {
182			return nil, err
183		}
184	}
185
186	draft := sl.Draft
187	if sl.AutoDetect {
188		_, detectedDraft, err := parseSchemaURL(doc)
189		if err != nil {
190			return nil, err
191		}
192		if detectedDraft != nil {
193			draft = *detectedDraft
194		}
195	}
196
197	err = d.parse(doc, draft)
198	if err != nil {
199		return nil, err
200	}
201
202	return &d, nil
203}
204