github.com/goldeneggg/goa@v1.3.1/design/apidsl/action.go (about) 1 package apidsl 2 3 import ( 4 "fmt" 5 "unicode" 6 7 "github.com/goadesign/goa/design" 8 "github.com/goadesign/goa/dslengine" 9 ) 10 11 // Files defines an API endpoint that serves static assets. The logic for what to do when the 12 // filename points to a file vs. a directory is the same as the standard http package ServeFile 13 // function. The path may end with a wildcard that matches the rest of the URL (e.g. *filepath). If 14 // it does the matching path is appended to filename to form the full file path, so: 15 // 16 // Files("/index.html", "/www/data/index.html") 17 // 18 // Returns the content of the file "/www/data/index.html" when requests are sent to "/index.html" 19 // and: 20 // 21 // Files("/assets/*filepath", "/www/data/assets") 22 // 23 // returns the content of the file "/www/data/assets/x/y/z" when requests are sent to 24 // "/assets/x/y/z". 25 // The file path may be specified as a relative path to the current path of the process. 26 // Files support setting a description, security scheme and doc links via additional DSL: 27 // 28 // Files("/index.html", "/www/data/index.html", func() { 29 // Description("Serve home page") 30 // Docs(func() { 31 // Description("Download docs") 32 // URL("http//cellarapi.com/docs/actions/download") 33 // }) 34 // Security("oauth2", func() { 35 // Scope("api:read") 36 // }) 37 // }) 38 func Files(path, filename string, dsls ...func()) { 39 if r, ok := resourceDefinition(); ok { 40 server := &design.FileServerDefinition{ 41 Parent: r, 42 RequestPath: path, 43 FilePath: filename, 44 Metadata: make(dslengine.MetadataDefinition), 45 } 46 if len(dsls) > 0 { 47 if !dslengine.Execute(dsls[0], server) { 48 return 49 } 50 } 51 r.FileServers = append(r.FileServers, server) 52 } 53 } 54 55 // Action implements the action definition DSL. Action definitions describe specific API endpoints 56 // including the URL, HTTP method and request parameters (via path wildcards or query strings) and 57 // payload (data structure describing the request HTTP body). An action belongs to a resource and 58 // "inherits" default values from the resource definition including the URL path prefix, default 59 // response media type and default payload attribute properties (inherited from the attribute with 60 // identical name in the resource default media type). Action definitions also describe all the 61 // possible responses including the HTTP status, headers and body. Here is an example showing all 62 // the possible sub-definitions: 63 // Action("Update", func() { 64 // Description("Update account") 65 // Docs(func() { 66 // Description("Update docs") 67 // URL("http//cellarapi.com/docs/actions/update") 68 // }) 69 // Scheme("http") 70 // Routing( 71 // PUT("/:id"), // Action path is relative to parent resource base path 72 // PUT("//orgs/:org/accounts/:id"), // The // prefix indicates an absolute path 73 // ) 74 // Params(func() { // Params describe the action parameters 75 // Param("org", String) // Parameters may correspond to path wildcards 76 // Param("id", Integer) 77 // Param("sort", func() { // or URL query string values. 78 // Enum("asc", "desc") 79 // }) 80 // }) 81 // Security("oauth2", func() { // Security sets the security scheme used to secure requests 82 // Scope("api:read") 83 // Scope("api:write") 84 // }) 85 // Headers(func() { // Headers describe relevant action headers 86 // Header("Authorization", String) 87 // Header("X-Account", Integer) 88 // Required("Authorization", "X-Account") 89 // }) 90 // Payload(UpdatePayload) // Payload describes the HTTP request body 91 // // OptionalPayload(UpdatePayload) // OptionalPayload defines an HTTP request body which may be omitted 92 // Response(NoContent) // Each possible HTTP response is described via Response 93 // Response(NotFound) 94 // }) 95 func Action(name string, dsl func()) { 96 if r, ok := resourceDefinition(); ok { 97 if r.Actions == nil { 98 r.Actions = make(map[string]*design.ActionDefinition) 99 } 100 action, ok := r.Actions[name] 101 if !ok { 102 action = &design.ActionDefinition{ 103 Parent: r, 104 Name: name, 105 Metadata: make(dslengine.MetadataDefinition), 106 } 107 } 108 if !dslengine.Execute(dsl, action) { 109 return 110 } 111 r.Actions[name] = action 112 } 113 } 114 115 // Routing lists the action route. Each route is defined with a function named after the HTTP method. 116 // The route function takes the path as argument. Route paths may use wildcards as described in the 117 // [httptreemux](https://godoc.org/github.com/dimfeld/httptreemux) package documentation. These 118 // wildcards define parameters using the `:name` or `*name` syntax where `:name` matches a path 119 // segment and `*name` is a catch-all that matches the path until the end. 120 func Routing(routes ...*design.RouteDefinition) { 121 if a, ok := actionDefinition(); ok { 122 for _, r := range routes { 123 r.Parent = a 124 a.Routes = append(a.Routes, r) 125 } 126 } 127 } 128 129 // GET creates a route using the GET HTTP method. 130 func GET(path string, dsl ...func()) *design.RouteDefinition { 131 route := &design.RouteDefinition{Verb: "GET", Path: path} 132 if len(dsl) != 0 { 133 if !dslengine.Execute(dsl[0], route) { 134 return nil 135 } 136 } 137 return route 138 } 139 140 // HEAD creates a route using the HEAD HTTP method. 141 func HEAD(path string, dsl ...func()) *design.RouteDefinition { 142 route := &design.RouteDefinition{Verb: "HEAD", Path: path} 143 if len(dsl) != 0 { 144 if !dslengine.Execute(dsl[0], route) { 145 return nil 146 } 147 } 148 return route 149 } 150 151 // POST creates a route using the POST HTTP method. 152 func POST(path string, dsl ...func()) *design.RouteDefinition { 153 route := &design.RouteDefinition{Verb: "POST", Path: path} 154 if len(dsl) != 0 { 155 if !dslengine.Execute(dsl[0], route) { 156 return nil 157 } 158 } 159 return route 160 } 161 162 // PUT creates a route using the PUT HTTP method. 163 func PUT(path string, dsl ...func()) *design.RouteDefinition { 164 route := &design.RouteDefinition{Verb: "PUT", Path: path} 165 if len(dsl) != 0 { 166 if !dslengine.Execute(dsl[0], route) { 167 return nil 168 } 169 } 170 return route 171 } 172 173 // DELETE creates a route using the DELETE HTTP method. 174 func DELETE(path string, dsl ...func()) *design.RouteDefinition { 175 route := &design.RouteDefinition{Verb: "DELETE", Path: path} 176 if len(dsl) != 0 { 177 if !dslengine.Execute(dsl[0], route) { 178 return nil 179 } 180 } 181 return route 182 } 183 184 // OPTIONS creates a route using the OPTIONS HTTP method. 185 func OPTIONS(path string, dsl ...func()) *design.RouteDefinition { 186 route := &design.RouteDefinition{Verb: "OPTIONS", Path: path} 187 if len(dsl) != 0 { 188 if !dslengine.Execute(dsl[0], route) { 189 return nil 190 } 191 } 192 return route 193 } 194 195 // TRACE creates a route using the TRACE HTTP method. 196 func TRACE(path string, dsl ...func()) *design.RouteDefinition { 197 route := &design.RouteDefinition{Verb: "TRACE", Path: path} 198 if len(dsl) != 0 { 199 if !dslengine.Execute(dsl[0], route) { 200 return nil 201 } 202 } 203 return route 204 } 205 206 // CONNECT creates a route using the CONNECT HTTP method. 207 func CONNECT(path string, dsl ...func()) *design.RouteDefinition { 208 route := &design.RouteDefinition{Verb: "CONNECT", Path: path} 209 if len(dsl) != 0 { 210 if !dslengine.Execute(dsl[0], route) { 211 return nil 212 } 213 } 214 return route 215 } 216 217 // PATCH creates a route using the PATCH HTTP method. 218 func PATCH(path string, dsl ...func()) *design.RouteDefinition { 219 route := &design.RouteDefinition{Verb: "PATCH", Path: path} 220 if len(dsl) != 0 { 221 if !dslengine.Execute(dsl[0], route) { 222 return nil 223 } 224 } 225 return route 226 } 227 228 // Headers implements the DSL for describing HTTP headers. The DSL syntax is identical to the one 229 // of Attribute. Here is an example defining a couple of headers with validations: 230 // 231 // Headers(func() { 232 // Header("Authorization") 233 // Header("X-Account", Integer, func() { 234 // Minimum(1) 235 // }) 236 // Required("Authorization") 237 // }) 238 // 239 // Headers can be used inside Action to define the action request headers, Response to define the 240 // response headers or Resource to define common request headers to all the resource actions. 241 func Headers(params ...interface{}) { 242 if len(params) == 0 { 243 dslengine.ReportError("missing parameter") 244 return 245 } 246 dsl, ok := params[0].(func()) 247 if ok { 248 switch def := dslengine.CurrentDefinition().(type) { 249 case *design.ActionDefinition: 250 headers := newAttribute(def.Parent.MediaType) 251 if dslengine.Execute(dsl, headers) { 252 def.Headers = def.Headers.Merge(headers) 253 } 254 255 case *design.ResourceDefinition: 256 headers := newAttribute(def.MediaType) 257 if dslengine.Execute(dsl, headers) { 258 def.Headers = def.Headers.Merge(headers) 259 } 260 261 case *design.ResponseDefinition: 262 var h *design.AttributeDefinition 263 switch actual := def.Parent.(type) { 264 case *design.ResourceDefinition: 265 h = newAttribute(actual.MediaType) 266 case *design.ActionDefinition: 267 h = newAttribute(actual.Parent.MediaType) 268 case nil: // API ResponseTemplate 269 h = &design.AttributeDefinition{} 270 default: 271 dslengine.ReportError("invalid use of Response or ResponseTemplate") 272 } 273 if dslengine.Execute(dsl, h) { 274 def.Headers = def.Headers.Merge(h) 275 } 276 277 default: 278 dslengine.IncompatibleDSL() 279 } 280 } else if cors, ok := corsDefinition(); ok { 281 vals := make([]string, len(params)) 282 for i, p := range params { 283 if v, ok := p.(string); ok { 284 vals[i] = v 285 } else { 286 dslengine.ReportError("invalid parameter at position %d: must be a string", i) 287 return 288 } 289 } 290 cors.Headers = vals 291 } else { 292 dslengine.IncompatibleDSL() 293 } 294 } 295 296 // Params describe the action parameters, either path parameters identified via wildcards or query 297 // string parameters if there is no corresponding path parameter. Each parameter is described via 298 // the Param function which uses the same DSL as the Attribute DSL. Here is an example: 299 // 300 // Params(func() { 301 // Param("id", Integer) // A path parameter defined using e.g. GET("/:id") 302 // Param("sort", String, func() { // A query string parameter 303 // Enum("asc", "desc") 304 // }) 305 // }) 306 // 307 // Params can be used inside Action to define the action parameters, Resource to define common 308 // parameters to all the resource actions or API to define common parameters to all the API actions. 309 // 310 // If Params is used inside Resource or Action then the resource base media type attributes provide 311 // default values for all the properties of params with identical names. For example: 312 // 313 // var BottleMedia = MediaType("application/vnd.bottle", func() { 314 // Attributes(func() { 315 // Attribute("name", String, "The name of the bottle", func() { 316 // MinLength(2) // BottleMedia has one attribute "name" which is a 317 // // string that must be at least 2 characters long. 318 // }) 319 // }) 320 // View("default", func() { 321 // Attribute("name") 322 // }) 323 // }) 324 // 325 // var _ = Resource("Bottle", func() { 326 // DefaultMedia(BottleMedia) // Resource "Bottle" uses "BottleMedia" as default 327 // Action("show", func() { // media type. 328 // Routing(GET("/:name")) 329 // Params(func() { 330 // Param("name") // inherits type, description and validation from 331 // // BottleMedia "name" attribute 332 // }) 333 // }) 334 // }) 335 // 336 func Params(dsl func()) { 337 var params *design.AttributeDefinition 338 switch def := dslengine.CurrentDefinition().(type) { 339 case *design.ActionDefinition: 340 params = newAttribute(def.Parent.MediaType) 341 case *design.ResourceDefinition: 342 params = newAttribute(def.MediaType) 343 case *design.APIDefinition: 344 params = new(design.AttributeDefinition) 345 default: 346 dslengine.IncompatibleDSL() 347 return 348 } 349 params.Type = make(design.Object) 350 if !dslengine.Execute(dsl, params) { 351 return 352 } 353 switch def := dslengine.CurrentDefinition().(type) { 354 case *design.ActionDefinition: 355 def.Params = def.Params.Merge(params) // Useful for traits 356 case *design.ResourceDefinition: 357 def.Params = def.Params.Merge(params) // Useful for traits 358 case *design.APIDefinition: 359 def.Params = def.Params.Merge(params) // Useful for traits 360 } 361 } 362 363 // Payload implements the action payload DSL. An action payload describes the HTTP request body 364 // data structure. The function accepts either a type or a DSL that describes the payload members 365 // using the Member DSL which accepts the same syntax as the Attribute DSL. This function can be 366 // called passing in a type, a DSL or both. Examples: 367 // 368 // Payload(BottlePayload) // Request payload is described by the BottlePayload type 369 // 370 // Payload(func() { // Request payload is an object and is described inline 371 // Member("Name") 372 // }) 373 // 374 // Payload(BottlePayload, func() { // Request payload is described by merging the inline 375 // Required("Name") // definition into the BottlePayload type. 376 // }) 377 // 378 func Payload(p interface{}, dsls ...func()) { 379 payload(false, p, dsls...) 380 } 381 382 // OptionalPayload implements the action optional payload DSL. The function works identically to the 383 // Payload DSL except it sets a bit in the action definition to denote that the payload is not 384 // required. Example: 385 // 386 // OptionalPayload(BottlePayload) // Request payload is described by the BottlePayload type and is optional 387 // 388 func OptionalPayload(p interface{}, dsls ...func()) { 389 payload(true, p, dsls...) 390 } 391 392 func payload(isOptional bool, p interface{}, dsls ...func()) { 393 if len(dsls) > 1 { 394 dslengine.ReportError("too many arguments given to Payload") 395 return 396 } 397 if a, ok := actionDefinition(); ok { 398 var att *design.AttributeDefinition 399 var dsl func() 400 switch actual := p.(type) { 401 case func(): 402 dsl = actual 403 att = newAttribute(a.Parent.MediaType) 404 att.Type = design.Object{} 405 case *design.AttributeDefinition: 406 att = design.DupAtt(actual) 407 case *design.UserTypeDefinition: 408 if len(dsls) == 0 { 409 a.Payload = actual 410 a.PayloadOptional = isOptional 411 return 412 } 413 att = design.DupAtt(actual.Definition()) 414 case *design.MediaTypeDefinition: 415 att = design.DupAtt(actual.AttributeDefinition) 416 case string: 417 ut, ok := design.Design.Types[actual] 418 if !ok { 419 dslengine.ReportError("unknown payload type %s", actual) 420 } 421 att = design.DupAtt(ut.AttributeDefinition) 422 case *design.Array: 423 att = &design.AttributeDefinition{Type: actual} 424 case *design.Hash: 425 att = &design.AttributeDefinition{Type: actual} 426 case design.Primitive: 427 att = &design.AttributeDefinition{Type: actual} 428 default: 429 dslengine.ReportError("invalid Payload argument, must be a type, a media type or a DSL building a type") 430 return 431 } 432 if len(dsls) == 1 { 433 if dsl != nil { 434 dslengine.ReportError("invalid arguments in Payload call, must be (type), (dsl) or (type, dsl)") 435 } 436 dsl = dsls[0] 437 } 438 if dsl != nil { 439 dslengine.Execute(dsl, att) 440 } 441 rn := camelize(a.Parent.Name) 442 an := camelize(a.Name) 443 a.Payload = &design.UserTypeDefinition{ 444 AttributeDefinition: att, 445 TypeName: fmt.Sprintf("%s%sPayload", an, rn), 446 } 447 a.PayloadOptional = isOptional 448 } 449 } 450 451 // newAttribute creates a new attribute definition using the media type with the given identifier 452 // as base type. 453 func newAttribute(baseMT string) *design.AttributeDefinition { 454 var base design.DataType 455 if mt := design.Design.MediaTypeWithIdentifier(baseMT); mt != nil { 456 base = mt.Type 457 } 458 return &design.AttributeDefinition{Reference: base} 459 } 460 461 func camelize(str string) string { 462 runes := []rune(str) 463 w, i := 0, 0 464 for i+1 <= len(runes) { 465 eow := false 466 if i+1 == len(runes) { 467 eow = true 468 } else if !validIdentifier(runes[i]) { 469 runes = append(runes[:i], runes[i+1:]...) 470 } else if spacer(runes[i+1]) { 471 eow = true 472 n := 1 473 for i+n+1 < len(runes) && spacer(runes[i+n+1]) { 474 n++ 475 } 476 copy(runes[i+1:], runes[i+n+1:]) 477 runes = runes[:len(runes)-n] 478 } else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) { 479 eow = true 480 } 481 i++ 482 if !eow { 483 continue 484 } 485 runes[w] = unicode.ToUpper(runes[w]) 486 w = i 487 } 488 return string(runes) 489 } 490 491 // validIdentifier returns true if the rune is a letter or number 492 func validIdentifier(r rune) bool { 493 return unicode.IsLetter(r) || unicode.IsDigit(r) 494 } 495 496 func spacer(c rune) bool { 497 switch c { 498 case '_', ' ', ':', '-': 499 return true 500 } 501 return false 502 }