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