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  }