Skip to content

Commit 124d641

Browse files
committedFeb 25, 2016
Add DSL for defining param, header and querystring based versioning
And generate code that sets up the service mux accordingly.
1 parent 3a1f258 commit 124d641

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 {

‎goagen/gen_app/writers.go

+53-7
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ type (
9090

9191
// ControllerTemplateData contains the information required to generate an action handler.
9292
ControllerTemplateData struct {
93+
API *design.APIDefinition // API definition
9394
Resource string // Lower case plural resource name, e.g. "bottles"
9495
Actions []map[string]interface{} // Array of actions, each action has keys "Name", "Routes", "Context" and "Unmarshal"
9596
Version *design.APIVersionDefinition // Controller API version
@@ -255,6 +256,13 @@ func NewControllersWriter(filename string) (*ControllersWriter, error) {
255256

256257
// Execute writes the handlers GoGenerator
257258
func (w *ControllersWriter) Execute(data []*ControllerTemplateData) error {
259+
if len(data) == 0 {
260+
return nil
261+
}
262+
fn := template.FuncMap{"versionFuncs": versionFuncs}
263+
if err := w.ExecuteTemplate("service", serviceT, fn, data[0]); err != nil {
264+
return err
265+
}
258266
for _, d := range data {
259267
if err := w.ExecuteTemplate("controller", ctrlT, nil, d); err != nil {
260268
return err
@@ -378,6 +386,22 @@ func hasAPIVersion(params *design.AttributeDefinition) bool {
378386
return false
379387
}
380388

389+
// versionFuncs returns an array of code snippets that invoke the select version func corresponding
390+
// to the version params, headers and querystrings defined in the design.
391+
func versionFuncs(api *design.APIDefinition) []string {
392+
var funcs []string
393+
for _, param := range api.VersionParams {
394+
funcs = append(funcs, fmt.Sprintf(`goa.PathSelectVersionFunc("%s", "%s")`, api.BasePath, param))
395+
}
396+
for _, header := range api.VersionHeaders {
397+
funcs = append(funcs, fmt.Sprintf(`goa.HeaderSelectVersionFunc("%s")`, header))
398+
}
399+
for _, query := range api.VersionQueries {
400+
funcs = append(funcs, fmt.Sprintf(`goa.QuerySelectVersionFunc("%s")`, query))
401+
}
402+
return funcs
403+
}
404+
381405
const (
382406
// ctxT generates the code for the context data type.
383407
// template input: *ContextTemplateData
@@ -538,26 +562,48 @@ type {{.Resource}}Controller interface {
538562
{{end}}}
539563
`
540564

541-
// mountT generates the code for a resource "Mount" function.
565+
// serviceT generates the service initialization code.
542566
// template input: *ControllerTemplateData
543-
mountT = `
544-
// Mount{{.Resource}}Controller "mounts" a {{.Resource}} resource controller on the given service.
545-
func Mount{{.Resource}}Controller(service *goa.Service, ctrl {{.Resource}}Controller) {
546-
// Setup encoders and decoders. This is idempotent and is done by each MountXXX function.
567+
serviceT = `
568+
// inited is true if initService has been called
569+
var inited = false
570+
571+
// initService sets up the service encoders, decoders and mux.
572+
func initService(service *goa.Service) {
573+
if inited {
574+
return
575+
}
576+
inited = true
577+
// Setup encoders and decoders
547578
{{range .EncoderMap}}{{$tmp := tempvar}}{{/*
548579
*/}} service.{{if not $.Version.IsDefault}}Version("{{$.Version.Version}}").{{end}}SetEncoder({{.PackageName}}.{{.Factory}}(), {{.Default}}, "{{join .MIMETypes "\", \""}}")
549580
{{end}}{{range .DecoderMap}}{{$tmp := tempvar}}{{/*
550581
*/}} service.{{if not $.Version.IsDefault}}Version("{{$.Version.Version}}").{{end}}SetDecoder({{.PackageName}}.{{.Factory}}(), {{.Default}}, "{{join .MIMETypes "\", \""}}")
551582
{{end}}
552-
// Setup endpoint handler
583+
{{if .API.APIVersions}}{{$versionFuncs := versionFuncs .API}}{{if gt (len $versionFuncs) 0}} // Configure mux for versioning.
584+
if mux, ok := service.Mux.(*goa.RootMux); ok {
585+
{{if gt (len $versionFuncs) 1}}{{range $i, $f := $versionFuncs}} func{{$i}} := {{$f}}
586+
{{end}} mux.SelectVersionFunc = goa.CombineSelectVersionFunc({{range $i, $_ := $versionFuncs}}func{{$i}}, {{end}})
587+
{{else}} mux.SelectVersionFunc = {{index $versionFuncs 0}}
588+
{{end}} }
589+
{{end}}{{end}}}
590+
591+
`
592+
593+
// mountT generates the code for a resource "Mount" function.
594+
// template input: *ControllerTemplateData
595+
mountT = `
596+
// Mount{{.Resource}}Controller "mounts" a {{.Resource}} resource controller on the given service.
597+
func Mount{{.Resource}}Controller(service *goa.Service, ctrl {{.Resource}}Controller) {
598+
initService(service)
553599
var h goa.Handler
554600
mux := service.{{if not .Version.IsDefault}}Version("{{.Version.Version}}").Mux{{else}}Mux{{end}}
555601
{{$res := .Resource}}{{$ver := .Version}}{{range .Actions}}{{$action := .}} h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
556602
rctx, err := New{{.Context}}(ctx)
557603
if err != nil {
558604
return goa.NewBadRequestError(err)
559605
}{{if not $ver.IsDefault}}
560-
rctx.APIVersion = service.Version("{{$ver.Version}}").VersionName{{end}}
606+
rctx.APIVersion = "{{$ver.Version}}"{{end}}
561607
{{if .Payload}}if rawPayload := goa.Request(ctx).Payload; rawPayload != nil {
562608
rctx.Payload = rawPayload.({{gotyperef .Payload nil 1}})
563609
}

‎goagen/gen_app/writers_test.go

+100-11
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,65 @@ var _ = Describe("ControllersWriter", func() {
398398
f, _ = os.Create(filename)
399399
})
400400

401+
Context("with a versioned API", func() {
402+
var data []*genapp.ControllerTemplateData
403+
404+
BeforeEach(func() {
405+
version := &design.APIVersionDefinition{Name: "", BasePath: "/api/:vp"}
406+
v1 := &design.APIVersionDefinition{Name: "v1"}
407+
v2 := &design.APIVersionDefinition{Name: "v2"}
408+
versions := map[string]*design.APIVersionDefinition{
409+
"1.0": v1,
410+
"2.0": v2,
411+
}
412+
api := &design.APIDefinition{
413+
APIVersionDefinition: version,
414+
APIVersions: versions,
415+
Resources: nil,
416+
Types: nil,
417+
MediaTypes: nil,
418+
VersionParams: []string{"vp"},
419+
VersionHeaders: []string{"vh"},
420+
VersionQueries: []string{"vq"},
421+
}
422+
encoderMap := map[string]*genapp.EncoderTemplateData{
423+
"github.com/goadesign/goa": &genapp.EncoderTemplateData{
424+
PackagePath: "github.com/goadesign/goa",
425+
PackageName: "goa",
426+
Factory: "NewEncoder",
427+
MIMETypes: []string{"application/json"},
428+
Default: true,
429+
},
430+
}
431+
decoderMap := map[string]*genapp.EncoderTemplateData{
432+
"github.com/goadesign/goa": &genapp.EncoderTemplateData{
433+
PackagePath: "github.com/goadesign/goa",
434+
PackageName: "goa",
435+
Factory: "NewDecoder",
436+
MIMETypes: []string{"application/json"},
437+
Default: true,
438+
},
439+
}
440+
data = []*genapp.ControllerTemplateData{&genapp.ControllerTemplateData{
441+
API: api,
442+
Resource: "resource",
443+
Actions: []map[string]interface{}{},
444+
Version: api.APIVersionDefinition,
445+
EncoderMap: encoderMap,
446+
DecoderMap: decoderMap,
447+
}}
448+
})
449+
450+
It("generates the service initialization code", func() {
451+
err := writer.Execute(data)
452+
Ω(err).ShouldNot(HaveOccurred())
453+
b, err := ioutil.ReadFile(filename)
454+
Ω(err).ShouldNot(HaveOccurred())
455+
written := string(b)
456+
Ω(written).Should(Equal(initController))
457+
})
458+
})
459+
401460
Context("with data", func() {
402461
var actions, verbs, paths, contexts, unmarshals []string
403462
var payloads []*design.UserTypeDefinition
@@ -418,6 +477,7 @@ var _ = Describe("ControllersWriter", func() {
418477

419478
JustBeforeEach(func() {
420479
codegen.TempCount = 0
480+
api := &design.APIDefinition{}
421481
d := &genapp.ControllerTemplateData{
422482
Resource: "Bottles",
423483
Version: &design.APIVersionDefinition{},
@@ -445,6 +505,7 @@ var _ = Describe("ControllersWriter", func() {
445505
}
446506
}
447507
if len(as) > 0 {
508+
d.API = api
448509
d.Actions = as
449510
d.EncoderMap = encoderMap
450511
d.DecoderMap = decoderMap
@@ -1008,16 +1069,48 @@ type BottlesController interface {
10081069
goa.Muxer
10091070
List(*ListBottleContext) error
10101071
}
1072+
`
1073+
1074+
initController = `
1075+
// inited is true if initService has been called
1076+
var inited = false
1077+
1078+
// initService sets up the service encoders, decoders and mux.
1079+
func initService(service *goa.Service) {
1080+
if inited {
1081+
return
1082+
}
1083+
inited = true
1084+
// Setup encoders and decoders
1085+
service.SetEncoder(goa.NewEncoder(), true, "application/json")
1086+
service.SetDecoder(goa.NewDecoder(), true, "application/json")
1087+
1088+
// Configure mux for versioning.
1089+
if mux, ok := service.Mux.(*goa.RootMux); ok {
1090+
func0 := goa.PathSelectVersionFunc("/api/:vp", "vp")
1091+
func1 := goa.HeaderSelectVersionFunc("vh")
1092+
func2 := goa.QuerySelectVersionFunc("vq")
1093+
mux.SelectVersionFunc = goa.CombineSelectVersionFunc(func0, func1, func2, )
1094+
}
1095+
}
1096+
1097+
// resourceController is the controller interface for the resource actions.
1098+
type resourceController interface {
1099+
goa.Muxer
1100+
}
1101+
1102+
// MountresourceController "mounts" a resource resource controller on the given service.
1103+
func MountresourceController(service *goa.Service, ctrl resourceController) {
1104+
initService(service)
1105+
var h goa.Handler
1106+
mux := service.Mux
1107+
}
10111108
`
10121109

10131110
encoderController = `
10141111
// MountBottlesController "mounts" a Bottles resource controller on the given service.
10151112
func MountBottlesController(service *goa.Service, ctrl BottlesController) {
1016-
// Setup encoders and decoders. This is idempotent and is done by each MountXXX function.
1017-
service.SetEncoder(goa.JSONEncoderFactory(), false, "application/json")
1018-
service.SetDecoder(goa.JSONDecoderFactory(), false, "application/json")
1019-
1020-
// Setup endpoint handler
1113+
initService(service)
10211114
var h goa.Handler
10221115
mux := service.Mux
10231116
h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
@@ -1033,9 +1126,7 @@ func MountBottlesController(service *goa.Service, ctrl BottlesController) {
10331126
`
10341127

10351128
simpleMount = `func MountBottlesController(service *goa.Service, ctrl BottlesController) {
1036-
// Setup encoders and decoders. This is idempotent and is done by each MountXXX function.
1037-
1038-
// Setup endpoint handler
1129+
initService(service)
10391130
var h goa.Handler
10401131
mux := service.Mux
10411132
h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
@@ -1059,9 +1150,7 @@ type BottlesController interface {
10591150
`
10601151

10611152
multiMount = `func MountBottlesController(service *goa.Service, ctrl BottlesController) {
1062-
// Setup encoders and decoders. This is idempotent and is done by each MountXXX function.
1063-
1064-
// Setup endpoint handler
1153+
initService(service)
10651154
var h goa.Handler
10661155
mux := service.Mux
10671156
h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {

‎mux.go

+31-14
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net/http"
66
"net/url"
77
"regexp"
8+
"strings"
89

910
"golang.org/x/net/context"
1011

@@ -77,21 +78,30 @@ func NewMux(service *Service) *RootMux {
7778
}
7879
}
7980

80-
// PathSelectVersionFunc returns a SelectVersionFunc that uses the given path pattern to extract the
81-
// version from the request path. Use the same path pattern given in the DSL to define the API base
82-
// path, e.g. "/api/:api_version".
83-
// If the pattern matches zeroVersion then the empty version is returned (i.e. the unversioned
84-
// controller handles the request).
85-
func PathSelectVersionFunc(pattern, zeroVersion string) SelectVersionFunc {
86-
rgs := design.WildcardRegex.ReplaceAllLiteralString(pattern, `/([^/]+)`)
81+
// PathSelectVersionFunc returns a SelectVersionFunc that uses the given path pattern and param to
82+
// extract the version from the request path. Use the same path pattern given in the DSL to define
83+
// the API base path, e.g. "/api/:api_version".
84+
func PathSelectVersionFunc(pattern, param string) (SelectVersionFunc, error) {
85+
params := design.ExtractWildcards(pattern)
86+
index := -1
87+
for i, p := range params {
88+
if p == param {
89+
index = i
90+
break
91+
}
92+
}
93+
if index == -1 {
94+
return nil, fmt.Errorf("Mux versioning setup: no param %s in pattern %s", param, pattern)
95+
}
96+
rgs := strings.Replace(pattern, ":"+param, `([^/]+)`, 1)
8797
rg := regexp.MustCompile("^" + rgs)
8898
return func(req *http.Request) (version string) {
8999
match := rg.FindStringSubmatch(req.URL.Path)
90-
if len(match) > 1 && match[1] != zeroVersion {
100+
if len(match) > 1 {
91101
version = match[1]
92102
}
93103
return
94-
}
104+
}, nil
95105
}
96106

97107
// HeaderSelectVersionFunc returns a SelectVersionFunc that looks for the version in the header with
@@ -113,13 +123,20 @@ func QuerySelectVersionFunc(query string) SelectVersionFunc {
113123
// CombineSelectVersionFunc returns a SelectVersionFunc that tries each func passed as argument
114124
// in order and returns the first non-empty string version.
115125
func CombineSelectVersionFunc(funcs ...SelectVersionFunc) SelectVersionFunc {
116-
return func(req *http.Request) string {
117-
for _, f := range funcs {
118-
if version := f(req); version != "" {
119-
return version
126+
switch len(funcs) {
127+
case 0:
128+
return nil
129+
case 1:
130+
return funcs[0]
131+
default:
132+
return func(req *http.Request) string {
133+
for _, f := range funcs {
134+
if version := f(req); version != "" {
135+
return version
136+
}
120137
}
138+
return ""
121139
}
122-
return ""
123140
}
124141
}
125142

‎mux_test.go

+6-16
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,23 @@ import (
99
)
1010

1111
var _ = Describe("PathSelectVersionFunc", func() {
12-
var pattern, zeroVersion string
12+
var pattern, param string
1313
var request *http.Request
1414

1515
var fn goa.SelectVersionFunc
1616
var version string
1717

1818
JustBeforeEach(func() {
19-
fn = goa.PathSelectVersionFunc(pattern, zeroVersion)
19+
var err error
20+
fn, err = goa.PathSelectVersionFunc(pattern, param)
21+
Ω(err).ShouldNot(HaveOccurred())
2022
version = fn(request)
2123
})
2224

23-
Context("using the default settings", func() {
25+
Context("using path versioning", func() {
2426
BeforeEach(func() {
2527
pattern = "/:version/"
26-
zeroVersion = "api"
28+
param = "version"
2729
})
2830

2931
Context("and a versioned request", func() {
@@ -37,18 +39,6 @@ var _ = Describe("PathSelectVersionFunc", func() {
3739
Ω(version).Should(Equal("v1"))
3840
})
3941
})
40-
41-
Context("and an unversioned request", func() {
42-
BeforeEach(func() {
43-
var err error
44-
request, err = http.NewRequest("GET", "/api/foo", nil)
45-
Ω(err).ShouldNot(HaveOccurred())
46-
})
47-
48-
It("routes to the unversioned controller", func() {
49-
Ω(version).Should(Equal(""))
50-
})
51-
})
5242
})
5343

5444
})

0 commit comments

Comments
 (0)
Please sign in to comment.