github.com/brycereitano/goa@v0.0.0-20170315073847-8ffa6c85e265/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 } 166 return nil 167 } 168 169 func parseAttributeArgs(baseAttr *design.AttributeDefinition, args ...interface{}) (design.DataType, string, func()) { 170 var ( 171 dataType design.DataType 172 description string 173 dsl func() 174 ok bool 175 ) 176 177 parseDataType := func(expected string, index int) { 178 if name, ok2 := args[index].(string); ok2 { 179 // Lookup type by name 180 if dataType, ok = design.Design.Types[name]; !ok { 181 var mt *design.MediaTypeDefinition 182 if mt = design.Design.MediaTypeWithIdentifier(name); mt == nil { 183 dataType = design.String // not nil to avoid panics 184 dslengine.InvalidArgError(expected, args[index]) 185 } else { 186 dataType = mt 187 } 188 } 189 return 190 } 191 if dataType, ok = args[index].(design.DataType); !ok { 192 dslengine.InvalidArgError(expected, args[index]) 193 } 194 } 195 parseDescription := func(expected string, index int) { 196 if description, ok = args[index].(string); !ok { 197 dslengine.InvalidArgError(expected, args[index]) 198 } 199 } 200 parseDSL := func(index int, success, failure func()) { 201 if dsl, ok = args[index].(func()); ok { 202 success() 203 } else { 204 failure() 205 } 206 } 207 208 success := func() {} 209 210 switch len(args) { 211 case 0: 212 if baseAttr != nil { 213 dataType = baseAttr.Type 214 } else { 215 dataType = design.String 216 } 217 case 1: 218 success = func() { 219 if baseAttr != nil { 220 dataType = baseAttr.Type 221 } 222 } 223 parseDSL(0, success, func() { parseDataType("type, type name or func()", 0) }) 224 case 2: 225 parseDataType("type or type name", 0) 226 parseDSL(1, success, func() { parseDescription("string or func()", 1) }) 227 case 3: 228 parseDataType("type or type name", 0) 229 parseDescription("string", 1) 230 parseDSL(2, success, func() { dslengine.InvalidArgError("func()", args[2]) }) 231 default: 232 dslengine.ReportError("too many arguments in call to Attribute") 233 } 234 235 return dataType, description, dsl 236 } 237 238 // Header is an alias of Attribute for the most part. 239 // 240 // Within an APIKeySecurity or JWTSecurity definition, Header 241 // defines that an implementation must check the given header to get 242 // the API Key. In this case, no `args` parameter is necessary. 243 func Header(name string, args ...interface{}) { 244 if _, ok := dslengine.CurrentDefinition().(*design.SecuritySchemeDefinition); ok { 245 if len(args) != 0 { 246 dslengine.ReportError("do not specify args") 247 return 248 } 249 inHeader(name) 250 return 251 } 252 253 Attribute(name, args...) 254 } 255 256 // Member is an alias of Attribute. 257 func Member(name string, args ...interface{}) { 258 Attribute(name, args...) 259 } 260 261 // Param is an alias of Attribute. 262 func Param(name string, args ...interface{}) { 263 Attribute(name, args...) 264 } 265 266 // Default sets the default value for an attribute. 267 // See http://json-schema.org/latest/json-schema-validation.html#anchor10. 268 func Default(def interface{}) { 269 if a, ok := attributeDefinition(); ok { 270 if a.Type != nil { 271 if !a.Type.CanHaveDefault() { 272 dslengine.ReportError("%s type cannot have a default value", qualifiedTypeName(a.Type)) 273 } else if !a.Type.IsCompatible(def) { 274 dslengine.ReportError("default value %#v is incompatible with attribute of type %s", 275 def, qualifiedTypeName(a.Type)) 276 } else { 277 a.SetDefault(def) 278 } 279 } else { 280 a.SetDefault(def) 281 } 282 } 283 } 284 285 // Example sets the example of an attribute to be used for the documentation: 286 // 287 // Attributes(func() { 288 // Attribute("ID", Integer, func() { 289 // Example(1) 290 // }) 291 // Attribute("name", String, func() { 292 // Example("Cabernet Sauvignon") 293 // }) 294 // Attribute("price", String) //If no Example() is provided, goa generates one that fits your specification 295 // }) 296 // 297 // If you do not want an auto-generated example for an attribute, add NoExample() to it. 298 func Example(exp interface{}) { 299 if a, ok := attributeDefinition(); ok { 300 if pass := a.SetExample(exp); !pass { 301 dslengine.ReportError("example value %#v is incompatible with attribute of type %s", 302 exp, a.Type.Name()) 303 } 304 } 305 } 306 307 // NoExample sets the example of an attribute to be blank for the documentation. It is used when 308 // users don't want any custom or auto-generated example 309 func NoExample() { 310 switch def := dslengine.CurrentDefinition().(type) { 311 case *design.APIDefinition: 312 def.NoExamples = true 313 case *design.AttributeDefinition: 314 def.SetExample(nil) 315 default: 316 dslengine.IncompatibleDSL() 317 } 318 } 319 320 // Enum adds a "enum" validation to the attribute. 321 // See http://json-schema.org/latest/json-schema-validation.html#anchor76. 322 func Enum(val ...interface{}) { 323 if a, ok := attributeDefinition(); ok { 324 ok := true 325 for i, v := range val { 326 // When can a.Type be nil? glad you asked 327 // There are two ways to write an Attribute declaration with the DSL that 328 // don't set the type: with one argument - just the name - in which case the type 329 // is set to String or with two arguments - the name and DSL. In this latter form 330 // the type can end up being either String - if the DSL does not define any 331 // attribute - or object if it does. 332 // Why allowing this? because it's not always possible to specify the type of an 333 // object - an object may just be declared inline to represent a substructure. 334 // OK then why not assuming object and not allowing for string? because the DSL 335 // where there's only one argument and the type is string implicitly is very 336 // useful and common, for example to list attributes that refer to other attributes 337 // such as responses that refer to responses defined at the API level or links that 338 // refer to the media type attributes. So if the form that takes a DSL always ended 339 // up defining an object we'd have a weird situation where one arg is string and 340 // two args is object. Breaks the least surprise principle. Soooo long story 341 // short the lesser evil seems to be to allow the ambiguity. Also tests like the 342 // one below are really a convenience to the user and not a fundamental feature 343 // - not checking in the case the type is not known yet is OK. 344 if a.Type != nil && !a.Type.IsCompatible(v) { 345 dslengine.ReportError("value %#v at index %d is incompatible with attribute of type %s", 346 v, i, a.Type.Name()) 347 ok = false 348 } 349 } 350 if ok { 351 a.AddValues(val) 352 } 353 } 354 } 355 356 // SupportedValidationFormats lists the supported formats for use with the 357 // Format DSL. 358 var SupportedValidationFormats = []string{ 359 "cidr", 360 "date-time", 361 "email", 362 "hostname", 363 "ipv4", 364 "ipv6", 365 "ip", 366 "mac", 367 "regexp", 368 "uri", 369 } 370 371 // Format adds a "format" validation to the attribute. 372 // See http://json-schema.org/latest/json-schema-validation.html#anchor104. 373 // The formats supported by goa are: 374 // 375 // "date-time": RFC3339 date time 376 // 377 // "email": RFC5322 email address 378 // 379 // "hostname": RFC1035 internet host name 380 // 381 // "ipv4", "ipv6", "ip": RFC2373 IPv4, IPv6 address or either 382 // 383 // "uri": RFC3986 URI 384 // 385 // "mac": IEEE 802 MAC-48, EUI-48 or EUI-64 MAC address 386 // 387 // "cidr": RFC4632 or RFC4291 CIDR notation IP address 388 // 389 // "regexp": RE2 regular expression 390 func Format(f string) { 391 if a, ok := attributeDefinition(); ok { 392 if a.Type != nil && a.Type.Kind() != design.StringKind { 393 incompatibleAttributeType("format", a.Type.Name(), "a string") 394 } else { 395 supported := false 396 for _, s := range SupportedValidationFormats { 397 if s == f { 398 supported = true 399 break 400 } 401 } 402 if !supported { 403 dslengine.ReportError("unsupported format %#v, supported formats are: %s", 404 f, strings.Join(SupportedValidationFormats, ", ")) 405 } else { 406 if a.Validation == nil { 407 a.Validation = &dslengine.ValidationDefinition{} 408 } 409 a.Validation.Format = f 410 } 411 } 412 } 413 } 414 415 // Pattern adds a "pattern" validation to the attribute. 416 // See http://json-schema.org/latest/json-schema-validation.html#anchor33. 417 func Pattern(p string) { 418 if a, ok := attributeDefinition(); ok { 419 if a.Type != nil && a.Type.Kind() != design.StringKind { 420 incompatibleAttributeType("pattern", a.Type.Name(), "a string") 421 } else { 422 _, err := regexp.Compile(p) 423 if err != nil { 424 dslengine.ReportError("invalid pattern %#v, %s", p, err) 425 } else { 426 if a.Validation == nil { 427 a.Validation = &dslengine.ValidationDefinition{} 428 } 429 a.Validation.Pattern = p 430 } 431 } 432 } 433 } 434 435 // Minimum adds a "minimum" validation to the attribute. 436 // See http://json-schema.org/latest/json-schema-validation.html#anchor21. 437 func Minimum(val interface{}) { 438 if a, ok := attributeDefinition(); ok { 439 if a.Type != nil && a.Type.Kind() != design.IntegerKind && a.Type.Kind() != design.NumberKind { 440 incompatibleAttributeType("minimum", a.Type.Name(), "an integer or a number") 441 } else { 442 var f float64 443 switch v := val.(type) { 444 case float32, float64, int, int8, int16, int32, int64, uint8, uint16, uint32, uint64: 445 f = reflect.ValueOf(v).Convert(reflect.TypeOf(float64(0.0))).Float() 446 case string: 447 var err error 448 f, err = strconv.ParseFloat(v, 64) 449 if err != nil { 450 dslengine.ReportError("invalid number value %#v", v) 451 return 452 } 453 default: 454 dslengine.ReportError("invalid number value %#v", v) 455 return 456 } 457 if a.Validation == nil { 458 a.Validation = &dslengine.ValidationDefinition{} 459 } 460 a.Validation.Minimum = &f 461 } 462 } 463 } 464 465 // Maximum adds a "maximum" validation to the attribute. 466 // See http://json-schema.org/latest/json-schema-validation.html#anchor17. 467 func Maximum(val interface{}) { 468 if a, ok := attributeDefinition(); ok { 469 if a.Type != nil && a.Type.Kind() != design.IntegerKind && a.Type.Kind() != design.NumberKind { 470 incompatibleAttributeType("maximum", a.Type.Name(), "an integer or a number") 471 } else { 472 var f float64 473 switch v := val.(type) { 474 case float32, float64, int, int8, int16, int32, int64, uint8, uint16, uint32, uint64: 475 f = reflect.ValueOf(v).Convert(reflect.TypeOf(float64(0.0))).Float() 476 case string: 477 var err error 478 f, err = strconv.ParseFloat(v, 64) 479 if err != nil { 480 dslengine.ReportError("invalid number value %#v", v) 481 return 482 } 483 default: 484 dslengine.ReportError("invalid number value %#v", v) 485 return 486 } 487 if a.Validation == nil { 488 a.Validation = &dslengine.ValidationDefinition{} 489 } 490 a.Validation.Maximum = &f 491 } 492 } 493 } 494 495 // MinLength adss a "minItems" validation to the attribute. 496 // See http://json-schema.org/latest/json-schema-validation.html#anchor45. 497 func MinLength(val int) { 498 if a, ok := attributeDefinition(); ok { 499 if a.Type != nil && a.Type.Kind() != design.StringKind && a.Type.Kind() != design.ArrayKind && a.Type.Kind() != design.HashKind { 500 incompatibleAttributeType("minimum length", a.Type.Name(), "a string or an array") 501 } else { 502 if a.Validation == nil { 503 a.Validation = &dslengine.ValidationDefinition{} 504 } 505 a.Validation.MinLength = &val 506 } 507 } 508 } 509 510 // MaxLength adss a "maxItems" validation to the attribute. 511 // See http://json-schema.org/latest/json-schema-validation.html#anchor42. 512 func MaxLength(val int) { 513 if a, ok := attributeDefinition(); ok { 514 if a.Type != nil && a.Type.Kind() != design.StringKind && a.Type.Kind() != design.ArrayKind { 515 incompatibleAttributeType("maximum length", a.Type.Name(), "a string or an array") 516 } else { 517 if a.Validation == nil { 518 a.Validation = &dslengine.ValidationDefinition{} 519 } 520 a.Validation.MaxLength = &val 521 } 522 } 523 } 524 525 // Required adds a "required" validation to the attribute. 526 // See http://json-schema.org/latest/json-schema-validation.html#anchor61. 527 func Required(names ...string) { 528 var at *design.AttributeDefinition 529 530 switch def := dslengine.CurrentDefinition().(type) { 531 case *design.AttributeDefinition: 532 at = def 533 case *design.MediaTypeDefinition: 534 at = def.AttributeDefinition 535 default: 536 dslengine.IncompatibleDSL() 537 return 538 } 539 540 if at.Type != nil && at.Type.Kind() != design.ObjectKind { 541 incompatibleAttributeType("required", at.Type.Name(), "an object") 542 } else { 543 if at.Validation == nil { 544 at.Validation = &dslengine.ValidationDefinition{} 545 } 546 at.Validation.AddRequired(names) 547 } 548 } 549 550 // incompatibleAttributeType reports an error for validations defined on 551 // incompatible attributes (e.g. max value on string). 552 func incompatibleAttributeType(validation, actual, expected string) { 553 dslengine.ReportError("invalid %s validation definition: attribute must be %s (but type is %s)", 554 validation, expected, actual) 555 } 556 557 // qualifiedTypeName returns the qualified type name for the given data type. 558 // This is useful in reporting types in error messages. 559 // (e.g) array<string>, hash<string, string>, hash<string, array<int>> 560 func qualifiedTypeName(t design.DataType) string { 561 switch t.Kind() { 562 case design.DateTimeKind: 563 return "datetime" 564 case design.ArrayKind: 565 return fmt.Sprintf("%s<%s>", t.Name(), qualifiedTypeName(t.ToArray().ElemType.Type)) 566 case design.HashKind: 567 h := t.ToHash() 568 return fmt.Sprintf("%s<%s, %s>", 569 t.Name(), 570 qualifiedTypeName(h.KeyType.Type), 571 qualifiedTypeName(h.ElemType.Type), 572 ) 573 } 574 return t.Name() 575 }