Skip to content

Commit 71a41d2

Browse files
author
Raphaël Simon
committed
Merge pull request goadesign#319 from goadesign/versioning_mux
Add DSL for defining param, header and querystring based versioning
2 parents 53d7c53 + 124d641 commit 71a41d2

11 files changed

+428
-60
lines changed

design/api.go

+9
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ type (
8282
Types map[string]*UserTypeDefinition
8383
// MediaTypes indexes the API media types by canonical identifier.
8484
MediaTypes map[string]*MediaTypeDefinition
85+
// VersionParams list the names of the path parameter wildcards that may contain
86+
// the name of the targeted API version.
87+
VersionParams []string
88+
// VersionHeaders list the names of the HTTP request headers that may contain the
89+
// name of the targeted API version.
90+
VersionHeaders []string
91+
// VersionQueries list the names of the HTTP request querystrings that may contain
92+
// the name of the targeted API version.
93+
VersionQueries []string
8594
// rand is the random generator used to generate examples.
8695
rand *RandomGenerator
8796
}

design/apidsl/api.go

+80-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import (
1616
// API("API name", func() {
1717
// Title("title") // API title used in documentation
1818
// Description("description") // API description used in documentation
19+
// VersionParam("version") // Path param that captures targeted version, can appear 0 or more times
20+
// VersionHeader("X-Api-Version") // Request header that captures targeted version, can appear 0 or more times
21+
// VersionQuery("version") // Querystring value that captures targeted version, can appear 0 or more times
1922
// TermsOfService("terms")
2023
// Contact(func() { // API Contact information
2124
// Name("contact name")
@@ -32,7 +35,7 @@ import (
3235
// })
3336
// Host("goa.design") // API hostname
3437
// Scheme("http")
35-
// BasePath("/base/:param") // Common base path to all API actions
38+
// BasePath("/base/:version/:param") // Common base path to all API actions
3639
// BaseParams(func() { // Common parameters to all API actions
3740
// Param("param")
3841
// })
@@ -78,7 +81,22 @@ func API(name string, dsl func()) *design.APIDefinition {
7881

7982
// Version is the top level design language function which defines the API global property values
8083
// for a given version. The DSL used to define the property values is identical to the one used by
81-
// the API function.
84+
// the API function. Here is an example that shows a *subset* of the Version
85+
// DSL (see the API function for all the other possible functions).
86+
//
87+
// Version("2.0", func() {
88+
// Title("API v2") // API version 2.0 title used in documentation
89+
// Description("This is v2") // API version description used in documentation
90+
// Docs(func() {
91+
// Description("v2 docs")
92+
// URL("v2 doc URL")
93+
// })
94+
// BasePath("/v2") // Common base path to all actions exposed by this API version
95+
// VersionHeader("X-Api-Version") // Usually only useful if BasePath is same as API
96+
// VersionQuery("version") // Generated code considers header first if specified then querystring
97+
// VersionQuery("v") // Multiple version headers or querystrings may be specified
98+
// VersionQuery("version", "v") // Equivalent to the two lines above
99+
// })
82100
func Version(ver string, dsl func()) *design.APIVersionDefinition {
83101
verdef := &design.APIVersionDefinition{Version: ver, DSLFunc: dsl}
84102
if _, ok := design.Design.APIVersions[ver]; ok {
@@ -95,6 +113,66 @@ func Version(ver string, dsl func()) *design.APIVersionDefinition {
95113
return verdef
96114
}
97115

116+
// VersionParam defines the name of the request path parameter that contains the targeted API version.
117+
// Multiple names may be specified in which case the value of the first to correspond to the name
118+
// of a param is used.
119+
func VersionParam(names ...string) {
120+
if api, ok := apiDefinition(true); ok {
121+
for _, n := range names {
122+
found := false
123+
for _, n2 := range api.VersionParams {
124+
if n == n2 {
125+
found = true
126+
break
127+
}
128+
}
129+
if !found {
130+
api.VersionParams = append(api.VersionParams, n)
131+
}
132+
}
133+
}
134+
}
135+
136+
// VersionHeader defines the name of the HTTP request header that contains the targeted API version.
137+
// Multiple names may be specified in which case the value of the first to correspond to the name
138+
// of a header is used.
139+
func VersionHeader(names ...string) {
140+
if api, ok := apiDefinition(true); ok {
141+
for _, n := range names {
142+
found := false
143+
for _, n2 := range api.VersionHeaders {
144+
if n == n2 {
145+
found = true
146+
break
147+
}
148+
}
149+
if !found {
150+
api.VersionHeaders = append(api.VersionHeaders, n)
151+
}
152+
}
153+
}
154+
}
155+
156+
// VersionQuery defines the name of the querystring that contains the targeted API version.
157+
// Multiple names may be specified in which case the value of the first to correspond to the name
158+
// of a querystring is used.
159+
func VersionQuery(names ...string) {
160+
if api, ok := apiDefinition(true); ok {
161+
for _, n := range names {
162+
found := false
163+
for _, n2 := range api.VersionQueries {
164+
if n == n2 {
165+
found = true
166+
break
167+
}
168+
}
169+
if !found {
170+
api.VersionQueries = append(api.VersionQueries, n)
171+
}
172+
}
173+
}
174+
}
175+
98176
// Description sets the definition description.
99177
// Description can be called inside API, Resource, Action or MediaType.
100178
func Description(d string) {

design/apidsl/api_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -315,4 +315,110 @@ var _ = Describe("API", func() {
315315
})
316316
})
317317
})
318+
319+
Context("using VersionParam", func() {
320+
const vparam = "version"
321+
BeforeEach(func() {
322+
name = "v1"
323+
dsl = func() {
324+
BasePath("/api/:" + vparam)
325+
VersionParam(vparam)
326+
}
327+
})
328+
329+
It("stores the version header name", func() {
330+
Ω(Design.Validate()).ShouldNot(HaveOccurred())
331+
Ω(Design.VersionParams).Should(Equal([]string{vparam}))
332+
})
333+
})
334+
335+
Context("using VersionHeader", func() {
336+
const vheader = "X-Api-Version"
337+
BeforeEach(func() {
338+
name = "v1"
339+
dsl = func() {
340+
VersionHeader(vheader)
341+
}
342+
})
343+
344+
It("stores the version header name", func() {
345+
Ω(Design.Validate()).ShouldNot(HaveOccurred())
346+
Ω(Design.VersionHeaders).Should(Equal([]string{vheader}))
347+
})
348+
})
349+
350+
Context("using VersionQuery", func() {
351+
const vquery = "version"
352+
BeforeEach(func() {
353+
name = "v1"
354+
dsl = func() {
355+
VersionQuery(vquery)
356+
}
357+
})
358+
359+
It("stores the version query name", func() {
360+
Ω(Design.Validate()).ShouldNot(HaveOccurred())
361+
Ω(Design.VersionQueries).Should(Equal([]string{vquery}))
362+
})
363+
})
364+
365+
Context("using VersionHeader to specify duplicates", func() {
366+
const vheader = "X-Api-Version"
367+
BeforeEach(func() {
368+
name = "v1"
369+
dsl = func() {
370+
VersionHeader(vheader)
371+
VersionHeader(vheader)
372+
}
373+
})
374+
375+
It("stores the version header name only once", func() {
376+
Ω(Design.Validate()).ShouldNot(HaveOccurred())
377+
Ω(Design.VersionHeaders).Should(Equal([]string{vheader}))
378+
})
379+
})
380+
381+
Context("using VersionQuery to specify duplicates", func() {
382+
const vquery = "version"
383+
BeforeEach(func() {
384+
name = "v1"
385+
dsl = func() {
386+
VersionQuery(vquery, vquery)
387+
}
388+
})
389+
390+
It("stores the version query name only once", func() {
391+
Ω(Design.Validate()).ShouldNot(HaveOccurred())
392+
Ω(Design.VersionQueries).Should(Equal([]string{vquery}))
393+
})
394+
})
395+
})
396+
397+
var _ = Describe("Version", func() {
398+
var name string
399+
var dsl func()
400+
401+
BeforeEach(func() {
402+
InitDesign()
403+
dslengine.Errors = nil
404+
name = ""
405+
dsl = nil
406+
})
407+
408+
JustBeforeEach(func() {
409+
Version(name, dsl)
410+
dslengine.Run()
411+
})
412+
413+
Context("with no DSL", func() {
414+
BeforeEach(func() {
415+
name = "v1"
416+
})
417+
418+
It("produces a valid version definition", func() {
419+
Ω(Design.Validate()).ShouldNot(HaveOccurred())
420+
Ω(Design.Versions()).Should(HaveLen(1))
421+
Ω(Design.Versions()[0]).Should(Equal(name))
422+
})
423+
})
318424
})

design/validation.go

+18
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ func (a *APIDefinition) Validate() error {
7171
a.validateContact(verr)
7272
a.validateLicense(verr)
7373
a.validateDocs(verr)
74+
a.validateVersionParams(verr)
7475

7576
a.IterateVersions(func(ver *APIVersionDefinition) error {
7677
var allRoutes []*routeInfo
@@ -179,6 +180,23 @@ func (a *APIDefinition) validateDocs(verr *dslengine.ValidationErrors) {
179180
}
180181
}
181182

183+
func (a *APIDefinition) validateVersionParams(verr *dslengine.ValidationErrors) {
184+
bwcs := ExtractWildcards(a.BasePath)
185+
for _, param := range a.VersionParams {
186+
found := false
187+
for _, wc := range bwcs {
188+
if wc == param {
189+
found = true
190+
break
191+
}
192+
}
193+
if !found {
194+
verr.Add(a, "invalid version param, %s is not an API base path param (base path is %#v)",
195+
param, a.BasePath)
196+
}
197+
}
198+
}
199+
182200
// Validate tests whether the resource definition is consistent: action names are valid and each action is
183201
// valid.
184202
func (r *ResourceDefinition) Validate(version *APIVersionDefinition) *dslengine.ValidationErrors {

doc.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ The definitions of the Bottle and UpdateBottlePayload data structures are ommitt
6969
Controllers
7070
7171
There is one controller interface generated per resource defined via the design language. The
72-
interface exposes the controller actions as well as methods to set controller specific middleware
73-
and error handlers (see below). User code must provide data structures that implement these
72+
interface exposes the controller actions. User code must provide data structures that implement these
7473
interfaces when mounting a controller onto a service. The controller data structure should include
7574
an anonymous field of type *goa.Controller which takes care of implementing the middleware and
7675
error handler handling.
@@ -113,5 +112,12 @@ input (Consumes) and output (Produces). goagen uses that information to registed
113112
packages with the service encoders and decoders via the SetEncoder and SetDecoder methods. The
114113
service exposes the Decode, DecodeRequest, Encode and EncodeResponse that implement a simple content
115114
type negotiation algorithm for picking the right encoder for the "Accept" request header.
115+
116+
Versioning
117+
118+
The VersionMux interface implemented by the RootMux struct exposes methods used by the generated
119+
code to setup the routing to versioned endpoints. The DSL defines how the API handles versioning:
120+
via request path, header, querystring or a combination. The generated code uses the VersionMux
121+
interface to setup the root mux accordingly.
116122
*/
117123
package goa

goagen/gen_app/generator.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ func (g *Generator) generateControllers(verdir string, version *design.APIVersio
328328
if !r.SupportsVersion(version.Version) {
329329
return nil
330330
}
331-
data := &ControllerTemplateData{Resource: codegen.Goify(r.Name, true)}
331+
data := &ControllerTemplateData{API: design.Design, Resource: codegen.Goify(r.Name, true)}
332332
err := r.IterateActions(func(a *design.ActionDefinition) error {
333333
context := fmt.Sprintf("%s%sContext", codegen.Goify(a.Name, true), codegen.Goify(r.Name, true))
334334
unmarshal := fmt.Sprintf("unmarshal%s%sPayload", codegen.Goify(a.Name, true), codegen.Goify(r.Name, true))

goagen/gen_app/generator_test.go

+16-7
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,19 @@ import (
430430
"net/http"
431431
)
432432
433+
// inited is true if initService has been called
434+
var inited = false
435+
436+
// initService sets up the service encoders, decoders and mux.
437+
func initService(service *goa.Service) {
438+
if inited {
439+
return
440+
}
441+
inited = true
442+
// Setup encoders and decoders
443+
444+
}
445+
433446
// WidgetController is the controller interface for the Widget actions.
434447
type WidgetController interface {
435448
goa.Muxer
@@ -438,17 +451,15 @@ type WidgetController interface {
438451
439452
// MountWidgetController "mounts" a Widget resource controller on the given service.
440453
func MountWidgetController(service *goa.Service, ctrl WidgetController) {
441-
// Setup encoders and decoders. This is idempotent and is done by each MountXXX function.
442-
443-
// Setup endpoint handler
454+
initService(service)
444455
var h goa.Handler
445456
mux := service.{{if .version}}Version("{{.version}}").Mux{{else}}Mux{{end}}
446457
h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
447458
rctx, err := NewGetWidgetContext(ctx)
448459
if err != nil {
449460
return goa.NewBadRequestError(err)
450461
}{{if .version}}
451-
rctx.APIVersion = service.Version("{{.version}}").VersionName{{end}}
462+
rctx.APIVersion = "{{.version}}"{{end}}
452463
return ctrl.Get(rctx)
453464
}
454465
mux.Handle("GET", "/:id", ctrl.MuxHandler("Get", h, nil))
@@ -494,9 +505,7 @@ package app
494505
const controllersSlicePayloadCode = `
495506
// MountWidgetController "mounts" a Widget resource controller on the given service.
496507
func MountWidgetController(service *goa.Service, ctrl WidgetController) {
497-
// Setup encoders and decoders. This is idempotent and is done by each MountXXX function.
498-
499-
// Setup endpoint handler
508+
initService(service)
500509
var h goa.Handler
501510
mux := service.Mux
502511
h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {

0 commit comments

Comments
 (0)