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