add versioning/README.md

Former-commit-id: 7ea92fadc982038533675996704b6bf89e149aae
This commit is contained in:
Gerasimos (Makis) Maropoulos 2018-11-11 02:18:19 +02:00
parent fc9e5b3c05
commit 70610af6fd
3 changed files with 131 additions and 23 deletions

86
versioning/README.md Normal file
View File

@ -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
}
})
```

View File

@ -6,6 +6,24 @@ import (
"github.com/hashicorp/go-version" "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 type Map map[string]context.Handler
func NewMatcher(versions Map) context.Handler { func NewMatcher(versions Map) context.Handler {
@ -18,15 +36,15 @@ func NewMatcher(versions Map) context.Handler {
return return
} }
version, err := version.NewVersion(versionString) ver, err := version.NewVersion(versionString)
if err != nil { if err != nil {
notFoundHandler(ctx) notFoundHandler(ctx)
return return
} }
for _, ch := range constraintsHandlers { for _, ch := range constraintsHandlers {
if ch.constraints.Check(version) { if ch.constraints.Check(ver) {
ctx.Header("X-API-Version", version.String()) ctx.Header("X-API-Version", ver.String())
ch.handler(ctx) ch.handler(ctx)
return return
} }

View File

@ -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) { func TestNewMatcher(t *testing.T) {
app := iris.New() app := iris.New()
@ -33,6 +41,17 @@ func TestNewMatcher(t *testing.T) {
versioning.NotFound: notFoundHandler, 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 := httptest.New(t, app)
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect(). 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(). e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect().
Status(iris.StatusOK).Body().Equal(v2Response) 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(). e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect().
Status(iris.StatusNotFound).Body().Equal("Not Found") Status(iris.StatusNotFound).Body().Equal("Not Found")
} }
func TestNewGroup(t *testing.T) { func TestNewGroup(t *testing.T) {
app := iris.New() 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") userAPI := app.Party("/api/user")
// [... static serving, middlewares and etc goes here]. // [... static serving, middlewares and etc goes here].