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  }