github.com/josephbuchma/goa@v1.2.0/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 or MediaType. 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 sets the contact or license URL. 333 func URL(url string) { 334 switch def := dslengine.CurrentDefinition().(type) { 335 case *design.ContactDefinition: 336 def.URL = url 337 case *design.LicenseDefinition: 338 def.URL = url 339 case *design.DocsDefinition: 340 def.URL = url 341 default: 342 dslengine.IncompatibleDSL() 343 } 344 } 345 346 // Consumes adds a MIME type to the list of MIME types the APIs supports when accepting requests. 347 // Consumes may also specify the path of the decoding package. 348 // The package must expose a DecoderFactory method that returns an object which implements 349 // goa.DecoderFactory. 350 func Consumes(args ...interface{}) { 351 if a, ok := apiDefinition(); ok { 352 if def := buildEncodingDefinition(false, args...); def != nil { 353 a.Consumes = append(a.Consumes, def) 354 } 355 } 356 } 357 358 // Produces adds a MIME type to the list of MIME types the APIs can encode responses with. 359 // Produces may also specify the path of the encoding package. 360 // The package must expose a EncoderFactory method that returns an object which implements 361 // goa.EncoderFactory. 362 func Produces(args ...interface{}) { 363 if a, ok := apiDefinition(); ok { 364 if def := buildEncodingDefinition(true, args...); def != nil { 365 a.Produces = append(a.Produces, def) 366 } 367 } 368 } 369 370 // buildEncodingDefinition builds up an encoding definition. 371 func buildEncodingDefinition(encoding bool, args ...interface{}) *design.EncodingDefinition { 372 var dsl func() 373 var ok bool 374 funcName := "Consumes" 375 if encoding { 376 funcName = "Produces" 377 } 378 if len(args) == 0 { 379 dslengine.ReportError("missing argument in call to %s", funcName) 380 return nil 381 } 382 if _, ok = args[0].(string); !ok { 383 dslengine.ReportError("first argument to %s must be a string (MIME type)", funcName) 384 return nil 385 } 386 last := len(args) 387 if dsl, ok = args[len(args)-1].(func()); ok { 388 last = len(args) - 1 389 } 390 mimeTypes := make([]string, last) 391 for i := 0; i < last; i++ { 392 var mimeType string 393 if mimeType, ok = args[i].(string); !ok { 394 dslengine.ReportError("argument #%d of %s must be a string (MIME type)", i, funcName) 395 return nil 396 } 397 mimeTypes[i] = mimeType 398 } 399 d := &design.EncodingDefinition{MIMETypes: mimeTypes, Encoder: encoding} 400 if dsl != nil { 401 dslengine.Execute(dsl, d) 402 } 403 return d 404 } 405 406 // Package sets the Go package path to the encoder or decoder. It must be used inside a 407 // Consumes or Produces DSL. 408 func Package(path string) { 409 if e, ok := encodingDefinition(); ok { 410 e.PackagePath = path 411 } 412 } 413 414 // Function sets the Go function name used to instantiate the encoder or decoder. Defaults to 415 // NewEncoder / NewDecoder. 416 func Function(fn string) { 417 if e, ok := encodingDefinition(); ok { 418 e.Function = fn 419 } 420 } 421 422 // ResponseTemplate defines a response template that action definitions can use to describe their 423 // responses. The template may specify the HTTP response status, header specification and body media 424 // type. The template consists of a name and an anonymous function. The function is called when an 425 // action uses the template to define a response. Response template functions accept string 426 // parameters they can use to define the response fields. Here is an example of a response template 427 // definition that uses a function with one argument corresponding to the name of the response body 428 // media type: 429 // 430 // ResponseTemplate(OK, func(mt string) { 431 // Status(200) // OK response uses status code 200 432 // Media(mt) // Media type name set by action definition 433 // Headers(func() { 434 // Header("X-Request-Id", func() { // X-Request-Id header contains a string 435 // Pattern("[0-9A-F]+") // Regexp used to validate the response header content 436 // }) 437 // Required("X-Request-Id") // Header is mandatory 438 // }) 439 // }) 440 // 441 // This template can the be used by actions to define the OK response as follows: 442 // 443 // Response(OK, "vnd.goa.example") 444 // 445 // goa comes with a set of predefined response templates (one per standard HTTP status code). The 446 // OK template is the only one that accepts an argument. It is used as shown in the example above to 447 // set the response media type. Other predefined templates do not use arguments. ResponseTemplate 448 // makes it possible to define additional response templates specific to the API. 449 func ResponseTemplate(name string, p interface{}) { 450 if a, ok := apiDefinition(); ok { 451 if a.Responses == nil { 452 a.Responses = make(map[string]*design.ResponseDefinition) 453 } 454 if a.ResponseTemplates == nil { 455 a.ResponseTemplates = make(map[string]*design.ResponseTemplateDefinition) 456 } 457 if _, ok := a.Responses[name]; ok { 458 dslengine.ReportError("multiple definitions for response template %s", name) 459 return 460 } 461 if _, ok := a.ResponseTemplates[name]; ok { 462 dslengine.ReportError("multiple definitions for response template %s", name) 463 return 464 } 465 setupResponseTemplate(a, name, p) 466 } 467 } 468 469 func setupResponseTemplate(a *design.APIDefinition, name string, p interface{}) { 470 if f, ok := p.(func()); ok { 471 r := &design.ResponseDefinition{Name: name} 472 if dslengine.Execute(f, r) { 473 a.Responses[name] = r 474 } 475 } else if tmpl, ok := p.(func(...string)); ok { 476 t := func(params ...string) *design.ResponseDefinition { 477 r := &design.ResponseDefinition{Name: name} 478 dslengine.Execute(func() { tmpl(params...) }, r) 479 return r 480 } 481 a.ResponseTemplates[name] = &design.ResponseTemplateDefinition{ 482 Name: name, 483 Template: t, 484 } 485 } else { 486 typ := reflect.TypeOf(p) 487 if kind := typ.Kind(); kind != reflect.Func { 488 dslengine.ReportError("dsl must be a function but got %s", kind) 489 return 490 } 491 492 num := typ.NumIn() 493 val := reflect.ValueOf(p) 494 t := func(params ...string) *design.ResponseDefinition { 495 if len(params) < num { 496 args := "1 argument" 497 if num > 0 { 498 args = fmt.Sprintf("%d arguments", num) 499 } 500 dslengine.ReportError("expected at least %s when invoking response template %s", args, name) 501 return nil 502 } 503 r := &design.ResponseDefinition{Name: name} 504 505 in := make([]reflect.Value, num) 506 for i := 0; i < num; i++ { 507 // type checking 508 if t := typ.In(i); t.Kind() != reflect.String { 509 dslengine.ReportError("ResponseTemplate parameters must be strings but type of parameter at position %d is %s", i, t) 510 return nil 511 } 512 // append input arguments 513 in[i] = reflect.ValueOf(params[i]) 514 } 515 dslengine.Execute(func() { val.Call(in) }, r) 516 return r 517 } 518 a.ResponseTemplates[name] = &design.ResponseTemplateDefinition{ 519 Name: name, 520 Template: t, 521 } 522 } 523 } 524 525 // Title sets the API title used by generated documentation, JSON Hyper-schema, code comments etc. 526 func Title(val string) { 527 if a, ok := apiDefinition(); ok { 528 a.Title = val 529 } 530 } 531 532 // Trait defines an API trait. A trait encapsulates arbitrary DSL that gets executed wherever the 533 // trait is called via the UseTrait function. 534 func Trait(name string, val ...func()) { 535 if a, ok := apiDefinition(); ok { 536 if len(val) < 1 { 537 dslengine.ReportError("missing trait DSL for %s", name) 538 return 539 } else if len(val) > 1 { 540 dslengine.ReportError("too many arguments given to Trait") 541 return 542 } 543 if _, ok := design.Design.Traits[name]; ok { 544 dslengine.ReportError("multiple definitions for trait %s%s", name, design.Design.Context()) 545 return 546 } 547 trait := &dslengine.TraitDefinition{Name: name, DSLFunc: val[0]} 548 if a.Traits == nil { 549 a.Traits = make(map[string]*dslengine.TraitDefinition) 550 } 551 a.Traits[name] = trait 552 } 553 } 554 555 // UseTrait executes the API trait with the given name. UseTrait can be used inside a Resource, 556 // Action, Type, MediaType or Attribute DSL. UseTrait takes a variable number 557 // of trait names. 558 func UseTrait(names ...string) { 559 var def dslengine.Definition 560 561 switch typedDef := dslengine.CurrentDefinition().(type) { 562 case *design.ResourceDefinition: 563 def = typedDef 564 case *design.ActionDefinition: 565 def = typedDef 566 case *design.AttributeDefinition: 567 def = typedDef 568 case *design.MediaTypeDefinition: 569 def = typedDef 570 default: 571 dslengine.IncompatibleDSL() 572 } 573 574 if def != nil { 575 for _, name := range names { 576 if trait, ok := design.Design.Traits[name]; ok { 577 dslengine.Execute(trait.DSLFunc, def) 578 } else { 579 dslengine.ReportError("unknown trait %s", name) 580 } 581 } 582 } 583 }