diff --git a/versioning/README.md b/versioning/README.md new file mode 100644 index 00000000..3e854d95 --- /dev/null +++ b/versioning/README.md @@ -0,0 +1,86 @@ +# Versioning + +The [versioning](https://github.com/kataras/iris/tree/master/versioning) package provides [semver](https://semver.org/) versioning for your APIs. It implements all the suggestions written at [api-guidelines](https://github.com/byrondover/api-guidelines/blob/master/Guidelines.md#versioning) and more. + + +The version comparison is done by the [go-version](https://github.com/hashicorp/go-version) package. It supports matching over patterns like `">= 1.0, < 3"` and etc. + +## Features + +- per route version matching, a normal iris handler with "switch" cases via Map for version => handler +- per group versioned routes and deprecation API +- version matching like ">= 1.0, < 2.0" or just "2.0.1" and etc. +- version not found handler (can be customized by simply adding the versioning.NotFound: customNotMatchVersionHandler on the Map) +- version is retrieved from the "Accept" and "Accept-Version" headers (can be customized via middleware) +- respond with "X-API-Version" header, if version found. +- deprecation options with customizable "X-API-Warn", "X-API-Deprecation-Date", "X-API-Deprecation-Info" headers via `Deprecated` wrapper. + +## Get version + +Current request version is retrieved by `versioning.GetVersion(ctx)`. + +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"` + +You can also set a custom version for a handler via a middleware by using the context's store values. +For example: +```go +func(ctx iris.Context) { + ctx.Values().Set(versioning.Key, ctx.URLParamDefault("version", "1.0")) + ctx.Next() +} +``` + +## Match version to handler + +The `versioning.NewMatcher(versioning.Map) iris.Handler` creates a single handler which decides what handler need to be executed based on the requested version. + +```go +app := iris.New() + +// middleware for all versions. +myMiddleware := func(ctx iris.Context) { + // [...] + ctx.Next() +} + +myCustomNotVersionFound := func(ctx iris.Context) { + ctx.StatusCode(404) + ctx.Writef("%s version not found", versioning.GetVersion(ctx)) +} + +userAPI := app.Party("/api/user") +userAPI.Get("/", myMiddleware, versioning.NewMatcher(versioning.Map{ + "1.0": sendHandler(v10Response), + ">= 2, < 3": sendHandler(v2Response), + versioning.NotFound: myCustomNotVersionFound, +})) +``` + +## Grouping versioned routes + +Impl & tests done, example not. **TODO** + +## Compare version manually from inside your handlers + +```go +// reports if the "version" is matching to the "is". +// the "is" can be a constraint like ">= 1, < 3". +If(version string, is string) bool +``` + +```go +// same as `If` but expects a Context to read the requested version. +Match(ctx iris.Context, expectedVersion string) bool +``` + +```go +app.Get("/api/user", func(ctx iris.Context) { + if versioning.Match(ctx, ">= 2.2.3") { + // [logic for >= 2.2.3 version of your handler goes here] + return + } +}) +``` + diff --git a/versioning/versioning.go b/versioning/versioning.go index 90d976c0..5e2b8d84 100644 --- a/versioning/versioning.go +++ b/versioning/versioning.go @@ -6,6 +6,24 @@ import ( "github.com/hashicorp/go-version" ) +func If(v string, is string) bool { + ver, err := version.NewVersion(v) + if err != nil { + return false + } + + constraints, err := version.NewConstraint(is) + if err != nil { + return false + } + + return constraints.Check(ver) +} + +func Match(ctx context.Context, expectedVersion string) bool { + return If(GetVersion(ctx), expectedVersion) +} + type Map map[string]context.Handler func NewMatcher(versions Map) context.Handler { @@ -18,15 +36,15 @@ func NewMatcher(versions Map) context.Handler { return } - version, err := version.NewVersion(versionString) + ver, err := version.NewVersion(versionString) if err != nil { notFoundHandler(ctx) return } for _, ch := range constraintsHandlers { - if ch.constraints.Check(version) { - ctx.Header("X-API-Version", version.String()) + if ch.constraints.Check(ver) { + ctx.Header("X-API-Version", ver.String()) ch.handler(ctx) return } diff --git a/versioning/versioning_test.go b/versioning/versioning_test.go index 06a205d2..ea31b344 100644 --- a/versioning/versioning_test.go +++ b/versioning/versioning_test.go @@ -23,6 +23,14 @@ func sendHandler(contents string) iris.Handler { } } +func TestIf(t *testing.T) { + if expected, got := true, versioning.If("1.0", ">=1"); expected != got { + t.Fatalf("expected %s to be %s", "1.0", ">= 1") + } + if expected, got := true, versioning.If("1.2.3", "> 1.2"); expected != got { + t.Fatalf("expected %s to be %s", "1.2.3", "> 1.2") + } +} func TestNewMatcher(t *testing.T) { app := iris.New() @@ -33,6 +41,17 @@ func TestNewMatcher(t *testing.T) { versioning.NotFound: notFoundHandler, })) + // middleware as usual. + myMiddleware := func(ctx iris.Context) { + ctx.Header("X-Custom", "something") + ctx.Next() + } + myVersions := versioning.Map{ + "1.0": sendHandler(v10Response), + } + + userAPI.Get("/with_middleware", myMiddleware, versioning.NewMatcher(myVersions)) + e := httptest.New(t, app) e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect(). @@ -44,32 +63,17 @@ func TestNewMatcher(t *testing.T) { e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect(). Status(iris.StatusOK).Body().Equal(v2Response) + // middleware as usual. + ex := e.GET("/api/user/with_middleware").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect() + ex.Status(iris.StatusOK).Body().Equal(v10Response) + ex.Header("X-Custom").Equal("something") + e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect(). Status(iris.StatusNotFound).Body().Equal("Not Found") } func TestNewGroup(t *testing.T) { app := iris.New() - // userAPI := app.Party("/api/user") - - // userAPIV10 := versioning.NewGroup("1.0", userAPI) - // userAPIV10.Get("/", sendHandler(v10Response)) - // userAPIV2 := versioning.NewGroup(">= 2, < 3", userAPI) - // userAPIV2.Get("/", sendHandler(v2Response)) - - // --- - - // userAPI := app.Party("/api/user") - // userVAPI := versioning.NewGroup(userAPI) - // userAPIV10 := userVAPI.Version("1.0") - // userAPIV10.Get("/", sendHandler(v10Response)) - - // userAPIV10 := userVAPI.Version("2.0") - // userAPIV10.Get("/", sendHandler(v10Response)) - // userVAPI.NotFound(...) - // userVAPI.Build() - - // -- userAPI := app.Party("/api/user") // [... static serving, middlewares and etc goes here].