github.com/NewMillennialGames/goa@v1.4.0/design/apidsl/attribute.go (about) 1 package apidsl 2 3 import ( 4 "fmt" 5 "reflect" 6 "regexp" 7 "strconv" 8 "strings" 9 10 "github.com/goadesign/goa/design" 11 "github.com/goadesign/goa/dslengine" 12 ) 13 14 // Attribute can be used in: View, Type, Attribute, Attributes 15 // 16 // Attribute implements the attribute definition DSL. An attribute describes a data structure 17 // recursively. Attributes are used for describing request headers, parameters and payloads - 18 // response bodies and headers - media types and types. An attribute definition is recursive: 19 // attributes may include other attributes. At the basic level an attribute has a name, 20 // a type and optionally a default value and validation rules. The type of an attribute can be one of: 21 // 22 // * The primitive types Boolean, Integer, Number, DateTime, UUID or String. 23 // 24 // * A type defined via the Type function. 25 // 26 // * A media type defined via the MediaType function. 27 // 28 // * An object described recursively with child attributes. 29 // 30 // * An array defined using the ArrayOf function. 31 // 32 // * An hashmap defined using the HashOf function. 33 // 34 // * The special type Any to indicate that the attribute may take any of the types listed above. 35 // 36 // Attributes can be defined using the Attribute, Param, Member or Header functions depending 37 // on where the definition appears. The syntax for all these DSL is the same. 38 // Here are some examples: 39 // 40 // Attribute("name") // Defines an attribute of type String 41 // 42 // Attribute("name", func() { 43 // Pattern("^foo") // Adds a validation rule to the attribute 44 // }) 45 // 46 // Attribute("name", Integer) // Defines an attribute of type Integer 47 // 48 // Attribute("name", Integer, func() { 49 // Default(42) // With a default value 50 // }) 51 // 52 // Attribute("name", Integer, "description") // Specifies a description 53 // 54 // Attribute("name", Integer, "description", func() { 55 // Enum(1, 2) // And validation rules 56 // }) 57 // 58 // Nested attributes: 59 // 60 // Attribute("nested", func() { 61 // Description("description") 62 // Attribute("child") 63 // Attribute("child2", func() { 64 // // .... 65 // }) 66 // Required("child") 67 // }) 68 // 69 // Here are all the valid usage of the Attribute function: 70 // 71 // Attribute(name string, dataType DataType, description string, dsl func()) 72 // 73 // Attribute(name string, dataType DataType, description string) 74 // 75 // Attribute(name string, dataType DataType, dsl func()) 76 // 77 // Attribute(name string, dataType DataType) 78 // 79 // Attribute(name string, dsl func()) // dataType is String or Object (if DSL defines child attributes) 80 // 81 // Attribute(name string) // dataType is String 82 func Attribute(name string, args ...interface{}) { 83 var parent *design.AttributeDefinition 84 85 switch def := dslengine.CurrentDefinition().(type) { 86 case *design.AttributeDefinition: 87 parent = def 88 case *design.MediaTypeDefinition: 89 parent = def.AttributeDefinition 90 case design.ContainerDefinition: 91 parent = def.Attribute() 92 case *design.APIDefinition: 93 if def.Params == nil { 94 def.Params = new(design.AttributeDefinition) 95 } 96 parent = def.Params 97 case *design.ResourceDefinition: 98 if def.Params == nil { 99 def.Params = new(design.AttributeDefinition) 100 } 101 parent = def.Params 102 default: 103 dslengine.IncompatibleDSL() 104 } 105 106 if parent != nil { 107 if parent.Type == nil { 108 parent.Type = make(design.Object) 109 } 110 if _, ok := parent.Type.(design.Object); !ok { 111 dslengine.ReportError("can't define child attributes on attribute of type %s", parent.Type.Name()) 112 return 113 } 114 115 baseAttr := attributeFromRef(name, parent.Reference) 116 dataType, description, dsl := parseAttributeArgs(baseAttr, args...) 117 if baseAttr != nil { 118 if description != "" { 119 baseAttr.Description = description 120 } 121 if dataType != nil { 122 baseAttr.Type = dataType 123 } 124 } else { 125 baseAttr = &design.AttributeDefinition{ 126 Type: dataType, 127 Description: description, 128 } 129 } 130 baseAttr.Reference = parent.Reference 131 if dsl != nil { 132 dslengine.Execute(dsl, baseAttr) 133 } 134 if baseAttr.Type == nil { 135 // DSL did not contain an "Attribute" declaration 136 baseAttr.Type = design.String 137 } 138 parent.Type.(design.Object)[name] = baseAttr 139 } 140 } 141 142 // attributeFromRef returns a base attribute given a reference data type. 143 // It takes care of running the DSL on the reference type if it hasn't run yet. 144 func attributeFromRef(name string, ref design.DataType) *design.AttributeDefinition { 145 if ref == nil { 146 return nil 147 } 148 switch t := ref.(type) { 149 case *design.UserTypeDefinition: 150 if t.DSLFunc != nil { 151 dsl := t.DSLFunc 152 t.DSLFunc = nil 153 dslengine.Execute(dsl, t.AttributeDefinition) 154 } 155 if att, ok := t.ToObject()[name]; ok { 156 return design.DupAtt(att) 157 } 158 case *design.MediaTypeDefinition: 159 if t.DSLFunc != nil { 160 dsl := t.DSLFunc 161 t.DSLFunc = nil 162 dslengine.Execute(dsl, t) 163 } 164 if att, ok := t.ToObject()[name]; ok { 165 return design.DupAtt(att) 166 } 167 case design.Object: 168 if att, ok := t[name]; ok { 169 return design.DupAtt(att) 170 } 171 } 172 return nil 173 } 174 175 func parseAttributeArgs(baseAttr *design.AttributeDefinition, args ...interface{}) (design.DataType, string, func()) { 176 var ( 177 dataType design.DataType 178 description string 179 dsl func() 180 ok bool 181 ) 182 183 parseDataType := func(expected string, index int) { 184 if name, ok2 := args[index].(string); ok2 { 185 // Lookup type by name 186 if dataType, ok = design.Design.Types[name]; !ok { 187 var mt *design.MediaTypeDefinition 188 if mt = design.Design.MediaTypeWithIdentifier(name); mt == nil { 189 dataType = design.String // not nil to avoid panics 190 dslengine.InvalidArgError(expected, args[index]) 191 } else { 192 dataType = mt 193 } 194 } 195 return 196 } 197 if dataType, ok = args[index].(design.DataType); !ok { 198 dslengine.InvalidArgError(expected, args[index]) 199 } 200 } 201 parseDescription := func(expected string, index int) { 202 if description, ok = args[index].(string); !ok { 203 dslengine.InvalidArgError(expected, args[index]) 204 } 205 } 206 parseDSL := func(index int, success, failure func()) { 207 if dsl, ok = args[index].(func()); ok { 208 success() 209 } else { 210 failure() 211 } 212 } 213 214 success := func() {} 215 216 switch len(args) { 217 case 0: 218 if baseAttr != nil { 219 dataType = baseAttr.Type 220 } else { 221 dataType = design.String 222 } 223 case 1: 224 success = func() { 225 if baseAttr != nil { 226 dataType = baseAttr.Type 227 } 228 } 229 parseDSL(0, success, func() { parseDataType("type, type name or func()", 0) }) 230 case 2: 231 parseDataType("type or type name", 0) 232 parseDSL(1, success, func() { parseDescription("string or func()", 1) }) 233 case 3: 234 parseDataType("type or type name", 0) 235 parseDescription("string", 1) 236 parseDSL(2, success, func() { dslengine.InvalidArgError("func()", args[2]) }) 237 default: 238 dslengine.ReportError("too many arguments in call to Attribute") 239 } 240 241 return dataType, description, dsl 242 } 243 244 // Header can be used in: Headers, APIKeySecurity, JWTSecurity 245 // 246 // Header is an alias of Attribute for the most part. 247 // 248 // Within an APIKeySecurity or JWTSecurity definition, Header 249 // defines that an implementation must check the given header to get 250 // the API Key. In this case, no `args` parameter is necessary. 251 func Header(name string, args ...interface{}) { 252 if _, ok := dslengine.CurrentDefinition().(*design.SecuritySchemeDefinition); ok { 253 if len(args) != 0 { 254 dslengine.ReportError("do not specify args") 255 return 256 } 257 inHeader(name) 258 return 259 } 260 261 Attribute(name, args...) 262 } 263 264 // Member can be used in: Payload 265 // 266 // Member is an alias of Attribute. 267 func Member(name string, args ...interface{}) { 268 Attribute(name, args...) 269 } 270 271 // Param can be used in: Params 272 // 273 // Param is an alias of Attribute. 274 func Param(name string, args ...interface{}) { 275 Attribute(name, args...) 276 } 277 278 // Default can be used in: Attribute 279 // 280 // Default sets the default value for an attribute. 281 // See http://json-schema.org/latest/json-schema-validation.html#anchor10. 282 func Default(def interface{}) { 283 if a, ok := attributeDefinition(); ok { 284 if a.Type != nil { 285 if !a.Type.CanHaveDefault() { 286 dslengine.ReportError("%s type cannot have a default value", qualifiedTypeName(a.Type)) 287 } else if !a.Type.IsCompatible(def) { 288 dslengine.ReportError("default value %#v is incompatible with attribute of type %s", 289 def, qualifiedTypeName(a.Type)) 290 } else { 291 a.SetDefault(def) 292 } 293 } else { 294 a.SetDefault(def) 295 } 296 } 297 } 298 299 // Example can be used in: Attribute, Header, Param, HashOf, ArrayOf 300 // 301 // Example sets the example of an attribute to be used for the documentation: 302 // 303 // Attributes(func() { 304 // Attribute("ID", Integer, func() { 305 // Example(1) 306 // }) 307 // Attribute("name", String, func() { 308 // Example("Cabernet Sauvignon") 309 // }) 310 // Attribute("price", String) //If no Example() is provided, goa generates one that fits your specification 311 // }) 312 // 313 // If you do not want an auto-generated example for an attribute, add NoExample() to it. 314 func Example(exp interface{}) { 315 if a, ok := attributeDefinition(); ok { 316 if pass := a.SetExample(exp); !pass { 317 dslengine.ReportError("example value %#v is incompatible with attribute of type %s", 318 exp, a.Type.Name()) 319 } 320 } 321 } 322 323 // ReadOnly can be used in: Attribute 324 // ReadOnly sets the readOnly property of an attribute to true. It is used when attributes are computed in the API and 325 // are not expected from the client 326 func ReadOnly() { 327 if a, ok := attributeDefinition(); ok { 328 a.SetReadOnly() 329 } 330 } 331 332 // NoExample can be used in: Attribute, Header, Param, HashOf, ArrayOf 333 // 334 // NoExample sets the example of an attribute to be blank for the documentation. It is used when 335 // users don't want any custom or auto-generated example 336 func NoExample() { 337 switch def := dslengine.CurrentDefinition().(type) { 338 case *design.APIDefinition: 339 def.NoExamples = true 340 case *design.AttributeDefinition: 341 def.SetExample(nil) 342 default: 343 dslengine.IncompatibleDSL() 344 } 345 } 346 347 // Enum can be used in: Attribute, Header, Param, HashOf, ArrayOf 348 // 349 // Enum adds a "enum" validation to the attribute. 350 // See http://json-schema.org/latest/json-schema-validation.html#anchor76. 351 func Enum(val ...interface{}) { 352 if a, ok := attributeDefinition(); ok { 353 ok := true 354 for i, v := range val { 355 // When can a.Type be nil? glad you asked 356 // There are two ways to write an Attribute declaration with the DSL that 357 // don't set the type: with one argument - just the name - in which case the type 358 // is set to String or with two arguments - the name and DSL. In this latter form 359 // the type can end up being either String - if the DSL does not define any 360 // attribute - or object if it does. 361 // Why allowing this? because it's not always possible to specify the type of an 362 // object - an object may just be declared inline to represent a substructure. 363 // OK then why not assuming object and not allowing for string? because the DSL 364 // where there's only one argument and the type is string implicitly is very 365 // useful and common, for example to list attributes that refer to other attributes 366 // such as responses that refer to responses defined at the API level or links that 367 // refer to the media type attributes. So if the form that takes a DSL always ended 368 // up defining an object we'd have a weird situation where one arg is string and 369 // two args is object. Breaks the least surprise principle. Soooo long story 370 // short the lesser evil seems to be to allow the ambiguity. Also tests like the 371 // one below are really a convenience to the user and not a fundamental feature 372 // - not checking in the case the type is not known yet is OK. 373 if a.Type != nil && !a.Type.IsCompatible(v) { 374 dslengine.ReportError("value %#v at index %d is incompatible with attribute of type %s", 375 v, i, a.Type.Name()) 376 ok = false 377 } 378 } 379 if ok { 380 a.AddValues(val) 381 } 382 } 383 } 384 385 // SupportedValidationFormats lists the supported formats for use with the 386 // Format DSL. 387 var SupportedValidationFormats = []string{ 388 "cidr", 389 "date", 390 "date-time", 391 "email", 392 "hostname", 393 "ipv4", 394 "ipv6", 395 "ip", 396 "mac", 397 "regexp", 398 "rfc1123", 399 "uri", 400 } 401 402 // Format can be used in: Attribute, Header, Param, HashOf, ArrayOf 403 // 404 // Format adds a "format" validation to the attribute. 405 // See http://json-schema.org/latest/json-schema-validation.html#anchor104. 406 // The formats supported by goa are: 407 // 408 // "date": RFC3339 date 409 // 410 // "date-time": RFC3339 date time 411 // 412 // "email": RFC5322 email address 413 // 414 // "hostname": RFC1035 internet host name 415 // 416 // "ipv4", "ipv6", "ip": RFC2373 IPv4, IPv6 address or either 417 // 418 // "uri": RFC3986 URI 419 // 420 // "mac": IEEE 802 MAC-48, EUI-48 or EUI-64 MAC address 421 // 422 // "cidr": RFC4632 or RFC4291 CIDR notation IP address 423 // 424 // "regexp": RE2 regular expression 425 // 426 // "rfc1123": RFC1123 date time 427 func Format(f string) { 428 if a, ok := attributeDefinition(); ok { 429 if a.Type != nil && a.Type.Kind() != design.StringKind { 430 incompatibleAttributeType("format", a.Type.Name(), "a string") 431 } else { 432 supported := false 433 for _, s := range SupportedValidationFormats { 434 if s == f { 435 supported = true 436 break 437 } 438 } 439 if !supported { 440 dslengine.ReportError("unsupported format %#v, supported formats are: %s", 441 f, strings.Join(SupportedValidationFormats, ", ")) 442 } else { 443 if a.Validation == nil { 444 a.Validation = &dslengine.ValidationDefinition{} 445 } 446 a.Validation.Format = f 447 } 448 } 449 } 450 } 451 452 // Pattern can be used in: Attribute, Header, Param, HashOf, ArrayOf 453 // 454 // Pattern adds a "pattern" validation to the attribute. 455 // See http://json-schema.org/latest/json-schema-validation.html#anchor33. 456 func Pattern(p string) { 457 if a, ok := attributeDefinition(); ok { 458 if a.Type != nil && a.Type.Kind() != design.StringKind { 459 incompatibleAttributeType("pattern", a.Type.Name(), "a string") 460 } else { 461 _, err := regexp.Compile(p) 462 if err != nil { 463 dslengine.ReportError("invalid pattern %#v, %s", p, err) 464 } else { 465 if a.Validation == nil { 466 a.Validation = &dslengine.ValidationDefinition{} 467 } 468 a.Validation.Pattern = p 469 } 470 } 471 } 472 } 473 474 // Minimum can be used in: Attribute, Header, Param, HashOf, ArrayOf 475 // 476 // Minimum adds a "minimum" validation to the attribute. 477 // See http://json-schema.org/latest/json-schema-validation.html#anchor21. 478 func Minimum(val interface{}) { 479 if a, ok := attributeDefinition(); ok { 480 if a.Type != nil && a.Type.Kind() != design.IntegerKind && a.Type.Kind() != design.NumberKind { 481 incompatibleAttributeType("minimum", a.Type.Name(), "an integer or a number") 482 } else { 483 var f float64 484 switch v := val.(type) { 485 case float32, float64, int, int8, int16, int32, int64, uint8, uint16, uint32, uint64: 486 f = reflect.ValueOf(v).Convert(reflect.TypeOf(float64(0.0))).Float() 487 case string: 488 var err error 489 f, err = strconv.ParseFloat(v, 64) 490 if err != nil { 491 dslengine.ReportError("invalid number value %#v", v) 492 return 493 } 494 default: 495 dslengine.ReportError("invalid number value %#v", v) 496 return 497 } 498 if a.Validation == nil { 499 a.Validation = &dslengine.ValidationDefinition{} 500 } 501 a.Validation.Minimum = &f 502 } 503 } 504 } 505 506 // Maximum can be used in: Attribute, Header, Param, HashOf, ArrayOf 507 // 508 // Maximum adds a "maximum" validation to the attribute. 509 // See http://json-schema.org/latest/json-schema-validation.html#anchor17. 510 func Maximum(val interface{}) { 511 if a, ok := attributeDefinition(); ok { 512 if a.Type != nil && a.Type.Kind() != design.IntegerKind && a.Type.Kind() != design.NumberKind { 513 incompatibleAttributeType("maximum", a.Type.Name(), "an integer or a number") 514 } else { 515 var f float64 516 switch v := val.(type) { 517 case float32, float64, int, int8, int16, int32, int64, uint8, uint16, uint32, uint64: 518 f = reflect.ValueOf(v).Convert(reflect.TypeOf(float64(0.0))).Float() 519 case string: 520 var err error 521 f, err = strconv.ParseFloat(v, 64) 522 if err != nil { 523 dslengine.ReportError("invalid number value %#v", v) 524 return 525 } 526 default: 527 dslengine.ReportError("invalid number value %#v", v) 528 return 529 } 530 if a.Validation == nil { 531 a.Validation = &dslengine.ValidationDefinition{} 532 } 533 a.Validation.Maximum = &f 534 } 535 } 536 } 537 538 // MinLength can be used in: Attribute, Header, Param, HashOf, ArrayOf 539 // 540 // MinLength adds a "minItems" validation to the attribute. 541 // See http://json-schema.org/latest/json-schema-validation.html#anchor45. 542 func MinLength(val int) { 543 if a, ok := attributeDefinition(); ok { 544 if a.Type != nil && a.Type.Kind() != design.StringKind && a.Type.Kind() != design.ArrayKind && a.Type.Kind() != design.HashKind { 545 incompatibleAttributeType("minimum length", a.Type.Name(), "a string or an array") 546 } else { 547 if a.Validation == nil { 548 a.Validation = &dslengine.ValidationDefinition{} 549 } 550 a.Validation.MinLength = &val 551 } 552 } 553 } 554 555 // MaxLength can be used in: Attribute, Header, Param, HashOf, ArrayOf 556 // 557 // MaxLength adds a "maxItems" validation to the attribute. 558 // See http://json-schema.org/latest/json-schema-validation.html#anchor42. 559 func MaxLength(val int) { 560 if a, ok := attributeDefinition(); ok { 561 if a.Type != nil && a.Type.Kind() != design.StringKind && a.Type.Kind() != design.ArrayKind { 562 incompatibleAttributeType("maximum length", a.Type.Name(), "a string or an array") 563 } else { 564 if a.Validation == nil { 565 a.Validation = &dslengine.ValidationDefinition{} 566 } 567 a.Validation.MaxLength = &val 568 } 569 } 570 } 571 572 // Required can be used in: Attributes, Headers, Payload, Type, Params 573 // 574 // Required adds a "required" validation to the attribute. 575 // See http://json-schema.org/latest/json-schema-validation.html#anchor61. 576 func Required(names ...string) { 577 var at *design.AttributeDefinition 578 579 switch def := dslengine.CurrentDefinition().(type) { 580 case *design.AttributeDefinition: 581 at = def 582 case *design.MediaTypeDefinition: 583 at = def.AttributeDefinition 584 default: 585 dslengine.IncompatibleDSL() 586 return 587 } 588 589 if at.Type != nil && at.Type.Kind() != design.ObjectKind { 590 incompatibleAttributeType("required", at.Type.Name(), "an object") 591 } else { 592 if at.Validation == nil { 593 at.Validation = &dslengine.ValidationDefinition{} 594 } 595 at.Validation.AddRequired(names) 596 } 597 } 598 599 // incompatibleAttributeType reports an error for validations defined on 600 // incompatible attributes (e.g. max value on string). 601 func incompatibleAttributeType(validation, actual, expected string) { 602 dslengine.ReportError("invalid %s validation definition: attribute must be %s (but type is %s)", 603 validation, expected, actual) 604 } 605 606 // qualifiedTypeName returns the qualified type name for the given data type. 607 // This is useful in reporting types in error messages. 608 // (e.g) array<string>, hash<string, string>, hash<string, array<int>> 609 func qualifiedTypeName(t design.DataType) string { 610 switch t.Kind() { 611 case design.DateTimeKind: 612 return "datetime" 613 case design.ArrayKind: 614 return fmt.Sprintf("%s<%s>", t.Name(), qualifiedTypeName(t.ToArray().ElemType.Type)) 615 case design.HashKind: 616 h := t.ToHash() 617 return fmt.Sprintf("%s<%s, %s>", 618 t.Name(), 619 qualifiedTypeName(h.KeyType.Type), 620 qualifiedTypeName(h.ElemType.Type), 621 ) 622 } 623 return t.Name() 624 }