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