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