1package analysis
2
3import (
4	"fmt"
5
6	"github.com/go-openapi/spec"
7	"github.com/go-openapi/strfmt"
8)
9
10// SchemaOpts configures the schema analyzer
11type SchemaOpts struct {
12	Schema   *spec.Schema
13	Root     interface{}
14	BasePath string
15	_        struct{}
16}
17
18// Schema analysis, will classify the schema according to known
19// patterns.
20func Schema(opts SchemaOpts) (*AnalyzedSchema, error) {
21	if opts.Schema == nil {
22		return nil, fmt.Errorf("no schema to analyze")
23	}
24
25	a := &AnalyzedSchema{
26		schema:   opts.Schema,
27		root:     opts.Root,
28		basePath: opts.BasePath,
29	}
30
31	a.initializeFlags()
32	a.inferKnownType()
33	a.inferEnum()
34	a.inferBaseType()
35
36	if err := a.inferMap(); err != nil {
37		return nil, err
38	}
39	if err := a.inferArray(); err != nil {
40		return nil, err
41	}
42
43	if err := a.inferTuple(); err != nil {
44		// NOTE(fredbi): currently, inferTuple() never returns an error
45		return nil, err
46	}
47
48	if err := a.inferFromRef(); err != nil {
49		return nil, err
50	}
51
52	a.inferSimpleSchema()
53	return a, nil
54}
55
56// AnalyzedSchema indicates what the schema represents
57type AnalyzedSchema struct {
58	schema   *spec.Schema
59	root     interface{}
60	basePath string
61
62	hasProps           bool
63	hasAllOf           bool
64	hasItems           bool
65	hasAdditionalProps bool
66	hasAdditionalItems bool
67	hasRef             bool
68
69	IsKnownType      bool
70	IsSimpleSchema   bool
71	IsArray          bool
72	IsSimpleArray    bool
73	IsMap            bool
74	IsSimpleMap      bool
75	IsExtendedObject bool
76	IsTuple          bool
77	IsTupleWithExtra bool
78	IsBaseType       bool
79	IsEnum           bool
80}
81
82// Inherits copies value fields from other onto this schema
83func (a *AnalyzedSchema) inherits(other *AnalyzedSchema) {
84	if other == nil {
85		return
86	}
87	a.hasProps = other.hasProps
88	a.hasAllOf = other.hasAllOf
89	a.hasItems = other.hasItems
90	a.hasAdditionalItems = other.hasAdditionalItems
91	a.hasAdditionalProps = other.hasAdditionalProps
92	a.hasRef = other.hasRef
93
94	a.IsKnownType = other.IsKnownType
95	a.IsSimpleSchema = other.IsSimpleSchema
96	a.IsArray = other.IsArray
97	a.IsSimpleArray = other.IsSimpleArray
98	a.IsMap = other.IsMap
99	a.IsSimpleMap = other.IsSimpleMap
100	a.IsExtendedObject = other.IsExtendedObject
101	a.IsTuple = other.IsTuple
102	a.IsTupleWithExtra = other.IsTupleWithExtra
103	a.IsBaseType = other.IsBaseType
104	a.IsEnum = other.IsEnum
105}
106
107func (a *AnalyzedSchema) inferFromRef() error {
108	if a.hasRef {
109		sch := new(spec.Schema)
110		sch.Ref = a.schema.Ref
111		err := spec.ExpandSchema(sch, a.root, nil)
112		if err != nil {
113			return err
114		}
115		if sch != nil {
116			// NOTE(fredbi): currently the only cause for errors in
117			// unresolved ref. Since spec.ExpandSchema() expands the
118			// schema recursively, there is no chance to get there,
119			// until we add more causes for error in this schema analysis.
120			rsch, err := Schema(SchemaOpts{
121				Schema:   sch,
122				Root:     a.root,
123				BasePath: a.basePath,
124			})
125			if err != nil {
126				return err
127			}
128			a.inherits(rsch)
129		}
130	}
131	return nil
132}
133
134func (a *AnalyzedSchema) inferSimpleSchema() {
135	a.IsSimpleSchema = a.IsKnownType || a.IsSimpleArray || a.IsSimpleMap
136}
137
138func (a *AnalyzedSchema) inferKnownType() {
139	tpe := a.schema.Type
140	format := a.schema.Format
141	a.IsKnownType = tpe.Contains("boolean") ||
142		tpe.Contains("integer") ||
143		tpe.Contains("number") ||
144		tpe.Contains("string") ||
145		(format != "" && strfmt.Default.ContainsName(format)) ||
146		(a.isObjectType() && !a.hasProps && !a.hasAllOf && !a.hasAdditionalProps && !a.hasAdditionalItems)
147}
148
149func (a *AnalyzedSchema) inferMap() error {
150	if a.isObjectType() {
151		hasExtra := a.hasProps || a.hasAllOf
152		a.IsMap = a.hasAdditionalProps && !hasExtra
153		a.IsExtendedObject = a.hasAdditionalProps && hasExtra
154		if a.IsMap {
155			if a.schema.AdditionalProperties.Schema != nil {
156				msch, err := Schema(SchemaOpts{
157					Schema:   a.schema.AdditionalProperties.Schema,
158					Root:     a.root,
159					BasePath: a.basePath,
160				})
161				if err != nil {
162					return err
163				}
164				a.IsSimpleMap = msch.IsSimpleSchema
165			} else if a.schema.AdditionalProperties.Allows {
166				a.IsSimpleMap = true
167			}
168		}
169	}
170	return nil
171}
172
173func (a *AnalyzedSchema) inferArray() error {
174	// an array has Items defined as an object schema, otherwise we qualify this JSON array as a tuple
175	// (yes, even if the Items array contains only one element).
176	// arrays in JSON schema may be unrestricted (i.e no Items specified).
177	// Note that arrays in Swagger MUST have Items. Nonetheless, we analyze unrestricted arrays.
178	//
179	// NOTE: the spec package misses the distinction between:
180	// items: [] and items: {}, so we consider both arrays here.
181	a.IsArray = a.isArrayType() && (a.schema.Items == nil || a.schema.Items.Schemas == nil)
182	if a.IsArray && a.hasItems {
183		if a.schema.Items.Schema != nil {
184			itsch, err := Schema(SchemaOpts{
185				Schema:   a.schema.Items.Schema,
186				Root:     a.root,
187				BasePath: a.basePath,
188			})
189			if err != nil {
190				return err
191			}
192			a.IsSimpleArray = itsch.IsSimpleSchema
193		}
194	}
195	if a.IsArray && !a.hasItems {
196		a.IsSimpleArray = true
197	}
198	return nil
199}
200
201func (a *AnalyzedSchema) inferTuple() error {
202	tuple := a.hasItems && a.schema.Items.Schemas != nil
203	a.IsTuple = tuple && !a.hasAdditionalItems
204	a.IsTupleWithExtra = tuple && a.hasAdditionalItems
205	return nil
206}
207
208func (a *AnalyzedSchema) inferBaseType() {
209	if a.isObjectType() {
210		a.IsBaseType = a.schema.Discriminator != ""
211	}
212}
213
214func (a *AnalyzedSchema) inferEnum() {
215	a.IsEnum = len(a.schema.Enum) > 0
216}
217
218func (a *AnalyzedSchema) initializeFlags() {
219	a.hasProps = len(a.schema.Properties) > 0
220	a.hasAllOf = len(a.schema.AllOf) > 0
221	a.hasRef = a.schema.Ref.String() != ""
222
223	a.hasItems = a.schema.Items != nil &&
224		(a.schema.Items.Schema != nil || len(a.schema.Items.Schemas) > 0)
225
226	a.hasAdditionalProps = a.schema.AdditionalProperties != nil &&
227		(a.schema.AdditionalProperties != nil || a.schema.AdditionalProperties.Allows)
228
229	a.hasAdditionalItems = a.schema.AdditionalItems != nil &&
230		(a.schema.AdditionalItems.Schema != nil || a.schema.AdditionalItems.Allows)
231
232}
233
234func (a *AnalyzedSchema) isObjectType() bool {
235	return !a.hasRef && (a.schema.Type == nil || a.schema.Type.Contains("") || a.schema.Type.Contains("object"))
236}
237
238func (a *AnalyzedSchema) isArrayType() bool {
239	return !a.hasRef && (a.schema.Type != nil && a.schema.Type.Contains("array"))
240}
241