From c74196c6d71dd6afaaeda7e230916e0bd51a555e Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 18 Nov 2018 02:41:24 +0200 Subject: [PATCH] finalize the API Former-commit-id: e680a9fc517c02eca66f83e42519c9820122fae8 --- versioning/README.md | 17 ++------ versioning/deprecation.go | 10 +++++ versioning/group.go | 80 +++++++++++++++-------------------- versioning/version.go | 28 ++++++++++-- versioning/versioning.go | 12 ++++++ versioning/versioning_test.go | 41 ++++++++++++------ 6 files changed, 114 insertions(+), 74 deletions(-) diff --git a/versioning/README.md b/versioning/README.md index e26fe07b..07af97ba 100644 --- a/versioning/README.md +++ b/versioning/README.md @@ -90,7 +90,7 @@ This will make the handler to send these headers to the client: Grouping routes by version is possible as well. Using the `versioning.NewGroup(version string) *versioning.Group` function you can create a group to register your versioned routes. -The `versioning.RegisterGroups(r iris.Party, groups ...*versioning.Group)` must be called in the end in order to register the routes to a specific `Party`. +The `versioning.RegisterGroups(r iris.Party, versionNotFoundHandler iris.Handler, groups ...*versioning.Group)` must be called in the end in order to register the routes to a specific `Party`. ```go app := iris.New() @@ -106,9 +106,11 @@ userAPIV2.Get("/", sendHandler(v2Response)) userAPIV2.Post("/", sendHandler(v2Response)) userAPIV2.Put("/other", sendHandler(v2Response)) -versioning.RegisterGroups(userAPI, userAPIV10, userAPIV2) +versioning.RegisterGroups(userAPI, versioning.NotFoundHandler, userAPIV10, userAPIV2) ``` +> A middleware can be registered to the actual `iris.Party` only, using the methods we learnt above, i.e by using the `versioning.Match` in order to detect what code/handler you want to be executed when "x" or no version is requested. + ### Deprecation for Group Just call the `Deprecated(versioning.DeprecationOptions)` on the group you want to notify your API consumers that this specific version is deprecated. @@ -117,17 +119,6 @@ Just call the `Deprecated(versioning.DeprecationOptions)` on the group you want userAPIV10 := versioning.NewGroup("1.0").Deprecated(versioning.DefaultDeprecationOptions) ``` -### Version not found for Groups - -In order to register a custom version not found handler you have to use the `versioning.Concat` first, which gives you the API to add a version not found handler. - -```go -versioning.Concat(userAPIV10, userAPIV2).NotFound(func(ctx iris.Context) { - ctx.StatusCode(iris.StatusNotFound) - ctx.Writef("unknown version %s", versioning.GetVersion(ctx)) -}).For(userAPI) -``` - ## Compare version manually from inside your handlers ```go diff --git a/versioning/deprecation.go b/versioning/deprecation.go index 3d8b5363..c7b8cd06 100644 --- a/versioning/deprecation.go +++ b/versioning/deprecation.go @@ -6,20 +6,30 @@ import ( "github.com/kataras/iris/context" ) +// DeprecationOptions describes the deprecation headers key-values. +// - "X-API-Warn": options.WarnMessage +// - "X-API-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate)) +// - "X-API-Deprecation-Info": options.DeprecationInfo type DeprecationOptions struct { WarnMessage string DeprecationDate time.Time DeprecationInfo string } +// ShouldHandle reports whether the deprecation headers should be present or no. func (opts DeprecationOptions) ShouldHandle() bool { return opts.WarnMessage != "" || !opts.DeprecationDate.IsZero() || opts.DeprecationInfo != "" } +// DefaultDeprecationOptions are the default deprecation options, +// it defaults the "X-API-Warn" header to a generic message. var DefaultDeprecationOptions = DeprecationOptions{ WarnMessage: "WARNING! You are using a deprecated version of this API.", } +// Deprecated marks a specific handler as a deprecated. +// Deprecated can be used to tell the clients that +// a newer version of that specific resource is available instead. func Deprecated(handler context.Handler, options DeprecationOptions) context.Handler { if options.WarnMessage == "" { options.WarnMessage = DefaultDeprecationOptions.WarnMessage diff --git a/versioning/group.go b/versioning/group.go index 77a23666..3ee4ef4c 100644 --- a/versioning/group.go +++ b/versioning/group.go @@ -1,14 +1,12 @@ package versioning import ( + "net/http" + "github.com/kataras/iris/context" "github.com/kataras/iris/core/router" ) -func RegisterGroups(r router.Party, groups ...*Group) []*router.Route { - return Concat(groups...).For(r) -} - type ( vroute struct { method string @@ -16,6 +14,8 @@ type ( versions Map } + // Group is a group of version-based routes. + // One version per one or more routes. Group struct { version string extraMethods []string @@ -25,6 +25,9 @@ type ( } ) +// NewGroup returns a ptr to Group based on the given "version". +// +// See `Handle` and `RegisterGroups` for more. func NewGroup(version string) *Group { return &Group{ version: version, @@ -33,7 +36,7 @@ func NewGroup(version string) *Group { // Deprecated marks this group and all its versioned routes // as deprecated versions of that endpoint. -// It can be called in the end just before `Concat` or `RegisterGroups` +// It can be called in the end just before `RegisterGroups` // or first by `NewGroup(...).Deprecated(...)`. It returns itself. func (g *Group) Deprecated(options DeprecationOptions) *Group { // if `Deprecated` is called in the end. @@ -47,13 +50,16 @@ func (g *Group) Deprecated(options DeprecationOptions) *Group { return g } +// AllowMethods can be called before `Handle/Get/Post...` +// to tell the underline router that all routes should be registered +// to these "methods" as well. func (g *Group) AllowMethods(methods ...string) *Group { g.extraMethods = append(g.extraMethods, methods...) return g } func (g *Group) addVRoute(method, path string, handler context.Handler) { - for _, r := range g.routes { // check if already exists. + for _, r := range g.routes { // check if route already exists. if r.method == method && r.path == path { return } @@ -67,8 +73,10 @@ func (g *Group) addVRoute(method, path string, handler context.Handler) { } // Handle registers a versioned route to the group. +// A call of `RegisterGroups` is necessary in order to register the actual routes +// when the group is complete. // -// See `Concat` and `RegisterGroups` for more. +// `RegisterGroups` for more. func (g *Group) Handle(method string, path string, handler context.Handler) { if g.deprecation.ShouldHandle() { // if `Deprecated` called first. handler = Deprecated(handler, g.deprecation) @@ -89,47 +97,47 @@ func (g *Group) None(path string, handler context.Handler) { // Get registers a versioned route for the Get http method. func (g *Group) Get(path string, handler context.Handler) { - g.Handle("GET", path, handler) + g.Handle(http.MethodGet, path, handler) } // Post registers a versioned route for the Post http method. func (g *Group) Post(path string, handler context.Handler) { - g.Handle("POST", path, handler) + g.Handle(http.MethodPost, path, handler) } // Put registers a versioned route for the Put http method func (g *Group) Put(path string, handler context.Handler) { - g.Handle("PUT", path, handler) + g.Handle(http.MethodPut, path, handler) } // Delete registers a versioned route for the Delete http method. func (g *Group) Delete(path string, handler context.Handler) { - g.Handle("DELETE", path, handler) + g.Handle(http.MethodDelete, path, handler) } // Connect registers a versioned route for the Connect http method. func (g *Group) Connect(path string, handler context.Handler) { - g.Handle("CONNECT", path, handler) + g.Handle(http.MethodConnect, path, handler) } // Head registers a versioned route for the Head http method. func (g *Group) Head(path string, handler context.Handler) { - g.Handle("HEAD", path, handler) + g.Handle(http.MethodHead, path, handler) } // Options registers a versioned route for the Options http method. func (g *Group) Options(path string, handler context.Handler) { - g.Handle("OPTIONS", path, handler) + g.Handle(http.MethodOptions, path, handler) } // Patch registers a versioned route for the Patch http method. func (g *Group) Patch(path string, handler context.Handler) { - g.Handle("PATCH", path, handler) + g.Handle(http.MethodPatch, path, handler) } // Trace registers a versioned route for the Trace http method. func (g *Group) Trace(path string, handler context.Handler) { - g.Handle("TRACE", path, handler) + g.Handle(http.MethodTrace, path, handler) } // Any registers a versioned route for ALL of the http methods @@ -146,49 +154,31 @@ func (g *Group) Any(registeredPath string, handler context.Handler) { g.Trace(registeredPath, handler) } -type Groups struct { - routes []vroute - - notFoundHandler context.Handler -} - -func Concat(groups ...*Group) *Groups { +// RegisterGroups registers one or more groups to an `iris.Party` or to the root router. +// See `NewGroup` and `NotFoundHandler` too. +func RegisterGroups(r router.Party, notFoundHandler context.Handler, groups ...*Group) (actualRoutes []*router.Route) { var total []vroute - for _, g := range groups { inner: for _, r := range g.routes { for i, tr := range total { if tr.method == r.method && tr.path == r.path { - for k, v := range r.versions { - total[i].versions[k] = v - } + total[i].versions[g.version] = r.versions[g.version] continue inner } } - } - total = append(total, g.routes...) + total = append(total, r) + } } - return &Groups{total, NotFoundHandler} -} - -func (g *Groups) NotFound(handler context.Handler) *Groups { - g.notFoundHandler = handler - return g -} - -func (g *Groups) For(r router.Party) (totalRoutesRegistered []*router.Route) { - for _, vr := range g.routes { - if g.notFoundHandler != nil { - vr.versions[NotFound] = g.notFoundHandler + for _, vr := range total { + if notFoundHandler != nil { + vr.versions[NotFound] = notFoundHandler } - // fmt.Printf("Method: %s | Path: %s | Versions: %#+v\n", vr.method, vr.path, vr.versions) - route := r.Handle(vr.method, vr.path, - NewMatcher(vr.versions)) - totalRoutesRegistered = append(totalRoutesRegistered, route) + route := r.Handle(vr.method, vr.path, NewMatcher(vr.versions)) + actualRoutes = append(actualRoutes, route) } return diff --git a/versioning/version.go b/versioning/version.go index 3de1f865..75d814ba 100644 --- a/versioning/version.go +++ b/versioning/version.go @@ -7,14 +7,28 @@ import ( ) const ( - AcceptVersionHeaderKey = "Accept-Version" - AcceptHeaderKey = "Accept" + // AcceptVersionHeaderKey is the header key of "Accept-Version". + AcceptVersionHeaderKey = "Accept-Version" + // AcceptHeaderKey is the header key of "Accept". + AcceptHeaderKey = "Accept" + // AcceptHeaderVersionValue is the Accept's header value search term the requested version. AcceptHeaderVersionValue = "version" - Key = "iris.api.version" // for use inside the ctx.Values(), not visible by the user. + // Key is the context key of the version, can be used to manually modify the "requested" version. + // Example of how you can change the default behavior to extract a requested version (which is by headers) + // from a "version" url parameter instead: + // func(ctx iris.Context) { // &version=1 + // ctx.Values().Set(versioning.Key, ctx.URLParamDefault("version", "1")) + // ctx.Next() + // } + Key = "iris.api.version" // for use inside the ctx.Values(), not visible by the user. + // NotFound is the key that can be used inside a `Map` or inside `ctx.Values().Set(versioning.Key, versioning.NotFound)` + // to tell that a version wasn't found, therefore the not found handler should handle the request instead. NotFound = Key + ".notfound" ) +// NotFoundHandler is the default version not found handler that +// is executed from `NewMatcher` when no version is registered as available to dispatch a resource. var NotFoundHandler = func(ctx context.Context) { // 303 is an option too, // end-dev has the chance to change that behavior by using the NotFound in the map: @@ -32,6 +46,14 @@ var NotFoundHandler = func(ctx context.Context) { ctx.WriteString("version not found") } +// GetVersion returns the current request version. +// +// By default the `GetVersion` will try to read from: +// - "Accept" header, i.e Accept: "application/json; version=1.0" +// - "Accept-Version" header, i.e Accept-Version: "1.0" +// +// However, the end developer can also set a custom version for a handler via a middleware by using the context's store key +// for versions (see `Key` for further details on that). func GetVersion(ctx context.Context) string { // firstly by context store, if manually set-ed by a middleware. if version := ctx.Values().GetString(Key); version != "" { diff --git a/versioning/versioning.go b/versioning/versioning.go index 5e2b8d84..ca280d1e 100644 --- a/versioning/versioning.go +++ b/versioning/versioning.go @@ -6,6 +6,8 @@ import ( "github.com/hashicorp/go-version" ) +// If reports whether the "version" is matching to the "is". +// the "is" can be a constraint like ">= 1, < 3". func If(v string, is string) bool { ver, err := version.NewVersion(v) if err != nil { @@ -20,12 +22,22 @@ func If(v string, is string) bool { return constraints.Check(ver) } +// Match acts exactly the same as `If` does but instead it accepts +// a Context, so it can be called by a handler to determinate the requested version. func Match(ctx context.Context, expectedVersion string) bool { return If(GetVersion(ctx), expectedVersion) } +// Map is a map of versions targets to a handlers, +// a handler per version or constraint, the key can be something like ">1, <=2" or just "1". type Map map[string]context.Handler +// NewMatcher creates a single handler which decides what handler +// should be executed based on the requested version. +// +// Use the `NewGroup` if you want to add many routes under a specific version. +// +// See `Map` and `NewGroup` too. func NewMatcher(versions Map) context.Handler { constraintsHandlers, notFoundHandler := buildConstraints(versions) diff --git a/versioning/versioning_test.go b/versioning/versioning_test.go index 84e27472..4f7d5600 100644 --- a/versioning/versioning_test.go +++ b/versioning/versioning_test.go @@ -79,25 +79,40 @@ func TestNewGroup(t *testing.T) { // [... static serving, middlewares and etc goes here]. userAPIV10 := versioning.NewGroup("1.0").Deprecated(versioning.DefaultDeprecationOptions) - userAPIV10.Get("/", sendHandler(v10Response)) + // V10middlewareResponse := "m1" + // userAPIV10.Use(func(ctx iris.Context) { + // println("exec userAPIV10.Use - midl1") + // sendHandler(V10middlewareResponse)(ctx) + // ctx.Next() + // }) + // userAPIV10.Use(func(ctx iris.Context) { + // println("exec userAPIV10.Use - midl2") + // sendHandler(V10middlewareResponse + "midl2")(ctx) + // ctx.Next() + // }) + // userAPIV10.Use(func(ctx iris.Context) { + // println("exec userAPIV10.Use - midl3") + // ctx.Next() + // }) + userAPIV10.Get("/", sendHandler(v10Response)) userAPIV2 := versioning.NewGroup(">= 2, < 3") + // V2middlewareResponse := "m2" + // userAPIV2.Use(func(ctx iris.Context) { + // println("exec userAPIV2.Use - midl1") + // sendHandler(V2middlewareResponse)(ctx) + // ctx.Next() + // }) + // userAPIV2.Use(func(ctx iris.Context) { + // println("exec userAPIV2.Use - midl2") + // ctx.Next() + // }) + userAPIV2.Get("/", sendHandler(v2Response)) userAPIV2.Post("/", sendHandler(v2Response)) userAPIV2.Put("/other", sendHandler(v2Response)) - // versioning.Concat(userAPIV10, userAPIV2). - // NotFound(func(ctx iris.Context) { - // ctx.StatusCode(iris.StatusNotFound) - // ctx.Writef("unknown version %s", versioning.GetVersion(ctx)) - // }). - // For(userAPI) - // This is legal too: - // For(app.PartyFunc("/api/user", func(r iris.Party) { - // // [... static serving, middlewares and etc goes here]. - // })) - - versioning.RegisterGroups(userAPI, userAPIV10, userAPIV2) + versioning.RegisterGroups(userAPI, versioning.NotFoundHandler, userAPIV10, userAPIV2) e := httptest.New(t, app)