github.com/brycereitano/goa@v0.0.0-20170315073847-8ffa6c85e265/goagen/gen_schema/json_schema.go (about) 1 package genschema 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/url" 7 "reflect" 8 "sort" 9 "strconv" 10 11 "github.com/goadesign/goa/design" 12 ) 13 14 type ( 15 // JSONSchema represents an instance of a JSON schema. 16 // See http://json-schema.org/documentation.html 17 JSONSchema struct { 18 Schema string `json:"$schema,omitempty"` 19 // Core schema 20 ID string `json:"id,omitempty"` 21 Title string `json:"title,omitempty"` 22 Type JSONType `json:"type,omitempty"` 23 Items *JSONSchema `json:"items,omitempty"` 24 Properties map[string]*JSONSchema `json:"properties,omitempty"` 25 Definitions map[string]*JSONSchema `json:"definitions,omitempty"` 26 Description string `json:"description,omitempty"` 27 DefaultValue interface{} `json:"default,omitempty"` 28 Example interface{} `json:"example,omitempty"` 29 30 // Hyper schema 31 Media *JSONMedia `json:"media,omitempty"` 32 ReadOnly bool `json:"readOnly,omitempty"` 33 PathStart string `json:"pathStart,omitempty"` 34 Links []*JSONLink `json:"links,omitempty"` 35 Ref string `json:"$ref,omitempty"` 36 37 // Validation 38 Enum []interface{} `json:"enum,omitempty"` 39 Format string `json:"format,omitempty"` 40 Pattern string `json:"pattern,omitempty"` 41 Minimum *float64 `json:"minimum,omitempty"` 42 Maximum *float64 `json:"maximum,omitempty"` 43 MinLength *int `json:"minLength,omitempty"` 44 MaxLength *int `json:"maxLength,omitempty"` 45 Required []string `json:"required,omitempty"` 46 AdditionalProperties bool `json:"additionalProperties,omitempty"` 47 48 // Union 49 AnyOf []*JSONSchema `json:"anyOf,omitempty"` 50 } 51 52 // JSONType is the JSON type enum. 53 JSONType string 54 55 // JSONMedia represents a "media" field in a JSON hyper schema. 56 JSONMedia struct { 57 BinaryEncoding string `json:"binaryEncoding,omitempty"` 58 Type string `json:"type,omitempty"` 59 } 60 61 // JSONLink represents a "link" field in a JSON hyper schema. 62 JSONLink struct { 63 Title string `json:"title,omitempty"` 64 Description string `json:"description,omitempty"` 65 Rel string `json:"rel,omitempty"` 66 Href string `json:"href,omitempty"` 67 Method string `json:"method,omitempty"` 68 Schema *JSONSchema `json:"schema,omitempty"` 69 TargetSchema *JSONSchema `json:"targetSchema,omitempty"` 70 MediaType string `json:"mediaType,omitempty"` 71 EncType string `json:"encType,omitempty"` 72 } 73 ) 74 75 const ( 76 // JSONArray represents a JSON array. 77 JSONArray JSONType = "array" 78 // JSONBoolean represents a JSON boolean. 79 JSONBoolean = "boolean" 80 // JSONInteger represents a JSON number without a fraction or exponent part. 81 JSONInteger = "integer" 82 // JSONNumber represents any JSON number. Number includes integer. 83 JSONNumber = "number" 84 // JSONNull represents the JSON null value. 85 JSONNull = "null" 86 // JSONObject represents a JSON object. 87 JSONObject = "object" 88 // JSONString represents a JSON string. 89 JSONString = "string" 90 // JSONFile is an extension used by Swagger to represent a file download. 91 JSONFile = "file" 92 ) 93 94 // SchemaRef is the JSON Hyper-schema standard href. 95 const SchemaRef = "http://json-schema.org/draft-04/hyper-schema" 96 97 var ( 98 // Definitions contains the generated JSON schema definitions 99 Definitions map[string]*JSONSchema 100 ) 101 102 // Initialize the global variables 103 func init() { 104 Definitions = make(map[string]*JSONSchema) 105 } 106 107 // NewJSONSchema instantiates a new JSON schema. 108 func NewJSONSchema() *JSONSchema { 109 js := JSONSchema{ 110 Properties: make(map[string]*JSONSchema), 111 Definitions: make(map[string]*JSONSchema), 112 } 113 return &js 114 } 115 116 // JSON serializes the schema into JSON. 117 // It makes sure the "$schema" standard field is set if needed prior to delegating to the standard 118 // JSON marshaler. 119 func (s *JSONSchema) JSON() ([]byte, error) { 120 if s.Ref == "" { 121 s.Schema = SchemaRef 122 } 123 return json.Marshal(s) 124 } 125 126 // APISchema produces the API JSON hyper schema. 127 func APISchema(api *design.APIDefinition) *JSONSchema { 128 api.IterateResources(func(r *design.ResourceDefinition) error { 129 GenerateResourceDefinition(api, r) 130 return nil 131 }) 132 scheme := "http" 133 if len(api.Schemes) > 0 { 134 scheme = api.Schemes[0] 135 } 136 u := url.URL{Scheme: scheme, Host: api.Host} 137 href := u.String() 138 links := []*JSONLink{ 139 { 140 Href: href, 141 Rel: "self", 142 }, 143 { 144 Href: "/schema", 145 Method: "GET", 146 Rel: "self", 147 TargetSchema: &JSONSchema{ 148 Schema: SchemaRef, 149 AdditionalProperties: true, 150 }, 151 }, 152 } 153 s := JSONSchema{ 154 ID: fmt.Sprintf("%s/schema", href), 155 Title: api.Title, 156 Description: api.Description, 157 Type: JSONObject, 158 Definitions: Definitions, 159 Properties: propertiesFromDefs(Definitions, "#/definitions/"), 160 Links: links, 161 } 162 return &s 163 } 164 165 // GenerateResourceDefinition produces the JSON schema corresponding to the given API resource. 166 // It stores the results in cachedSchema. 167 func GenerateResourceDefinition(api *design.APIDefinition, r *design.ResourceDefinition) { 168 s := NewJSONSchema() 169 s.Description = r.Description 170 s.Type = JSONObject 171 s.Title = r.Name 172 Definitions[r.Name] = s 173 if mt, ok := api.MediaTypes[r.MediaType]; ok { 174 for _, v := range mt.Views { 175 buildMediaTypeSchema(api, mt, v.Name, s) 176 } 177 } 178 r.IterateActions(func(a *design.ActionDefinition) error { 179 var requestSchema *JSONSchema 180 if a.Payload != nil { 181 requestSchema = TypeSchema(api, a.Payload) 182 requestSchema.Description = a.Name + " payload" 183 } 184 if a.Params != nil { 185 params := design.DupAtt(a.Params) 186 // We don't want to keep the path params, these are defined inline in the href 187 for _, r := range a.Routes { 188 for _, p := range r.Params() { 189 delete(params.Type.ToObject(), p) 190 } 191 } 192 } 193 var targetSchema *JSONSchema 194 var identifier string 195 for _, resp := range a.Responses { 196 if mt, ok := api.MediaTypes[resp.MediaType]; ok { 197 if identifier == "" { 198 identifier = mt.Identifier 199 } else { 200 identifier = "" 201 } 202 if targetSchema == nil { 203 targetSchema = TypeSchema(api, mt) 204 } else if targetSchema.AnyOf == nil { 205 firstSchema := targetSchema 206 targetSchema = NewJSONSchema() 207 targetSchema.AnyOf = []*JSONSchema{firstSchema, TypeSchema(api, mt)} 208 } else { 209 targetSchema.AnyOf = append(targetSchema.AnyOf, TypeSchema(api, mt)) 210 } 211 } 212 } 213 for i, r := range a.Routes { 214 link := JSONLink{ 215 Title: a.Name, 216 Rel: a.Name, 217 Href: toSchemaHref(api, r), 218 Method: r.Verb, 219 Schema: requestSchema, 220 TargetSchema: targetSchema, 221 MediaType: identifier, 222 } 223 if i == 0 { 224 if ca := a.Parent.CanonicalAction(); ca != nil { 225 if ca.Name == a.Name { 226 link.Rel = "self" 227 } 228 } 229 } 230 s.Links = append(s.Links, &link) 231 } 232 return nil 233 }) 234 } 235 236 // MediaTypeRef produces the JSON reference to the media type definition with the given view. 237 func MediaTypeRef(api *design.APIDefinition, mt *design.MediaTypeDefinition, view string) string { 238 projected, _, err := mt.Project(view) 239 if err != nil { 240 panic(fmt.Sprintf("failed to project media type %#v: %s", mt.Identifier, err)) // bug 241 } 242 if _, ok := Definitions[projected.TypeName]; !ok { 243 GenerateMediaTypeDefinition(api, projected, "default") 244 } 245 ref := fmt.Sprintf("#/definitions/%s", projected.TypeName) 246 return ref 247 } 248 249 // TypeRef produces the JSON reference to the type definition. 250 func TypeRef(api *design.APIDefinition, ut *design.UserTypeDefinition) string { 251 if _, ok := Definitions[ut.TypeName]; !ok { 252 GenerateTypeDefinition(api, ut) 253 } 254 return fmt.Sprintf("#/definitions/%s", ut.TypeName) 255 } 256 257 // GenerateMediaTypeDefinition produces the JSON schema corresponding to the given media type and 258 // given view. 259 func GenerateMediaTypeDefinition(api *design.APIDefinition, mt *design.MediaTypeDefinition, view string) { 260 if _, ok := Definitions[mt.TypeName]; ok { 261 return 262 } 263 s := NewJSONSchema() 264 s.Title = fmt.Sprintf("Mediatype identifier: %s", mt.Identifier) 265 Definitions[mt.TypeName] = s 266 buildMediaTypeSchema(api, mt, view, s) 267 } 268 269 // GenerateTypeDefinition produces the JSON schema corresponding to the given type. 270 func GenerateTypeDefinition(api *design.APIDefinition, ut *design.UserTypeDefinition) { 271 if _, ok := Definitions[ut.TypeName]; ok { 272 return 273 } 274 s := NewJSONSchema() 275 s.Title = ut.TypeName 276 Definitions[ut.TypeName] = s 277 buildAttributeSchema(api, s, ut.AttributeDefinition) 278 } 279 280 // TypeSchema produces the JSON schema corresponding to the given data type. 281 func TypeSchema(api *design.APIDefinition, t design.DataType) *JSONSchema { 282 s := NewJSONSchema() 283 switch actual := t.(type) { 284 case design.Primitive: 285 if name := actual.Name(); name != "any" { 286 s.Type = JSONType(actual.Name()) 287 } 288 switch actual.Kind() { 289 case design.UUIDKind: 290 s.Format = "uuid" 291 case design.DateTimeKind: 292 s.Format = "date-time" 293 case design.NumberKind: 294 s.Format = "double" 295 case design.IntegerKind: 296 s.Format = "int64" 297 } 298 case *design.Array: 299 s.Type = JSONArray 300 s.Items = NewJSONSchema() 301 buildAttributeSchema(api, s.Items, actual.ElemType) 302 case design.Object: 303 s.Type = JSONObject 304 for n, at := range actual { 305 prop := NewJSONSchema() 306 buildAttributeSchema(api, prop, at) 307 s.Properties[n] = prop 308 } 309 case *design.Hash: 310 s.Type = JSONObject 311 s.AdditionalProperties = true 312 case *design.UserTypeDefinition: 313 s.Ref = TypeRef(api, actual) 314 case *design.MediaTypeDefinition: 315 // Use "default" view by default 316 s.Ref = MediaTypeRef(api, actual, design.DefaultView) 317 } 318 return s 319 } 320 321 type mergeItems []struct { 322 a, b interface{} 323 needed bool 324 } 325 326 func (s *JSONSchema) createMergeItems(other *JSONSchema) mergeItems { 327 return mergeItems{ 328 {&s.ID, other.ID, s.ID == ""}, 329 {&s.Type, other.Type, s.Type == ""}, 330 {&s.Ref, other.Ref, s.Ref == ""}, 331 {&s.Items, other.Items, s.Items == nil}, 332 {&s.DefaultValue, other.DefaultValue, s.DefaultValue == nil}, 333 {&s.Title, other.Title, s.Title == ""}, 334 {&s.Media, other.Media, s.Media == nil}, 335 {&s.ReadOnly, other.ReadOnly, s.ReadOnly == false}, 336 {&s.PathStart, other.PathStart, s.PathStart == ""}, 337 {&s.Enum, other.Enum, s.Enum == nil}, 338 {&s.Format, other.Format, s.Format == ""}, 339 {&s.Pattern, other.Pattern, s.Pattern == ""}, 340 {&s.AdditionalProperties, other.AdditionalProperties, s.AdditionalProperties == false}, 341 { 342 a: s.Minimum, b: other.Minimum, 343 needed: (s.Minimum == nil && s.Minimum != nil) || 344 (s.Minimum != nil && other.Minimum != nil && *s.Minimum > *other.Minimum), 345 }, 346 { 347 a: s.Maximum, b: other.Maximum, 348 needed: (s.Maximum == nil && other.Maximum != nil) || 349 (s.Maximum != nil && other.Maximum != nil && *s.Maximum < *other.Maximum), 350 }, 351 { 352 a: s.MinLength, b: other.MinLength, 353 needed: (s.MinLength == nil && other.MinLength != nil) || 354 (s.MinLength != nil && other.MinLength != nil && *s.MinLength > *other.MinLength), 355 }, 356 { 357 a: s.MaxLength, b: other.MaxLength, 358 needed: (s.MaxLength == nil && other.MaxLength != nil) || 359 (s.MaxLength != nil && other.MaxLength != nil && *s.MaxLength > *other.MaxLength), 360 }, 361 } 362 } 363 364 // Merge does a two level deep merge of other into s. 365 func (s *JSONSchema) Merge(other *JSONSchema) { 366 items := s.createMergeItems(other) 367 for _, v := range items { 368 if v.needed && v.b != nil { 369 reflect.Indirect(reflect.ValueOf(v.a)).Set(reflect.ValueOf(v.b)) 370 } 371 } 372 373 for n, p := range other.Properties { 374 if _, ok := s.Properties[n]; !ok { 375 if s.Properties == nil { 376 s.Properties = make(map[string]*JSONSchema) 377 } 378 s.Properties[n] = p 379 } 380 } 381 382 for n, d := range other.Definitions { 383 if _, ok := s.Definitions[n]; !ok { 384 s.Definitions[n] = d 385 } 386 } 387 388 for _, l := range other.Links { 389 s.Links = append(s.Links, l) 390 } 391 392 for _, r := range other.Required { 393 s.Required = append(s.Required, r) 394 } 395 } 396 397 // Dup creates a shallow clone of the given schema. 398 func (s *JSONSchema) Dup() *JSONSchema { 399 js := JSONSchema{ 400 ID: s.ID, 401 Description: s.Description, 402 Schema: s.Schema, 403 Type: s.Type, 404 DefaultValue: s.DefaultValue, 405 Title: s.Title, 406 Media: s.Media, 407 ReadOnly: s.ReadOnly, 408 PathStart: s.PathStart, 409 Links: s.Links, 410 Ref: s.Ref, 411 Enum: s.Enum, 412 Format: s.Format, 413 Pattern: s.Pattern, 414 Minimum: s.Minimum, 415 Maximum: s.Maximum, 416 MinLength: s.MinLength, 417 MaxLength: s.MaxLength, 418 Required: s.Required, 419 AdditionalProperties: s.AdditionalProperties, 420 } 421 for n, p := range s.Properties { 422 js.Properties[n] = p.Dup() 423 } 424 if s.Items != nil { 425 js.Items = s.Items.Dup() 426 } 427 for n, d := range s.Definitions { 428 js.Definitions[n] = d.Dup() 429 } 430 return &js 431 } 432 433 // buildAttributeSchema initializes the given JSON schema that corresponds to the given attribute. 434 func buildAttributeSchema(api *design.APIDefinition, s *JSONSchema, at *design.AttributeDefinition) *JSONSchema { 435 if at.View != "" { 436 inner := NewJSONSchema() 437 inner.Ref = MediaTypeRef(api, at.Type.(*design.MediaTypeDefinition), at.View) 438 s.Merge(inner) 439 return s 440 } 441 s.Merge(TypeSchema(api, at.Type)) 442 if s.Ref != "" { 443 // Ref is exclusive with other fields 444 return s 445 } 446 s.DefaultValue = toStringMap(at.DefaultValue) 447 s.Description = at.Description 448 s.Example = at.GenerateExample(api.RandomGenerator(), nil) 449 val := at.Validation 450 if val == nil { 451 return s 452 } 453 s.Enum = val.Values 454 s.Format = val.Format 455 s.Pattern = val.Pattern 456 if val.Minimum != nil { 457 s.Minimum = val.Minimum 458 } 459 if val.Maximum != nil { 460 s.Maximum = val.Maximum 461 } 462 if val.MinLength != nil { 463 s.MinLength = val.MinLength 464 } 465 if val.MaxLength != nil { 466 s.MaxLength = val.MaxLength 467 } 468 s.Required = val.Required 469 return s 470 } 471 472 // toStringMap converts map[interface{}]interface{} to a map[string]interface{} when possible. 473 func toStringMap(val interface{}) interface{} { 474 switch actual := val.(type) { 475 case map[interface{}]interface{}: 476 m := make(map[string]interface{}) 477 for k, v := range actual { 478 m[toString(k)] = toStringMap(v) 479 } 480 return m 481 case []interface{}: 482 mapSlice := make([]interface{}, len(actual)) 483 for i, e := range actual { 484 mapSlice[i] = toStringMap(e) 485 } 486 return mapSlice 487 default: 488 return actual 489 } 490 } 491 492 // toString returns the string representation of the given type. 493 func toString(val interface{}) string { 494 switch actual := val.(type) { 495 case string: 496 return actual 497 case int: 498 return strconv.Itoa(actual) 499 case float64: 500 return strconv.FormatFloat(actual, 'f', -1, 64) 501 case bool: 502 return strconv.FormatBool(actual) 503 default: 504 panic("unexpected key type") 505 } 506 } 507 508 // toSchemaHref produces a href that replaces the path wildcards with JSON schema references when 509 // appropriate. 510 func toSchemaHref(api *design.APIDefinition, r *design.RouteDefinition) string { 511 params := r.Params() 512 args := make([]interface{}, len(params)) 513 for i, p := range params { 514 args[i] = fmt.Sprintf("/{%s}", p) 515 } 516 tmpl := design.WildcardRegex.ReplaceAllLiteralString(r.FullPath(), "%s") 517 return fmt.Sprintf(tmpl, args...) 518 } 519 520 // propertiesFromDefs creates a Properties map referencing the given definitions under the given 521 // path. 522 func propertiesFromDefs(definitions map[string]*JSONSchema, path string) map[string]*JSONSchema { 523 res := make(map[string]*JSONSchema, len(definitions)) 524 for n := range definitions { 525 if n == "identity" { 526 continue 527 } 528 s := NewJSONSchema() 529 s.Ref = path + n 530 res[n] = s 531 } 532 return res 533 } 534 535 // buildMediaTypeSchema initializes s as the JSON schema representing mt for the given view. 536 func buildMediaTypeSchema(api *design.APIDefinition, mt *design.MediaTypeDefinition, view string, s *JSONSchema) { 537 s.Media = &JSONMedia{Type: mt.Identifier} 538 projected, linksUT, err := mt.Project(view) 539 if err != nil { 540 panic(fmt.Sprintf("failed to project media type %#v: %s", mt.Identifier, err)) // bug 541 } 542 if linksUT != nil { 543 links := linksUT.Type.ToObject() 544 lnames := make([]string, len(links)) 545 i := 0 546 for n := range links { 547 lnames[i] = n 548 i++ 549 } 550 sort.Strings(lnames) 551 for _, ln := range lnames { 552 var ( 553 att = links[ln] 554 lmt = att.Type.(*design.MediaTypeDefinition) 555 r = lmt.Resource 556 href string 557 ) 558 if r != nil { 559 href = toSchemaHref(api, r.CanonicalAction().Routes[0]) 560 } 561 sm := NewJSONSchema() 562 sm.Ref = MediaTypeRef(api, lmt, "default") 563 s.Links = append(s.Links, &JSONLink{ 564 Title: ln, 565 Rel: ln, 566 Description: att.Description, 567 Href: href, 568 Method: "GET", 569 TargetSchema: sm, 570 MediaType: lmt.Identifier, 571 }) 572 } 573 } 574 buildAttributeSchema(api, s, projected.AttributeDefinition) 575 }