github.com/zak-blake/goa@v1.4.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  }