github.com/shogo82148/goa-v1@v1.6.2/goagen/gen_swagger/swagger_test.go (about)

     1  package genswagger_test
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  
     7  	"github.com/go-openapi/loads"
     8  	. "github.com/onsi/ginkgo"
     9  	. "github.com/onsi/gomega"
    10  	"github.com/shogo82148/goa-v1/design"
    11  	"github.com/shogo82148/goa-v1/design/apidsl"
    12  	"github.com/shogo82148/goa-v1/dslengine"
    13  	genschema "github.com/shogo82148/goa-v1/goagen/gen_schema"
    14  	genswagger "github.com/shogo82148/goa-v1/goagen/gen_swagger"
    15  	_ "github.com/shogo82148/goa-v1/goagen/gen_swagger/internal/design"
    16  )
    17  
    18  // validateSwagger validates that the given swagger object represents a valid Swagger spec.
    19  func validateSwagger(swagger *genswagger.Swagger) {
    20  	b, err := json.Marshal(swagger)
    21  	Ω(err).ShouldNot(HaveOccurred())
    22  	doc, err := loads.Analyzed(json.RawMessage(b), "")
    23  	Ω(err).ShouldNot(HaveOccurred())
    24  	Ω(doc).ShouldNot(BeNil())
    25  }
    26  
    27  // validateSwaggerWithFragments validates that the given swagger object represents a valid Swagger spec
    28  // and contains fragments
    29  func validateSwaggerWithFragments(swagger *genswagger.Swagger, fragments [][]byte) {
    30  	b, err := json.Marshal(swagger)
    31  	Ω(err).ShouldNot(HaveOccurred())
    32  	doc, err := loads.Analyzed(json.RawMessage(b), "")
    33  	Ω(err).ShouldNot(HaveOccurred())
    34  	Ω(doc).ShouldNot(BeNil())
    35  	for _, sub := range fragments {
    36  		Ω(bytes.Contains(b, sub)).Should(BeTrue())
    37  	}
    38  }
    39  
    40  var _ = Describe("New", func() {
    41  	var swagger *genswagger.Swagger
    42  	var newErr error
    43  
    44  	BeforeEach(func() {
    45  		swagger = nil
    46  		newErr = nil
    47  		dslengine.Reset()
    48  		genschema.Definitions = make(map[string]*genschema.JSONSchema)
    49  	})
    50  
    51  	JustBeforeEach(func() {
    52  		err := dslengine.Run()
    53  		Ω(err).ShouldNot(HaveOccurred())
    54  		swagger, newErr = genswagger.New(design.Design)
    55  	})
    56  
    57  	Context("with a valid API definition", func() {
    58  		const (
    59  			title        = "title"
    60  			description  = "description"
    61  			terms        = "terms"
    62  			contactEmail = "contactEmail@goa.design"
    63  			contactName  = "contactName"
    64  			contactURL   = "http://contactURL.com"
    65  			license      = "license"
    66  			licenseURL   = "http://licenseURL.com"
    67  			host         = "host"
    68  			scheme       = "https"
    69  			basePath     = "/base"
    70  			tag          = "tag"
    71  			docDesc      = "doc description"
    72  			docURL       = "http://docURL.com"
    73  		)
    74  
    75  		BeforeEach(func() {
    76  			apidsl.API("test", func() {
    77  				apidsl.Title(title)
    78  				apidsl.Metadata("swagger:tag:" + tag)
    79  				apidsl.Metadata("swagger:tag:"+tag+":desc", "Tag desc.")
    80  				apidsl.Metadata("swagger:tag:"+tag+":url", "http://example.com/tag")
    81  				apidsl.Metadata("swagger:tag:"+tag+":url:desc", "Huge docs")
    82  				apidsl.Description(description)
    83  				apidsl.TermsOfService(terms)
    84  				apidsl.Contact(func() {
    85  					apidsl.Email(contactEmail)
    86  					apidsl.Name(contactName)
    87  					apidsl.URL(contactURL)
    88  				})
    89  				apidsl.License(func() {
    90  					apidsl.Name(license)
    91  					apidsl.URL(licenseURL)
    92  				})
    93  				apidsl.Docs(func() {
    94  					apidsl.Description(docDesc)
    95  					apidsl.URL(docURL)
    96  				})
    97  				apidsl.Host(host)
    98  				apidsl.Scheme(scheme)
    99  				apidsl.BasePath(basePath)
   100  			})
   101  		})
   102  
   103  		It("sets all the basic fields", func() {
   104  			Ω(newErr).ShouldNot(HaveOccurred())
   105  			Ω(swagger).Should(Equal(&genswagger.Swagger{
   106  				Swagger: "2.0",
   107  				Info: &genswagger.Info{
   108  					Title:          title,
   109  					Description:    description,
   110  					TermsOfService: terms,
   111  					Contact: &design.ContactDefinition{
   112  						Name:  contactName,
   113  						Email: contactEmail,
   114  						URL:   contactURL,
   115  					},
   116  					License: &design.LicenseDefinition{
   117  						Name: license,
   118  						URL:  licenseURL,
   119  					},
   120  					Version: "",
   121  				},
   122  				Host:     host,
   123  				BasePath: basePath,
   124  				Schemes:  []string{"https"},
   125  				Paths:    make(map[string]interface{}),
   126  				Consumes: []string{"application/json", "application/xml", "application/gob", "application/x-gob"},
   127  				Produces: []string{"application/json", "application/xml", "application/gob", "application/x-gob"},
   128  				Tags: []*genswagger.Tag{{Name: tag, Description: "Tag desc.", ExternalDocs: &genswagger.ExternalDocs{
   129  					URL: "http://example.com/tag", Description: "Huge docs",
   130  				}}},
   131  				ExternalDocs: &genswagger.ExternalDocs{
   132  					Description: docDesc,
   133  					URL:         docURL,
   134  				},
   135  			}))
   136  		})
   137  
   138  		It("serializes into valid swagger JSON", func() { validateSwagger(swagger) })
   139  
   140  		Context("with base params", func() {
   141  			const (
   142  				basePath    = "/s/:strParam/i/:intParam/n/:numParam/b/:boolParam"
   143  				strParam    = "strParam"
   144  				intParam    = "intParam"
   145  				numParam    = "numParam"
   146  				boolParam   = "boolParam"
   147  				queryParam  = "queryParam"
   148  				description = "description"
   149  				intMin      = 1.0
   150  				floatMax    = 2.4
   151  				enum1       = "enum1"
   152  				enum2       = "enum2"
   153  			)
   154  
   155  			BeforeEach(func() {
   156  				base := design.Design.DSLFunc
   157  				design.Design.DSLFunc = func() {
   158  					base()
   159  					apidsl.BasePath(basePath)
   160  					apidsl.Params(func() {
   161  						apidsl.Param(strParam, design.String, func() {
   162  							apidsl.Description(description)
   163  							apidsl.Format("email")
   164  						})
   165  						apidsl.Param(intParam, design.Integer, func() {
   166  							apidsl.Minimum(intMin)
   167  						})
   168  						apidsl.Param(numParam, design.Number, func() {
   169  							apidsl.Maximum(floatMax)
   170  						})
   171  						apidsl.Param(boolParam, design.Boolean)
   172  						apidsl.Param(queryParam, func() {
   173  							apidsl.Enum(enum1, enum2)
   174  						})
   175  					})
   176  				}
   177  			})
   178  
   179  			It("sets the BasePath and Parameters fields", func() {
   180  				Ω(newErr).ShouldNot(HaveOccurred())
   181  				Ω(swagger.BasePath).Should(Equal(basePath))
   182  				Ω(swagger.Parameters).Should(HaveLen(5))
   183  				Ω(swagger.Parameters[strParam]).ShouldNot(BeNil())
   184  				Ω(swagger.Parameters[strParam].Name).Should(Equal(strParam))
   185  				Ω(swagger.Parameters[strParam].In).Should(Equal("path"))
   186  				Ω(swagger.Parameters[strParam].Description).Should(Equal("description"))
   187  				Ω(swagger.Parameters[strParam].Required).Should(BeTrue())
   188  				Ω(swagger.Parameters[strParam].Type).Should(Equal("string"))
   189  				Ω(swagger.Parameters[strParam].Format).Should(Equal("email"))
   190  				Ω(swagger.Parameters[intParam]).ShouldNot(BeNil())
   191  				Ω(swagger.Parameters[intParam].Name).Should(Equal(intParam))
   192  				Ω(swagger.Parameters[intParam].In).Should(Equal("path"))
   193  				Ω(swagger.Parameters[intParam].Required).Should(BeTrue())
   194  				Ω(swagger.Parameters[intParam].Type).Should(Equal("integer"))
   195  				Ω(*swagger.Parameters[intParam].Minimum).Should(Equal(intMin))
   196  				Ω(swagger.Parameters[numParam]).ShouldNot(BeNil())
   197  				Ω(swagger.Parameters[numParam].Name).Should(Equal(numParam))
   198  				Ω(swagger.Parameters[numParam].In).Should(Equal("path"))
   199  				Ω(swagger.Parameters[numParam].Required).Should(BeTrue())
   200  				Ω(swagger.Parameters[numParam].Type).Should(Equal("number"))
   201  				Ω(*swagger.Parameters[numParam].Maximum).Should(Equal(floatMax))
   202  				Ω(swagger.Parameters[boolParam]).ShouldNot(BeNil())
   203  				Ω(swagger.Parameters[boolParam].Name).Should(Equal(boolParam))
   204  				Ω(swagger.Parameters[boolParam].In).Should(Equal("path"))
   205  				Ω(swagger.Parameters[boolParam].Required).Should(BeTrue())
   206  				Ω(swagger.Parameters[boolParam].Type).Should(Equal("boolean"))
   207  				Ω(swagger.Parameters[queryParam]).ShouldNot(BeNil())
   208  				Ω(swagger.Parameters[queryParam].Name).Should(Equal(queryParam))
   209  				Ω(swagger.Parameters[queryParam].In).Should(Equal("query"))
   210  				Ω(swagger.Parameters[queryParam].Type).Should(Equal("string"))
   211  				Ω(swagger.Parameters[queryParam].Enum).Should(Equal([]interface{}{enum1, enum2}))
   212  			})
   213  
   214  			It("serializes into valid swagger JSON", func() { validateSwagger(swagger) })
   215  		})
   216  
   217  		Context("with required payload", func() {
   218  			BeforeEach(func() {
   219  				p := apidsl.Type("RequiredPayload", func() {
   220  					apidsl.Member("m1", design.String)
   221  				})
   222  				apidsl.Resource("res", func() {
   223  					apidsl.Action("act", func() {
   224  						apidsl.Routing(
   225  							apidsl.PUT("/"),
   226  						)
   227  						apidsl.Payload(p)
   228  					})
   229  				})
   230  			})
   231  
   232  			It("serializes into valid swagger JSON", func() {
   233  				validateSwaggerWithFragments(swagger, [][]byte{
   234  					[]byte(`"required":true`),
   235  				})
   236  			})
   237  		})
   238  
   239  		Context("with a payload of type Any", func() {
   240  			BeforeEach(func() {
   241  				apidsl.Resource("res", func() {
   242  					apidsl.Action("act", func() {
   243  						apidsl.Routing(
   244  							apidsl.PUT("/"),
   245  						)
   246  						apidsl.Payload(design.Any, func() {
   247  							apidsl.Example("example")
   248  						})
   249  					})
   250  				})
   251  			})
   252  
   253  			It("serializes into valid swagger JSON", func() {
   254  				validateSwaggerWithFragments(swagger, [][]byte{
   255  					[]byte(`"ActResPayload":{"title":"ActResPayload","example":"example"}`),
   256  				})
   257  			})
   258  
   259  		})
   260  
   261  		Context("with optional payload", func() {
   262  			BeforeEach(func() {
   263  				p := apidsl.Type("OptionalPayload", func() {
   264  					apidsl.Member("m1", design.String)
   265  				})
   266  				apidsl.Resource("res", func() {
   267  					apidsl.Action("act", func() {
   268  						apidsl.Routing(
   269  							apidsl.PUT("/"),
   270  						)
   271  						apidsl.OptionalPayload(p)
   272  					})
   273  				})
   274  			})
   275  
   276  			It("serializes into valid swagger JSON", func() {
   277  				validateSwaggerWithFragments(swagger, [][]byte{
   278  					[]byte(`"required":false`),
   279  				})
   280  			})
   281  
   282  		})
   283  
   284  		Context("with multipart/form-data payload", func() {
   285  			BeforeEach(func() {
   286  				f := apidsl.Type("MultipartPayload", func() {
   287  					apidsl.Attribute("image", design.File, "Binary image data")
   288  				})
   289  				apidsl.Resource("res", func() {
   290  					apidsl.Action("act", func() {
   291  						apidsl.Routing(
   292  							apidsl.PUT("/"),
   293  						)
   294  						apidsl.MultipartForm()
   295  						apidsl.Payload(f)
   296  					})
   297  				})
   298  			})
   299  
   300  			It("does not modify the API level consumes", func() {
   301  				Ω(newErr).ShouldNot(HaveOccurred())
   302  				Ω(swagger.Consumes).Should(HaveLen(4))
   303  				Ω(swagger.Consumes).Should(ConsistOf("application/json", "application/xml", "application/gob", "application/x-gob"))
   304  			})
   305  
   306  			It("adds an Action level consumes for multipart/form-data", func() {
   307  				Ω(newErr).ShouldNot(HaveOccurred())
   308  				Ω(swagger.Paths).Should(HaveLen(1))
   309  				Ω(swagger.Paths["/"]).ShouldNot(BeNil())
   310  
   311  				a := swagger.Paths["/"].(*genswagger.Path)
   312  				Ω(a.Put).ShouldNot(BeNil())
   313  				cs := a.Put.Consumes
   314  				Ω(cs).Should(HaveLen(1))
   315  				Ω(cs[0]).Should(Equal("multipart/form-data"))
   316  			})
   317  
   318  			It("adds an File parameter", func() {
   319  				Ω(newErr).ShouldNot(HaveOccurred())
   320  				Ω(swagger.Paths).Should(HaveLen(1))
   321  				Ω(swagger.Paths["/"]).ShouldNot(BeNil())
   322  
   323  				a := swagger.Paths["/"].(*genswagger.Path)
   324  				Ω(a.Put).ShouldNot(BeNil())
   325  				ps := a.Put.Parameters
   326  				Ω(ps).Should(HaveLen(1))
   327  				Ω(ps[0]).Should(Equal(&genswagger.Parameter{In: "formData", Name: "image", Type: "file", Description: "Binary image data", Required: false}))
   328  			})
   329  
   330  			It("serializes into valid swagger JSON", func() { validateSwagger(swagger) })
   331  		})
   332  
   333  		Context("with recursive payload", func() {
   334  			BeforeEach(func() {
   335  				p := apidsl.Type("RecursivePayload", func() {
   336  					apidsl.Member("m1", "RecursivePayload")
   337  					apidsl.Member("m2", apidsl.ArrayOf("RecursivePayload"))
   338  					apidsl.Member("m3", apidsl.HashOf(design.String, "RecursivePayload"))
   339  					apidsl.Member("m4", func() {
   340  						apidsl.Member("m5", design.String)
   341  						apidsl.Member("m6", "RecursivePayload")
   342  					})
   343  				})
   344  				apidsl.Resource("res", func() {
   345  					apidsl.Action("act", func() {
   346  						apidsl.Routing(
   347  							apidsl.PUT("/"),
   348  						)
   349  						apidsl.Payload(p)
   350  					})
   351  				})
   352  			})
   353  
   354  			It("serializes into valid swagger JSON", func() { validateSwagger(swagger) })
   355  		})
   356  
   357  		Context("with zero value validations", func() {
   358  			const (
   359  				intParam = "intParam"
   360  				numParam = "numParam"
   361  				strParam = "strParam"
   362  				intMin   = 0.0
   363  				floatMax = 0.0
   364  			)
   365  
   366  			BeforeEach(func() {
   367  				PayloadWithZeroValueValidations := apidsl.Type("PayloadWithZeroValueValidations", func() {
   368  					apidsl.Attribute(strParam, design.String, func() {
   369  						apidsl.MinLength(0)
   370  						apidsl.MaxLength(0)
   371  					})
   372  				})
   373  				apidsl.Resource("res", func() {
   374  					apidsl.Action("act", func() {
   375  						apidsl.Routing(
   376  							apidsl.PUT("/"),
   377  						)
   378  						apidsl.Params(func() {
   379  							apidsl.Param(intParam, design.Integer, func() {
   380  								apidsl.Minimum(intMin)
   381  							})
   382  							apidsl.Param(numParam, design.Number, func() {
   383  								apidsl.Maximum(floatMax)
   384  							})
   385  						})
   386  						apidsl.Payload(PayloadWithZeroValueValidations)
   387  					})
   388  				})
   389  			})
   390  
   391  			It("serializes into valid swagger JSON", func() {
   392  				validateSwaggerWithFragments(swagger, [][]byte{
   393  					// payload
   394  					[]byte(`"minLength":0`),
   395  					[]byte(`"maxLength":0`),
   396  					// param
   397  					[]byte(`"minimum":0`),
   398  					[]byte(`"maximum":0`),
   399  				})
   400  			})
   401  		})
   402  
   403  		Context("with minItems and maxItems validations in payload's attribute", func() {
   404  			const (
   405  				arrParam = "arrParam"
   406  				minVal   = 0
   407  				maxVal   = 42
   408  			)
   409  
   410  			BeforeEach(func() {
   411  				PayloadWithValidations := apidsl.Type("Payload", func() {
   412  					apidsl.Attribute(arrParam, apidsl.ArrayOf(design.String), func() {
   413  						apidsl.MinLength(minVal)
   414  						apidsl.MaxLength(maxVal)
   415  					})
   416  				})
   417  				apidsl.Resource("res", func() {
   418  					apidsl.Action("act", func() {
   419  						apidsl.Routing(
   420  							apidsl.PUT("/"),
   421  						)
   422  						apidsl.Payload(PayloadWithValidations)
   423  					})
   424  				})
   425  			})
   426  
   427  			It("serializes into valid swagger JSON", func() {
   428  				validateSwaggerWithFragments(swagger, [][]byte{
   429  					// payload
   430  					[]byte(`"minItems":0`),
   431  					[]byte(`"maxItems":42`),
   432  				})
   433  			})
   434  		})
   435  
   436  		Context("with minItems and maxItems validations in payload", func() {
   437  			const (
   438  				strParam = "strParam"
   439  				minVal   = 0
   440  				maxVal   = 42
   441  			)
   442  
   443  			BeforeEach(func() {
   444  				PayloadWithValidations := apidsl.Type("Payload", func() {
   445  					apidsl.Attribute(strParam, design.String)
   446  				})
   447  				apidsl.Resource("res", func() {
   448  					apidsl.Action("act", func() {
   449  						apidsl.Routing(
   450  							apidsl.PUT("/"),
   451  						)
   452  						apidsl.Payload(apidsl.ArrayOf(PayloadWithValidations), func() {
   453  							apidsl.MinLength(minVal)
   454  							apidsl.MaxLength(maxVal)
   455  						})
   456  					})
   457  				})
   458  			})
   459  
   460  			It("serializes into valid swagger JSON", func() {
   461  				validateSwaggerWithFragments(swagger, [][]byte{
   462  					// payload
   463  					[]byte(`"minItems":0`),
   464  					[]byte(`"maxItems":42`),
   465  				})
   466  			})
   467  		})
   468  
   469  		Context("with response templates", func() {
   470  			const okName = "OK"
   471  			const okDesc = "OK description"
   472  			const notFoundName = "NotFound"
   473  			const notFoundDesc = "NotFound description"
   474  			const notFoundMt = "application/json"
   475  			const headerName = "headerName"
   476  
   477  			BeforeEach(func() {
   478  				account := apidsl.MediaType("application/vnd.goa.test.account", func() {
   479  					apidsl.Description("Account")
   480  					apidsl.Attributes(func() {
   481  						apidsl.Attribute("id", design.Integer)
   482  						apidsl.Attribute("href", design.String)
   483  					})
   484  					apidsl.View("default", func() {
   485  						apidsl.Attribute("id")
   486  						apidsl.Attribute("href")
   487  					})
   488  					apidsl.View("link", func() {
   489  						apidsl.Attribute("id")
   490  						apidsl.Attribute("href")
   491  					})
   492  				})
   493  				mt := apidsl.MediaType("application/vnd.goa.test.bottle", func() {
   494  					apidsl.Description("A bottle of wine")
   495  					apidsl.Attributes(func() {
   496  						apidsl.Attribute("id", design.Integer, "ID of bottle")
   497  						apidsl.Attribute("href", design.String, "API href of bottle")
   498  						apidsl.Attribute("account", account, "Owner account")
   499  						apidsl.Links(func() {
   500  							apidsl.Link("account") // Defines a link to the Account media type
   501  						})
   502  						apidsl.Required("id", "href")
   503  					})
   504  					apidsl.View("default", func() {
   505  						apidsl.Attribute("id")
   506  						apidsl.Attribute("href")
   507  						apidsl.Attribute("links") // Default view renders links
   508  					})
   509  					apidsl.View("extended", func() {
   510  						apidsl.Attribute("id")
   511  						apidsl.Attribute("href")
   512  						apidsl.Attribute("account") // Extended view renders account inline
   513  						apidsl.Attribute("links")   // Extended view also renders links
   514  					})
   515  				})
   516  				base := design.Design.DSLFunc
   517  				design.Design.DSLFunc = func() {
   518  					base()
   519  					apidsl.ResponseTemplate(okName, func() {
   520  						apidsl.Description(okDesc)
   521  						apidsl.Status(404)
   522  						apidsl.Media(mt)
   523  						apidsl.Headers(func() {
   524  							apidsl.Header(headerName, func() {
   525  								apidsl.Format("hostname")
   526  							})
   527  						})
   528  					})
   529  					apidsl.ResponseTemplate(notFoundName, func() {
   530  						apidsl.Description(notFoundDesc)
   531  						apidsl.Status(404)
   532  
   533  						apidsl.Media(notFoundMt)
   534  					})
   535  				}
   536  			})
   537  
   538  			It("sets the Responses fields", func() {
   539  				Ω(newErr).ShouldNot(HaveOccurred())
   540  				Ω(swagger.Responses).Should(HaveLen(2))
   541  				Ω(swagger.Responses[notFoundName]).ShouldNot(BeNil())
   542  				Ω(swagger.Responses[notFoundName].Description).Should(Equal(notFoundDesc))
   543  				Ω(swagger.Responses[okName]).ShouldNot(BeNil())
   544  				Ω(swagger.Responses[okName].Description).Should(Equal(okDesc))
   545  			})
   546  
   547  			It("serializes into valid swagger JSON", func() { validateSwagger(swagger) })
   548  		})
   549  
   550  		Context("with resources", func() {
   551  			var (
   552  				minLength1  = 1
   553  				maxLength10 = 10
   554  				minimum_2   = -2.0
   555  				maximum2    = 2.0
   556  				minItems1   = 1
   557  				maxItems5   = 5
   558  			)
   559  			BeforeEach(func() {
   560  				Country := apidsl.MediaType("application/vnd.goa.example.origin", func() {
   561  					apidsl.Description("Origin of bottle")
   562  					apidsl.Attributes(func() {
   563  						apidsl.Attribute("id")
   564  						apidsl.Attribute("href")
   565  						apidsl.Attribute("country")
   566  					})
   567  					apidsl.View("default", func() {
   568  						apidsl.Attribute("id")
   569  						apidsl.Attribute("href")
   570  						apidsl.Attribute("country")
   571  					})
   572  					apidsl.View("tiny", func() {
   573  						apidsl.Attribute("id")
   574  					})
   575  				})
   576  				BottleMedia := apidsl.MediaType("application/vnd.goa.example.bottle", func() {
   577  					apidsl.Description("A bottle of wine")
   578  					apidsl.Attributes(func() {
   579  						apidsl.Attribute("id", design.Integer, "ID of bottle")
   580  						apidsl.Attribute("href", design.String, "API href of bottle")
   581  						apidsl.Attribute("origin", Country, "Details on wine origin")
   582  						apidsl.Links(func() {
   583  							apidsl.Link("origin", "tiny")
   584  						})
   585  						apidsl.Required("id", "href")
   586  					})
   587  					apidsl.View("default", func() {
   588  						apidsl.Attribute("id")
   589  						apidsl.Attribute("href")
   590  						apidsl.Attribute("links")
   591  					})
   592  					apidsl.View("extended", func() {
   593  						apidsl.Attribute("id")
   594  						apidsl.Attribute("href")
   595  						apidsl.Attribute("origin")
   596  						apidsl.Attribute("links")
   597  					})
   598  				})
   599  				UpdatePayload := apidsl.Type("UpdatePayload", func() {
   600  					apidsl.Description("Type of create and upload action payloads")
   601  					apidsl.Attribute("name", design.String, "name of bottle")
   602  					apidsl.Attribute("origin", Country, "Details on wine origin")
   603  					apidsl.Required("name")
   604  				})
   605  				apidsl.Resource("res", func() {
   606  					apidsl.Metadata("swagger:tag:res")
   607  					apidsl.Description("A wine bottle")
   608  					apidsl.DefaultMedia(BottleMedia)
   609  					apidsl.BasePath("/bottles")
   610  					apidsl.UseTrait("Authenticated")
   611  
   612  					apidsl.Action("Update", func() {
   613  						apidsl.Metadata("swagger:tag:Update")
   614  						apidsl.Metadata("swagger:summary", "a summary")
   615  						apidsl.Description("Update account")
   616  						apidsl.Docs(func() {
   617  							apidsl.Description("docs")
   618  							apidsl.URL("http://cellarapi.com/docs/actions/update")
   619  						})
   620  						apidsl.Routing(
   621  							apidsl.PUT("/:id"),
   622  							apidsl.PUT("//orgs/:org/accounts/:id"),
   623  						)
   624  						apidsl.Params(func() {
   625  							apidsl.Param("org", design.String)
   626  							apidsl.Param("id", design.Integer)
   627  							apidsl.Param("sort", func() {
   628  								apidsl.Enum("asc", "desc")
   629  							})
   630  						})
   631  						apidsl.Headers(func() {
   632  							apidsl.Header("Authorization", design.String)
   633  							apidsl.Header("X-Account", design.Integer)
   634  							apidsl.Header("OptionalBoolWithDefault", design.Boolean, "defaults true", func() {
   635  								apidsl.Default(true)
   636  							})
   637  							apidsl.Header("OptionalRegex", design.String, func() {
   638  								apidsl.Pattern(`[a-z]\d+`)
   639  								apidsl.MinLength(minLength1)
   640  								apidsl.MaxLength(maxLength10)
   641  							})
   642  							apidsl.Header("OptionalInt", design.Integer, func() {
   643  								apidsl.Minimum(minimum_2)
   644  								apidsl.Maximum(maximum2)
   645  							})
   646  							apidsl.Header("OptionalArray", apidsl.ArrayOf(design.String), func() {
   647  								// interpreted as MinItems & MaxItems:
   648  								apidsl.MinLength(minItems1)
   649  								apidsl.MaxLength(maxItems5)
   650  							})
   651  							apidsl.Header("OverrideRequiredHeader")
   652  							apidsl.Header("OverrideOptionalHeader")
   653  							apidsl.Required("Authorization", "X-Account", "OverrideOptionalHeader")
   654  						})
   655  						apidsl.Payload(UpdatePayload)
   656  						apidsl.Response(design.OK, func() {
   657  							apidsl.Media(apidsl.CollectionOf(BottleMedia), "extended")
   658  						})
   659  						apidsl.Response(design.NoContent)
   660  						apidsl.Response(design.NotFound, design.ErrorMedia)
   661  						apidsl.Response(design.BadRequest, design.ErrorMedia)
   662  					})
   663  
   664  					apidsl.Action("hidden", func() {
   665  						apidsl.Description("Does not show up in Swagger spec")
   666  						apidsl.Metadata("swagger:generate", "false")
   667  						apidsl.Routing(apidsl.GET("/hidden"))
   668  						apidsl.Response(design.OK)
   669  					})
   670  				})
   671  				base := design.Design.DSLFunc
   672  				design.Design.DSLFunc = func() {
   673  					base()
   674  					apidsl.Trait("Authenticated", func() {
   675  						apidsl.Headers(func() {
   676  							apidsl.Header("header")
   677  							apidsl.Header("OverrideRequiredHeader", design.String, "to be overridden in Action and not marked Required")
   678  							apidsl.Header("OverrideOptionalHeader", design.String, "to be overridden in Action and marked Required")
   679  							apidsl.Header("OptionalResourceHeaderWithEnum", func() {
   680  								apidsl.Enum("a", "b")
   681  							})
   682  							apidsl.Required("header", "OverrideRequiredHeader")
   683  						})
   684  					})
   685  				}
   686  			})
   687  
   688  			It("sets the Path fields", func() {
   689  				Ω(newErr).ShouldNot(HaveOccurred())
   690  				Ω(swagger.Paths).Should(HaveLen(2))
   691  				Ω(swagger.Paths["/orgs/{org}/accounts/{id}"]).ShouldNot(BeNil())
   692  				a := swagger.Paths["/orgs/{org}/accounts/{id}"].(*genswagger.Path)
   693  				Ω(a.Put).ShouldNot(BeNil())
   694  				ps := a.Put.Parameters
   695  				Ω(ps).Should(HaveLen(14))
   696  				// check Headers in detail
   697  				Ω(ps[3]).Should(Equal(&genswagger.Parameter{In: "header", Name: "Authorization", Type: "string", Required: true}))
   698  				Ω(ps[4]).Should(Equal(&genswagger.Parameter{In: "header", Name: "OptionalArray", Type: "array", CollectionFormat: "multi",
   699  					Items: &genswagger.Items{Type: "string"}, MinItems: &minItems1, MaxItems: &maxItems5}))
   700  				Ω(ps[5]).Should(Equal(&genswagger.Parameter{In: "header", Name: "OptionalBoolWithDefault", Type: "boolean",
   701  					Description: "defaults true", Default: true}))
   702  				Ω(ps[6]).Should(Equal(&genswagger.Parameter{In: "header", Name: "OptionalInt", Type: "integer", Minimum: &minimum_2, Maximum: &maximum2}))
   703  				Ω(ps[7]).Should(Equal(&genswagger.Parameter{In: "header", Name: "OptionalRegex", Type: "string",
   704  					Pattern: `[a-z]\d+`, MinLength: &minLength1, MaxLength: &maxLength10}))
   705  				Ω(ps[8]).Should(Equal(&genswagger.Parameter{In: "header", Name: "OptionalResourceHeaderWithEnum", Type: "string",
   706  					Enum: []interface{}{"a", "b"}}))
   707  				Ω(ps[9]).Should(Equal(&genswagger.Parameter{In: "header", Name: "OverrideOptionalHeader", Type: "string", Required: true}))
   708  				Ω(ps[10]).Should(Equal(&genswagger.Parameter{In: "header", Name: "OverrideRequiredHeader", Type: "string", Required: true}))
   709  				Ω(ps[11]).Should(Equal(&genswagger.Parameter{In: "header", Name: "X-Account", Type: "integer", Required: true}))
   710  				Ω(ps[12]).Should(Equal(&genswagger.Parameter{In: "header", Name: "header", Type: "string", Required: true}))
   711  				Ω(swagger.Paths["/base/bottles/{id}"]).ShouldNot(BeNil())
   712  				b := swagger.Paths["/base/bottles/{id}"].(*genswagger.Path)
   713  				Ω(b.Put).ShouldNot(BeNil())
   714  				Ω(b.Put.Parameters).Should(HaveLen(14))
   715  				Ω(b.Put.Produces).Should(Equal([]string{"application/vnd.goa.error", "application/vnd.goa.example.bottle; type=collection"}))
   716  			})
   717  
   718  			It("should set the inherited tag and the action tag", func() {
   719  				tags := []string{"res", "Update"}
   720  				a := swagger.Paths["/orgs/{org}/accounts/{id}"].(*genswagger.Path)
   721  				Ω(a.Put).ShouldNot(BeNil())
   722  				Ω(a.Put.Tags).Should(Equal(tags))
   723  				b := swagger.Paths["/base/bottles/{id}"].(*genswagger.Path)
   724  				Ω(b.Put.Tags).Should(Equal(tags))
   725  			})
   726  
   727  			It("sets the summary from the summary tag", func() {
   728  				a := swagger.Paths["/orgs/{org}/accounts/{id}"].(*genswagger.Path)
   729  				Ω(a.Put.Summary).Should(Equal("a summary"))
   730  			})
   731  
   732  			It("generates the media type collection schema", func() {
   733  				Ω(swagger.Definitions).Should(HaveLen(7))
   734  				Ω(swagger.Definitions).Should(HaveKey("GoaExampleBottleExtendedCollection"))
   735  			})
   736  
   737  			It("serializes into valid swagger JSON", func() { validateSwagger(swagger) })
   738  		})
   739  
   740  		Context("with metadata", func() {
   741  			const gat = "gat"
   742  			const extension = `{"foo":"bar"}`
   743  			const stringExtension = "foo"
   744  
   745  			var (
   746  				unmarshaled map[string]interface{}
   747  				_           = json.Unmarshal([]byte(extension), &unmarshaled)
   748  			)
   749  
   750  			BeforeEach(func() {
   751  				apidsl.Resource("res", func() {
   752  					apidsl.Metadata("swagger:tag:res")
   753  					apidsl.Metadata("struct:tag:json", "resource")
   754  					apidsl.Metadata("swagger:extension:x-resource", extension)
   755  					apidsl.Metadata("swagger:extension:x-string", stringExtension)
   756  					apidsl.Action("act", func() {
   757  						apidsl.Metadata("swagger:tag:Update")
   758  						apidsl.Metadata("struct:tag:json", "action")
   759  						apidsl.Metadata("swagger:extension:x-action", extension)
   760  						apidsl.Security("password", func() {
   761  							apidsl.Metadata("swagger:extension:x-security", extension)
   762  						})
   763  						apidsl.Routing(
   764  							apidsl.PUT("/", func() {
   765  								apidsl.Metadata("swagger:extension:x-put", extension)
   766  							}),
   767  						)
   768  						apidsl.Params(func() {
   769  							apidsl.Param("param", func() {
   770  								apidsl.Metadata("swagger:extension:x-param", extension)
   771  							})
   772  						})
   773  						apidsl.Response(design.NoContent, func() {
   774  							apidsl.Metadata("swagger:extension:x-response", extension)
   775  						})
   776  					})
   777  				})
   778  				base := design.Design.DSLFunc
   779  				design.Design.DSLFunc = func() {
   780  					base()
   781  					apidsl.Metadata("swagger:tag:" + gat)
   782  					apidsl.Metadata("struct:tag:json", "api")
   783  					apidsl.Metadata("swagger:extension:x-api", extension)
   784  					apidsl.BasicAuthSecurity("password")
   785  				}
   786  			})
   787  
   788  			It("should set the swagger object tags", func() {
   789  				Ω(swagger.Tags).Should(HaveLen(2))
   790  				tags := []*genswagger.Tag{
   791  					{Name: gat, Description: "", ExternalDocs: nil, Extensions: map[string]interface{}{"x-api": unmarshaled}},
   792  					{Name: tag, Description: "Tag desc.", ExternalDocs: &genswagger.ExternalDocs{URL: "http://example.com/tag", Description: "Huge docs"}, Extensions: map[string]interface{}{"x-api": unmarshaled}},
   793  				}
   794  				Ω(swagger.Tags).Should(Equal(tags))
   795  			})
   796  
   797  			It("should set the action tags", func() {
   798  				p := swagger.Paths["/"].(*genswagger.Path)
   799  				Ω(p.Put.Tags).Should(HaveLen(2))
   800  				tags := []string{"res", "Update"}
   801  				Ω(p.Put.Tags).Should(Equal(tags))
   802  			})
   803  
   804  			It("should set the swagger extensions", func() {
   805  				Ω(swagger.Info.Extensions).Should(HaveLen(1))
   806  				Ω(swagger.Info.Extensions["x-api"]).Should(Equal(unmarshaled))
   807  				p := swagger.Paths["/"].(*genswagger.Path)
   808  				Ω(p.Extensions).Should(HaveLen(1))
   809  				Ω(p.Extensions["x-action"]).Should(Equal(unmarshaled))
   810  				Ω(p.Put.Extensions).Should(HaveLen(1))
   811  				Ω(p.Put.Extensions["x-put"]).Should(Equal(unmarshaled))
   812  				Ω(p.Put.Parameters[0].Extensions).Should(HaveLen(1))
   813  				Ω(p.Put.Parameters[0].Extensions["x-param"]).Should(Equal(unmarshaled))
   814  				Ω(p.Put.Responses["204"].Extensions).Should(HaveLen(1))
   815  				Ω(p.Put.Responses["204"].Extensions["x-response"]).Should(Equal(unmarshaled))
   816  				Ω(swagger.Paths["x-resource"]).ShouldNot(BeNil())
   817  				rs := swagger.Paths["x-resource"].(map[string]interface{})
   818  				Ω(rs).Should(Equal(unmarshaled))
   819  				rs2 := swagger.Paths["x-string"].(string)
   820  				Ω(rs2).Should(Equal(stringExtension))
   821  				Ω(swagger.SecurityDefinitions["password"].Extensions).Should(HaveLen(1))
   822  				Ω(swagger.SecurityDefinitions["password"].Extensions["x-security"]).Should(Equal(unmarshaled))
   823  			})
   824  
   825  		})
   826  	})
   827  })