github.com/ccrossley/goa@v1.3.1/design/apidsl/api.go (about) 1 package apidsl 2 3 import ( 4 "fmt" 5 "reflect" 6 "regexp" 7 "strings" 8 9 "github.com/goadesign/goa/design" 10 "github.com/goadesign/goa/dslengine" 11 ) 12 13 // API implements the top level API DSL. It defines the API name, default description and other 14 // default global property values. Here is an example showing all the possible API sub-definitions: 15 // 16 // API("API name", func() { 17 // Title("title") // API title used in documentation 18 // Description("description") // API description used in documentation 19 // Version("2.0") // API version being described 20 // TermsOfService("terms") 21 // Contact(func() { // API Contact information 22 // Name("contact name") 23 // Email("contact email") 24 // URL("contact URL") 25 // }) 26 // License(func() { // API Licensing information 27 // Name("license name") 28 // URL("license URL") 29 // }) 30 // Docs(func() { 31 // Description("doc description") 32 // URL("doc URL") 33 // }) 34 // Host("goa.design") // API hostname 35 // Scheme("http") 36 // BasePath("/base/:param") // Common base path to all API actions 37 // Params(func() { // Common parameters to all API actions 38 // Param("param") 39 // }) 40 // Security("JWT") 41 // Origin("http://swagger.goa.design", func() { // Define CORS policy, may be prefixed with "*" wildcard 42 // Headers("X-Shared-Secret") // One or more authorized headers, use "*" to authorize all 43 // Methods("GET", "POST") // One or more authorized HTTP methods 44 // Expose("X-Time") // One or more headers exposed to clients 45 // MaxAge(600) // How long to cache a prefligh request response 46 // Credentials() // Sets Access-Control-Allow-Credentials header 47 // }) 48 // Consumes("application/xml") // Built-in encoders and decoders 49 // Consumes("application/json") 50 // Produces("application/gob") 51 // Produces("application/json", func() { // Custom encoder 52 // Package("github.com/goadesign/goa/encoding/json") 53 // }) 54 // ResponseTemplate("static", func() { // Response template for use by actions 55 // Description("description") 56 // Status(404) 57 // MediaType("application/json") 58 // }) 59 // ResponseTemplate("dynamic", func(arg1, arg2 string) { 60 // Description(arg1) 61 // Status(200) 62 // MediaType(arg2) 63 // }) 64 // NoExample() // Prevent automatic generation of examples 65 // Trait("Authenticated", func() { // Traits define DSL that can be run anywhere 66 // Headers(func() { 67 // Header("header") 68 // Required("header") 69 // }) 70 // }) 71 // } 72 // 73 func API(name string, dsl func()) *design.APIDefinition { 74 if design.Design.Name != "" { 75 dslengine.ReportError("multiple API definitions, only one is allowed") 76 return nil 77 } 78 if !dslengine.IsTopLevelDefinition() { 79 dslengine.IncompatibleDSL() 80 return nil 81 } 82 83 if name == "" { 84 dslengine.ReportError("API name cannot be empty") 85 } 86 design.Design.Name = name 87 design.Design.DSLFunc = dsl 88 return design.Design 89 } 90 91 // Version specifies the API version. One design describes one version. 92 func Version(ver string) { 93 if api, ok := apiDefinition(); ok { 94 api.Version = ver 95 } 96 } 97 98 // Description sets the definition description. 99 // Description can be called inside API, Resource, Action, MediaType, Attribute, Response or ResponseTemplate 100 func Description(d string) { 101 switch def := dslengine.CurrentDefinition().(type) { 102 case *design.APIDefinition: 103 def.Description = d 104 case *design.ResourceDefinition: 105 def.Description = d 106 case *design.FileServerDefinition: 107 def.Description = d 108 case *design.ActionDefinition: 109 def.Description = d 110 case *design.MediaTypeDefinition: 111 def.Description = d 112 case *design.AttributeDefinition: 113 def.Description = d 114 case *design.ResponseDefinition: 115 def.Description = d 116 case *design.DocsDefinition: 117 def.Description = d 118 case *design.SecuritySchemeDefinition: 119 def.Description = d 120 default: 121 dslengine.IncompatibleDSL() 122 } 123 } 124 125 // BasePath defines the API base path, i.e. the common path prefix to all the API actions. 126 // The path may define wildcards (see Routing for a description of the wildcard syntax). 127 // The corresponding parameters must be described using Params. 128 func BasePath(val string) { 129 switch def := dslengine.CurrentDefinition().(type) { 130 case *design.APIDefinition: 131 def.BasePath = val 132 case *design.ResourceDefinition: 133 def.BasePath = val 134 if !strings.HasPrefix(val, "//") { 135 awcs := design.ExtractWildcards(design.Design.BasePath) 136 wcs := design.ExtractWildcards(val) 137 for _, awc := range awcs { 138 for _, wc := range wcs { 139 if awc == wc { 140 dslengine.ReportError(`duplicate wildcard "%s" in API and resource base paths`, wc) 141 } 142 } 143 } 144 } 145 default: 146 dslengine.IncompatibleDSL() 147 } 148 } 149 150 // Origin defines the CORS policy for a given origin. The origin can use a wildcard prefix 151 // such as "https://*.mydomain.com". The special value "*" defines the policy for all origins 152 // (in which case there should be only one Origin DSL in the parent resource). 153 // The origin can also be a regular expression wrapped into "/". 154 // Example: 155 // 156 // Origin("http://swagger.goa.design", func() { // Define CORS policy, may be prefixed with "*" wildcard 157 // Headers("X-Shared-Secret") // One or more authorized headers, use "*" to authorize all 158 // Methods("GET", "POST") // One or more authorized HTTP methods 159 // Expose("X-Time") // One or more headers exposed to clients 160 // MaxAge(600) // How long to cache a prefligh request response 161 // Credentials() // Sets Access-Control-Allow-Credentials header 162 // }) 163 // 164 // Origin("/(api|swagger)[.]goa[.]design/", func() {}) // Define CORS policy with a regular expression 165 func Origin(origin string, dsl func()) { 166 cors := &design.CORSDefinition{Origin: origin} 167 168 if strings.HasPrefix(origin, "/") && strings.HasSuffix(origin, "/") { 169 cors.Regexp = true 170 cors.Origin = strings.Trim(origin, "/") 171 } 172 173 if !dslengine.Execute(dsl, cors) { 174 return 175 } 176 var parent dslengine.Definition 177 switch def := dslengine.CurrentDefinition().(type) { 178 case *design.APIDefinition: 179 parent = def 180 if def.Origins == nil { 181 def.Origins = make(map[string]*design.CORSDefinition) 182 } 183 def.Origins[origin] = cors 184 case *design.ResourceDefinition: 185 parent = def 186 if def.Origins == nil { 187 def.Origins = make(map[string]*design.CORSDefinition) 188 } 189 def.Origins[origin] = cors 190 default: 191 dslengine.IncompatibleDSL() 192 return 193 } 194 cors.Parent = parent 195 } 196 197 // Methods sets the origin allowed methods. Used in Origin DSL. 198 func Methods(vals ...string) { 199 if cors, ok := corsDefinition(); ok { 200 cors.Methods = vals 201 } 202 } 203 204 // Expose sets the origin exposed headers. Used in Origin DSL. 205 func Expose(vals ...string) { 206 if cors, ok := corsDefinition(); ok { 207 cors.Exposed = vals 208 } 209 } 210 211 // MaxAge sets the cache expiry for preflight request responses. Used in Origin DSL. 212 func MaxAge(val uint) { 213 if cors, ok := corsDefinition(); ok { 214 cors.MaxAge = val 215 } 216 } 217 218 // Credentials sets the allow credentials response header. Used in Origin DSL. 219 func Credentials() { 220 if cors, ok := corsDefinition(); ok { 221 cors.Credentials = true 222 } 223 } 224 225 // TermsOfService describes the API terms of services or links to them. 226 func TermsOfService(terms string) { 227 if a, ok := apiDefinition(); ok { 228 a.TermsOfService = terms 229 } 230 } 231 232 // Regular expression used to validate RFC1035 hostnames*/ 233 var hostnameRegex = regexp.MustCompile(`^[[:alnum:]][[:alnum:]\-]{0,61}[[:alnum:]]|[[:alpha:]]$`) 234 235 // Host sets the API hostname. 236 func Host(host string) { 237 if !hostnameRegex.MatchString(host) { 238 dslengine.ReportError(`invalid hostname value "%s"`, host) 239 return 240 } 241 242 if a, ok := apiDefinition(); ok { 243 a.Host = host 244 } 245 } 246 247 // Scheme sets the API URL schemes. 248 func Scheme(vals ...string) { 249 ok := true 250 for _, v := range vals { 251 if v != "http" && v != "https" && v != "ws" && v != "wss" { 252 dslengine.ReportError(`invalid scheme "%s", must be one of "http", "https", "ws" or "wss"`, v) 253 ok = false 254 } 255 } 256 if !ok { 257 return 258 } 259 260 switch def := dslengine.CurrentDefinition().(type) { 261 case *design.APIDefinition: 262 def.Schemes = append(def.Schemes, vals...) 263 case *design.ResourceDefinition: 264 def.Schemes = append(def.Schemes, vals...) 265 case *design.ActionDefinition: 266 def.Schemes = append(def.Schemes, vals...) 267 default: 268 dslengine.IncompatibleDSL() 269 } 270 } 271 272 // Contact sets the API contact information. 273 func Contact(dsl func()) { 274 contact := new(design.ContactDefinition) 275 if !dslengine.Execute(dsl, contact) { 276 return 277 } 278 if a, ok := apiDefinition(); ok { 279 a.Contact = contact 280 } 281 } 282 283 // License sets the API license information. 284 func License(dsl func()) { 285 license := new(design.LicenseDefinition) 286 if !dslengine.Execute(dsl, license) { 287 return 288 } 289 if a, ok := apiDefinition(); ok { 290 a.License = license 291 } 292 } 293 294 // Docs provides external documentation pointers. 295 func Docs(dsl func()) { 296 docs := new(design.DocsDefinition) 297 if !dslengine.Execute(dsl, docs) { 298 return 299 } 300 301 switch def := dslengine.CurrentDefinition().(type) { 302 case *design.APIDefinition: 303 def.Docs = docs 304 case *design.ActionDefinition: 305 def.Docs = docs 306 case *design.FileServerDefinition: 307 def.Docs = docs 308 default: 309 dslengine.IncompatibleDSL() 310 } 311 } 312 313 // Name sets the contact or license name. 314 func Name(name string) { 315 switch def := dslengine.CurrentDefinition().(type) { 316 case *design.ContactDefinition: 317 def.Name = name 318 case *design.LicenseDefinition: 319 def.Name = name 320 default: 321 dslengine.IncompatibleDSL() 322 } 323 } 324 325 // Email sets the contact email. 326 func Email(email string) { 327 if c, ok := contactDefinition(); ok { 328 c.Email = email 329 } 330 } 331 332 // URL can be used in: Contact, License, Docs 333 // 334 // URL sets the contact, license, or Docs URL. 335 func URL(url string) { 336 switch def := dslengine.CurrentDefinition().(type) { 337 case *design.ContactDefinition: 338 def.URL = url 339 case *design.LicenseDefinition: 340 def.URL = url 341 case *design.DocsDefinition: 342 def.URL = url 343 default: 344 dslengine.IncompatibleDSL() 345 } 346 } 347 348 // Consumes adds a MIME type to the list of MIME types the APIs supports when accepting requests. 349 // Consumes may also specify the path of the decoding package. 350 // The package must expose a DecoderFactory method that returns an object which implements 351 // goa.DecoderFactory. 352 func Consumes(args ...interface{}) { 353 if a, ok := apiDefinition(); ok { 354 if def := buildEncodingDefinition(false, args...); def != nil { 355 a.Consumes = append(a.Consumes, def) 356 } 357 } 358 } 359 360 // Produces adds a MIME type to the list of MIME types the APIs can encode responses with. 361 // Produces may also specify the path of the encoding package. 362 // The package must expose a EncoderFactory method that returns an object which implements 363 // goa.EncoderFactory. 364 func Produces(args ...interface{}) { 365 if a, ok := apiDefinition(); ok { 366 if def := buildEncodingDefinition(true, args...); def != nil { 367 a.Produces = append(a.Produces, def) 368 } 369 } 370 } 371 372 // buildEncodingDefinition builds up an encoding definition. 373 func buildEncodingDefinition(encoding bool, args ...interface{}) *design.EncodingDefinition { 374 var dsl func() 375 var ok bool 376 funcName := "Consumes" 377 if encoding { 378 funcName = "Produces" 379 } 380 if len(args) == 0 { 381 dslengine.ReportError("missing argument in call to %s", funcName) 382 return nil 383 } 384 if _, ok = args[0].(string); !ok { 385 dslengine.ReportError("first argument to %s must be a string (MIME type)", funcName) 386 return nil 387 } 388 last := len(args) 389 if dsl, ok = args[len(args)-1].(func()); ok { 390 last = len(args) - 1 391 } 392 mimeTypes := make([]string, last) 393 for i := 0; i < last; i++ { 394 var mimeType string 395 if mimeType, ok = args[i].(string); !ok { 396 dslengine.ReportError("argument #%d of %s must be a string (MIME type)", i, funcName) 397 return nil 398 } 399 mimeTypes[i] = mimeType 400 } 401 d := &design.EncodingDefinition{MIMETypes: mimeTypes, Encoder: encoding} 402 if dsl != nil { 403 dslengine.Execute(dsl, d) 404 } 405 return d 406 } 407 408 // Package sets the Go package path to the encoder or decoder. It must be used inside a 409 // Consumes or Produces DSL. 410 func Package(path string) { 411 if e, ok := encodingDefinition(); ok { 412 e.PackagePath = path 413 } 414 } 415 416 // Function sets the Go function name used to instantiate the encoder or decoder. Defaults to 417 // NewEncoder / NewDecoder. 418 func Function(fn string) { 419 if e, ok := encodingDefinition(); ok { 420 e.Function = fn 421 } 422 } 423 424 // ResponseTemplate defines a response template that action definitions can use to describe their 425 // responses. The template may specify the HTTP response status, header specification and body media 426 // type. The template consists of a name and an anonymous function. The function is called when an 427 // action uses the template to define a response. Response template functions accept string 428 // parameters they can use to define the response fields. Here is an example of a response template 429 // definition that uses a function with one argument corresponding to the name of the response body 430 // media type: 431 // 432 // ResponseTemplate(OK, func(mt string) { 433 // Status(200) // OK response uses status code 200 434 // Media(mt) // Media type name set by action definition 435 // Headers(func() { 436 // Header("X-Request-Id", func() { // X-Request-Id header contains a string 437 // Pattern("[0-9A-F]+") // Regexp used to validate the response header content 438 // }) 439 // Required("X-Request-Id") // Header is mandatory 440 // }) 441 // }) 442 // 443 // This template can the be used by actions to define the OK response as follows: 444 // 445 // Response(OK, "vnd.goa.example") 446 // 447 // goa comes with a set of predefined response templates (one per standard HTTP status code). The 448 // OK template is the only one that accepts an argument. It is used as shown in the example above to 449 // set the response media type. Other predefined templates do not use arguments. ResponseTemplate 450 // makes it possible to define additional response templates specific to the API. 451 func ResponseTemplate(name string, p interface{}) { 452 if a, ok := apiDefinition(); ok { 453 if a.Responses == nil { 454 a.Responses = make(map[string]*design.ResponseDefinition) 455 } 456 if a.ResponseTemplates == nil { 457 a.ResponseTemplates = make(map[string]*design.ResponseTemplateDefinition) 458 } 459 if _, ok := a.Responses[name]; ok { 460 dslengine.ReportError("multiple definitions for response template %s", name) 461 return 462 } 463 if _, ok := a.ResponseTemplates[name]; ok { 464 dslengine.ReportError("multiple definitions for response template %s", name) 465 return 466 } 467 setupResponseTemplate(a, name, p) 468 } 469 } 470 471 func setupResponseTemplate(a *design.APIDefinition, name string, p interface{}) { 472 if f, ok := p.(func()); ok { 473 r := &design.ResponseDefinition{Name: name} 474 if dslengine.Execute(f, r) { 475 a.Responses[name] = r 476 } 477 } else if tmpl, ok := p.(func(...string)); ok { 478 t := func(params ...string) *design.ResponseDefinition { 479 r := &design.ResponseDefinition{Name: name} 480 dslengine.Execute(func() { tmpl(params...) }, r) 481 return r 482 } 483 a.ResponseTemplates[name] = &design.ResponseTemplateDefinition{ 484 Name: name, 485 Template: t, 486 } 487 } else { 488 typ := reflect.TypeOf(p) 489 if kind := typ.Kind(); kind != reflect.Func { 490 dslengine.ReportError("dsl must be a function but got %s", kind) 491 return 492 } 493 494 num := typ.NumIn() 495 val := reflect.ValueOf(p) 496 t := func(params ...string) *design.ResponseDefinition { 497 if len(params) < num { 498 args := "1 argument" 499 if num > 0 { 500 args = fmt.Sprintf("%d arguments", num) 501 } 502 dslengine.ReportError("expected at least %s when invoking response template %s", args, name) 503 return nil 504 } 505 r := &design.ResponseDefinition{Name: name} 506 507 in := make([]reflect.Value, num) 508 for i := 0; i < num; i++ { 509 // type checking 510 if t := typ.In(i); t.Kind() != reflect.String { 511 dslengine.ReportError("ResponseTemplate parameters must be strings but type of parameter at position %d is %s", i, t) 512 return nil 513 } 514 // append input arguments 515 in[i] = reflect.ValueOf(params[i]) 516 } 517 dslengine.Execute(func() { val.Call(in) }, r) 518 return r 519 } 520 a.ResponseTemplates[name] = &design.ResponseTemplateDefinition{ 521 Name: name, 522 Template: t, 523 } 524 } 525 } 526 527 // Title sets the API title used by generated documentation, JSON Hyper-schema, code comments etc. 528 func Title(val string) { 529 if a, ok := apiDefinition(); ok { 530 a.Title = val 531 } 532 } 533 534 // Trait defines an API trait. A trait encapsulates arbitrary DSL that gets executed wherever the 535 // trait is called via the UseTrait function. 536 func Trait(name string, val ...func()) { 537 if a, ok := apiDefinition(); ok { 538 if len(val) < 1 { 539 dslengine.ReportError("missing trait DSL for %s", name) 540 return 541 } else if len(val) > 1 { 542 dslengine.ReportError("too many arguments given to Trait") 543 return 544 } 545 if _, ok := design.Design.Traits[name]; ok { 546 dslengine.ReportError("multiple definitions for trait %s%s", name, design.Design.Context()) 547 return 548 } 549 trait := &dslengine.TraitDefinition{Name: name, DSLFunc: val[0]} 550 if a.Traits == nil { 551 a.Traits = make(map[string]*dslengine.TraitDefinition) 552 } 553 a.Traits[name] = trait 554 } 555 } 556 557 // UseTrait executes the API trait with the given name. UseTrait can be used inside a Resource, 558 // Action, Type, MediaType or Attribute DSL. UseTrait takes a variable number 559 // of trait names. 560 func UseTrait(names ...string) { 561 var def dslengine.Definition 562 563 switch typedDef := dslengine.CurrentDefinition().(type) { 564 case *design.ResourceDefinition: 565 def = typedDef 566 case *design.ActionDefinition: 567 def = typedDef 568 case *design.AttributeDefinition: 569 def = typedDef 570 case *design.MediaTypeDefinition: 571 def = typedDef 572 default: 573 dslengine.IncompatibleDSL() 574 } 575 576 if def != nil { 577 for _, name := range names { 578 if trait, ok := design.Design.Traits[name]; ok { 579 dslengine.Execute(trait.DSLFunc, def) 580 } else { 581 dslengine.ReportError("unknown trait %s", name) 582 } 583 } 584 } 585 }