1// Copyright 2017 Google LLC. All Rights Reserved.
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 jsonschema
16
17import (
18	"fmt"
19	"log"
20	"strings"
21)
22
23//
24// OPERATIONS
25// The following methods perform operations on Schemas.
26//
27
28// IsEmpty returns true if no members of the Schema are specified.
29func (schema *Schema) IsEmpty() bool {
30	return (schema.Schema == nil) &&
31		(schema.ID == nil) &&
32		(schema.MultipleOf == nil) &&
33		(schema.Maximum == nil) &&
34		(schema.ExclusiveMaximum == nil) &&
35		(schema.Minimum == nil) &&
36		(schema.ExclusiveMinimum == nil) &&
37		(schema.MaxLength == nil) &&
38		(schema.MinLength == nil) &&
39		(schema.Pattern == nil) &&
40		(schema.AdditionalItems == nil) &&
41		(schema.Items == nil) &&
42		(schema.MaxItems == nil) &&
43		(schema.MinItems == nil) &&
44		(schema.UniqueItems == nil) &&
45		(schema.MaxProperties == nil) &&
46		(schema.MinProperties == nil) &&
47		(schema.Required == nil) &&
48		(schema.AdditionalProperties == nil) &&
49		(schema.Properties == nil) &&
50		(schema.PatternProperties == nil) &&
51		(schema.Dependencies == nil) &&
52		(schema.Enumeration == nil) &&
53		(schema.Type == nil) &&
54		(schema.AllOf == nil) &&
55		(schema.AnyOf == nil) &&
56		(schema.OneOf == nil) &&
57		(schema.Not == nil) &&
58		(schema.Definitions == nil) &&
59		(schema.Title == nil) &&
60		(schema.Description == nil) &&
61		(schema.Default == nil) &&
62		(schema.Format == nil) &&
63		(schema.Ref == nil)
64}
65
66// IsEqual returns true if two schemas are equal.
67func (schema *Schema) IsEqual(schema2 *Schema) bool {
68	return schema.String() == schema2.String()
69}
70
71// SchemaOperation represents a function that can be applied to a Schema.
72type SchemaOperation func(schema *Schema, context string)
73
74// Applies a specified function to a Schema and all of the Schemas that it contains.
75func (schema *Schema) applyToSchemas(operation SchemaOperation, context string) {
76
77	if schema.AdditionalItems != nil {
78		s := schema.AdditionalItems.Schema
79		if s != nil {
80			s.applyToSchemas(operation, "AdditionalItems")
81		}
82	}
83
84	if schema.Items != nil {
85		if schema.Items.SchemaArray != nil {
86			for _, s := range *(schema.Items.SchemaArray) {
87				s.applyToSchemas(operation, "Items.SchemaArray")
88			}
89		} else if schema.Items.Schema != nil {
90			schema.Items.Schema.applyToSchemas(operation, "Items.Schema")
91		}
92	}
93
94	if schema.AdditionalProperties != nil {
95		s := schema.AdditionalProperties.Schema
96		if s != nil {
97			s.applyToSchemas(operation, "AdditionalProperties")
98		}
99	}
100
101	if schema.Properties != nil {
102		for _, pair := range *(schema.Properties) {
103			s := pair.Value
104			s.applyToSchemas(operation, "Properties")
105		}
106	}
107	if schema.PatternProperties != nil {
108		for _, pair := range *(schema.PatternProperties) {
109			s := pair.Value
110			s.applyToSchemas(operation, "PatternProperties")
111		}
112	}
113
114	if schema.Dependencies != nil {
115		for _, pair := range *(schema.Dependencies) {
116			schemaOrStringArray := pair.Value
117			s := schemaOrStringArray.Schema
118			if s != nil {
119				s.applyToSchemas(operation, "Dependencies")
120			}
121		}
122	}
123
124	if schema.AllOf != nil {
125		for _, s := range *(schema.AllOf) {
126			s.applyToSchemas(operation, "AllOf")
127		}
128	}
129	if schema.AnyOf != nil {
130		for _, s := range *(schema.AnyOf) {
131			s.applyToSchemas(operation, "AnyOf")
132		}
133	}
134	if schema.OneOf != nil {
135		for _, s := range *(schema.OneOf) {
136			s.applyToSchemas(operation, "OneOf")
137		}
138	}
139	if schema.Not != nil {
140		schema.Not.applyToSchemas(operation, "Not")
141	}
142
143	if schema.Definitions != nil {
144		for _, pair := range *(schema.Definitions) {
145			s := pair.Value
146			s.applyToSchemas(operation, "Definitions")
147		}
148	}
149
150	operation(schema, context)
151}
152
153// CopyProperties copies all non-nil properties from the source Schema to the schema Schema.
154func (schema *Schema) CopyProperties(source *Schema) {
155	if source.Schema != nil {
156		schema.Schema = source.Schema
157	}
158	if source.ID != nil {
159		schema.ID = source.ID
160	}
161	if source.MultipleOf != nil {
162		schema.MultipleOf = source.MultipleOf
163	}
164	if source.Maximum != nil {
165		schema.Maximum = source.Maximum
166	}
167	if source.ExclusiveMaximum != nil {
168		schema.ExclusiveMaximum = source.ExclusiveMaximum
169	}
170	if source.Minimum != nil {
171		schema.Minimum = source.Minimum
172	}
173	if source.ExclusiveMinimum != nil {
174		schema.ExclusiveMinimum = source.ExclusiveMinimum
175	}
176	if source.MaxLength != nil {
177		schema.MaxLength = source.MaxLength
178	}
179	if source.MinLength != nil {
180		schema.MinLength = source.MinLength
181	}
182	if source.Pattern != nil {
183		schema.Pattern = source.Pattern
184	}
185	if source.AdditionalItems != nil {
186		schema.AdditionalItems = source.AdditionalItems
187	}
188	if source.Items != nil {
189		schema.Items = source.Items
190	}
191	if source.MaxItems != nil {
192		schema.MaxItems = source.MaxItems
193	}
194	if source.MinItems != nil {
195		schema.MinItems = source.MinItems
196	}
197	if source.UniqueItems != nil {
198		schema.UniqueItems = source.UniqueItems
199	}
200	if source.MaxProperties != nil {
201		schema.MaxProperties = source.MaxProperties
202	}
203	if source.MinProperties != nil {
204		schema.MinProperties = source.MinProperties
205	}
206	if source.Required != nil {
207		schema.Required = source.Required
208	}
209	if source.AdditionalProperties != nil {
210		schema.AdditionalProperties = source.AdditionalProperties
211	}
212	if source.Properties != nil {
213		schema.Properties = source.Properties
214	}
215	if source.PatternProperties != nil {
216		schema.PatternProperties = source.PatternProperties
217	}
218	if source.Dependencies != nil {
219		schema.Dependencies = source.Dependencies
220	}
221	if source.Enumeration != nil {
222		schema.Enumeration = source.Enumeration
223	}
224	if source.Type != nil {
225		schema.Type = source.Type
226	}
227	if source.AllOf != nil {
228		schema.AllOf = source.AllOf
229	}
230	if source.AnyOf != nil {
231		schema.AnyOf = source.AnyOf
232	}
233	if source.OneOf != nil {
234		schema.OneOf = source.OneOf
235	}
236	if source.Not != nil {
237		schema.Not = source.Not
238	}
239	if source.Definitions != nil {
240		schema.Definitions = source.Definitions
241	}
242	if source.Title != nil {
243		schema.Title = source.Title
244	}
245	if source.Description != nil {
246		schema.Description = source.Description
247	}
248	if source.Default != nil {
249		schema.Default = source.Default
250	}
251	if source.Format != nil {
252		schema.Format = source.Format
253	}
254	if source.Ref != nil {
255		schema.Ref = source.Ref
256	}
257}
258
259// TypeIs returns true if the Type of a Schema includes the specified type
260func (schema *Schema) TypeIs(typeName string) bool {
261	if schema.Type != nil {
262		// the schema Type is either a string or an array of strings
263		if schema.Type.String != nil {
264			return (*(schema.Type.String) == typeName)
265		} else if schema.Type.StringArray != nil {
266			for _, n := range *(schema.Type.StringArray) {
267				if n == typeName {
268					return true
269				}
270			}
271		}
272	}
273	return false
274}
275
276// ResolveRefs resolves "$ref" elements in a Schema and its children.
277// But if a reference refers to an object type, is inside a oneOf, or contains a oneOf,
278// the reference is kept and we expect downstream tools to separately model these
279// referenced schemas.
280func (schema *Schema) ResolveRefs() {
281	rootSchema := schema
282	count := 1
283	for count > 0 {
284		count = 0
285		schema.applyToSchemas(
286			func(schema *Schema, context string) {
287				if schema.Ref != nil {
288					resolvedRef, err := rootSchema.resolveJSONPointer(*(schema.Ref))
289					if err != nil {
290						log.Printf("%+v", err)
291					} else if resolvedRef.TypeIs("object") {
292						// don't substitute for objects, we'll model the referenced schema with a class
293					} else if context == "OneOf" {
294						// don't substitute for references inside oneOf declarations
295					} else if resolvedRef.OneOf != nil {
296						// don't substitute for references that contain oneOf declarations
297					} else if resolvedRef.AdditionalProperties != nil {
298						// don't substitute for references that look like objects
299					} else {
300						schema.Ref = nil
301						schema.CopyProperties(resolvedRef)
302						count++
303					}
304				}
305			}, "")
306	}
307}
308
309// resolveJSONPointer resolves JSON pointers.
310// This current implementation is very crude and custom for OpenAPI 2.0 schemas.
311// It panics for any pointer that it is unable to resolve.
312func (schema *Schema) resolveJSONPointer(ref string) (result *Schema, err error) {
313	parts := strings.Split(ref, "#")
314	if len(parts) == 2 {
315		documentName := parts[0] + "#"
316		if documentName == "#" && schema.ID != nil {
317			documentName = *(schema.ID)
318		}
319		path := parts[1]
320		document := schemas[documentName]
321		pathParts := strings.Split(path, "/")
322
323		// we currently do a very limited (hard-coded) resolution of certain paths and log errors for missed cases
324		if len(pathParts) == 1 {
325			return document, nil
326		} else if len(pathParts) == 3 {
327			switch pathParts[1] {
328			case "definitions":
329				dictionary := document.Definitions
330				for _, pair := range *dictionary {
331					if pair.Name == pathParts[2] {
332						result = pair.Value
333					}
334				}
335			case "properties":
336				dictionary := document.Properties
337				for _, pair := range *dictionary {
338					if pair.Name == pathParts[2] {
339						result = pair.Value
340					}
341				}
342			default:
343				break
344			}
345		}
346	}
347	if result == nil {
348		return nil, fmt.Errorf("unresolved pointer: %+v", ref)
349	}
350	return result, nil
351}
352
353// ResolveAllOfs replaces "allOf" elements by merging their properties into the parent Schema.
354func (schema *Schema) ResolveAllOfs() {
355	schema.applyToSchemas(
356		func(schema *Schema, context string) {
357			if schema.AllOf != nil {
358				for _, allOf := range *(schema.AllOf) {
359					schema.CopyProperties(allOf)
360				}
361				schema.AllOf = nil
362			}
363		}, "resolveAllOfs")
364}
365
366// ResolveAnyOfs replaces all "anyOf" elements with "oneOf".
367func (schema *Schema) ResolveAnyOfs() {
368	schema.applyToSchemas(
369		func(schema *Schema, context string) {
370			if schema.AnyOf != nil {
371				schema.OneOf = schema.AnyOf
372				schema.AnyOf = nil
373			}
374		}, "resolveAnyOfs")
375}
376
377// return a pointer to a copy of a passed-in string
378func stringptr(input string) (output *string) {
379	return &input
380}
381
382// CopyOfficialSchemaProperty copies a named property from the official JSON Schema definition
383func (schema *Schema) CopyOfficialSchemaProperty(name string) {
384	*schema.Properties = append(*schema.Properties,
385		NewNamedSchema(name,
386			&Schema{Ref: stringptr("http://json-schema.org/draft-04/schema#/properties/" + name)}))
387}
388
389// CopyOfficialSchemaProperties copies named properties from the official JSON Schema definition
390func (schema *Schema) CopyOfficialSchemaProperties(names []string) {
391	for _, name := range names {
392		schema.CopyOfficialSchemaProperty(name)
393	}
394}
395