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