mirror of
https://github.com/kataras/iris.git
synced 2026-01-09 13:05:56 +00:00
API versioning improvements
Replace the go-version package with a regex-free alternative semver
the result: versioned apis have almost zero performance cost now
thanks @motogo for your kind donation ❤️ - please check your github notifications
This commit is contained in:
@@ -1,57 +1,107 @@
|
||||
package versioning
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris/v12/context"
|
||||
"github.com/kataras/iris/v12/core/router"
|
||||
)
|
||||
|
||||
// Property to be defined inside the registered
|
||||
// Party on NewGroup, useful for a party to know its (optional) version
|
||||
// when the versioning feature is used.
|
||||
const Property = "iris.party.version"
|
||||
"github.com/blang/semver/v4"
|
||||
)
|
||||
|
||||
// API is a type alias of router.Party.
|
||||
// This is required in order for a Group instance
|
||||
// to implement the Party interface without field conflict.
|
||||
type API = router.Party
|
||||
|
||||
// Group is a group of version-based routes.
|
||||
// One version per one or more routes.
|
||||
// Group represents a group of resources that should
|
||||
// be handled based on a version requested by the client.
|
||||
// See `NewGroup` for more.
|
||||
type Group struct {
|
||||
API
|
||||
|
||||
// Information not currently in-use.
|
||||
version string
|
||||
validate semver.Range
|
||||
deprecation DeprecationOptions
|
||||
}
|
||||
|
||||
// NewGroup returns a ptr to Group based on the given "version" constraint.
|
||||
// NewGroup returns a version Group based on the given "version" constraint.
|
||||
// Group completes the Party interface.
|
||||
// The returned Group wraps a cloned Party of the given "r" Party therefore,
|
||||
// any changes to its parent won't affect this one (e.g. register global middlewares afterwards).
|
||||
//
|
||||
// A version is extracted through the versioning.GetVersion function:
|
||||
// Accept-Version: 1.0.0
|
||||
// Accept: application/json; version=1.0.0
|
||||
// You can customize it by setting a version based on the request context:
|
||||
// api.Use(func(ctx *context.Context) {
|
||||
// if version := ctx.URLParam("version"); version != "" {
|
||||
// SetVersion(ctx, version)
|
||||
// }
|
||||
//
|
||||
// ctx.Next()
|
||||
// })
|
||||
// OR:
|
||||
// api.Use(versioning.FromQuery("version", ""))
|
||||
//
|
||||
// Examples at: _examples/routing/versioning
|
||||
// Usage:
|
||||
// app := iris.New()
|
||||
// api := app.Party("/api")
|
||||
// v1 := versioning.NewGroup(api, ">= 1, < 2")
|
||||
// v1 := versioning.NewGroup(api, ">=1.0.0 <2.0.0")
|
||||
// v1.Get/Post/Put/Delete...
|
||||
//
|
||||
// See the `GetVersion` function to learn how
|
||||
// a version is extracted and matched over this.
|
||||
func NewGroup(r router.Party, version string) *Group {
|
||||
// Valid ranges are:
|
||||
// - "<1.0.0"
|
||||
// - "<=1.0.0"
|
||||
// - ">1.0.0"
|
||||
// - ">=1.0.0"
|
||||
// - "1.0.0", "=1.0.0", "==1.0.0"
|
||||
// - "!1.0.0", "!=1.0.0"
|
||||
//
|
||||
// A Range can consist of multiple ranges separated by space:
|
||||
// Ranges can be linked by logical AND:
|
||||
// - ">1.0.0 <2.0.0" would match between both ranges, so "1.1.1" and "1.8.7"
|
||||
// but not "1.0.0" or "2.0.0"
|
||||
// - ">1.0.0 <3.0.0 !2.0.3-beta.2" would match every version between 1.0.0 and 3.0.0
|
||||
// except 2.0.3-beta.2
|
||||
//
|
||||
// Ranges can also be linked by logical OR:
|
||||
// - "<2.0.0 || >=3.0.0" would match "1.x.x" and "3.x.x" but not "2.x.x"
|
||||
//
|
||||
// AND has a higher precedence than OR. It's not possible to use brackets.
|
||||
//
|
||||
// Ranges can be combined by both AND and OR
|
||||
//
|
||||
// - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`,
|
||||
// but not `4.2.1`, `2.1.1`
|
||||
func NewGroup(r API, version string) *Group {
|
||||
version = strings.ReplaceAll(version, ",", " ")
|
||||
version = strings.TrimSpace(version)
|
||||
|
||||
verRange, err := semver.ParseRange(version)
|
||||
if err != nil {
|
||||
r.Logger().Errorf("versioning: %s: %s", r.GetRelPath(), strings.ToLower(err.Error()))
|
||||
return &Group{API: r}
|
||||
}
|
||||
|
||||
// Clone this one.
|
||||
r = r.Party("/")
|
||||
r.Properties()[Property] = version
|
||||
|
||||
// Note that this feature alters the RouteRegisterRule to RouteOverlap
|
||||
// the RouteOverlap rule does not contain any performance downside
|
||||
// but it's good to know that if you registered other mode, this wanna change it.
|
||||
r.SetRegisterRule(router.RouteOverlap)
|
||||
r.UseOnce(Handler(version)) // this is required in order to not populate this middleware to the next group.
|
||||
|
||||
handler := makeHandler(verRange)
|
||||
// This is required in order to not populate this middleware to the next group.
|
||||
r.UseOnce(handler)
|
||||
// This is required for versioned custom error handlers,
|
||||
// of course if the parent registered one then this will do nothing.
|
||||
r.UseError(handler)
|
||||
|
||||
return &Group{
|
||||
API: r,
|
||||
version: version,
|
||||
API: r,
|
||||
validate: verRange,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,18 +118,18 @@ func (g *Group) Deprecated(options DeprecationOptions) *Group {
|
||||
return g
|
||||
}
|
||||
|
||||
// FromQuery is a simple helper which tries to
|
||||
// set the version constraint from a given URL Query Parameter.
|
||||
// The X-Api-Version is still valid.
|
||||
func FromQuery(urlQueryParameterName string, defaultVersion string) context.Handler {
|
||||
func makeHandler(validate semver.Range) context.Handler {
|
||||
return func(ctx *context.Context) {
|
||||
version := ctx.URLParam(urlQueryParameterName)
|
||||
if version == "" {
|
||||
version = defaultVersion
|
||||
}
|
||||
|
||||
if version != "" {
|
||||
SetVersion(ctx, version)
|
||||
if !matchVersionRange(ctx, validate) {
|
||||
// The overlapped handler has an exception
|
||||
// of a type of context.NotFound (which versioning.ErrNotFound wraps)
|
||||
// to clear the status code
|
||||
// and the error to ignore this
|
||||
// when available match version exists (see `NewGroup`).
|
||||
if h := NotFoundHandler; h != nil {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Next()
|
||||
|
||||
60
versioning/group_test.go
Normal file
60
versioning/group_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package versioning_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/httptest"
|
||||
"github.com/kataras/iris/v12/versioning"
|
||||
)
|
||||
|
||||
func notFoundHandler(ctx iris.Context) {
|
||||
ctx.NotFound()
|
||||
}
|
||||
|
||||
const (
|
||||
v10Response = "v1.0 handler"
|
||||
v2Response = "v2.x handler"
|
||||
)
|
||||
|
||||
func sendHandler(contents string) iris.Handler {
|
||||
return func(ctx iris.Context) {
|
||||
ctx.WriteString(contents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewGroup(t *testing.T) {
|
||||
app := iris.New()
|
||||
|
||||
userAPI := app.Party("/api/user")
|
||||
// [... static serving, middlewares and etc goes here].
|
||||
|
||||
userAPIV10 := versioning.NewGroup(userAPI, "1.0.0").Deprecated(versioning.DefaultDeprecationOptions)
|
||||
|
||||
userAPIV10.Get("/", sendHandler(v10Response))
|
||||
userAPIV2 := versioning.NewGroup(userAPI, ">= 2.0.0 < 3.0.0")
|
||||
|
||||
userAPIV2.Get("/", sendHandler(v2Response))
|
||||
userAPIV2.Post("/", sendHandler(v2Response))
|
||||
userAPIV2.Put("/other", sendHandler(v2Response))
|
||||
|
||||
e := httptest.New(t, app)
|
||||
|
||||
ex := e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect()
|
||||
ex.Status(iris.StatusOK).Body().Equal(v10Response)
|
||||
ex.Header("X-API-Warn").Equal(versioning.DefaultDeprecationOptions.WarnMessage)
|
||||
|
||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect().
|
||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||
e.POST("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||
e.PUT("/api/user/other").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||
|
||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect().
|
||||
Status(iris.StatusNotImplemented).Body().Equal("version not found")
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris/v12/context"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -52,11 +54,89 @@ var NotFoundHandler = func(ctx *context.Context) {
|
||||
ctx.StopWithPlainError(501, ErrNotFound)
|
||||
}
|
||||
|
||||
// FromQuery is a simple helper which tries to
|
||||
// set the version constraint from a given URL Query Parameter.
|
||||
// The X-Api-Version is still valid.
|
||||
func FromQuery(urlQueryParameterName string, defaultVersion string) context.Handler {
|
||||
return func(ctx *context.Context) {
|
||||
version := ctx.URLParam(urlQueryParameterName)
|
||||
if version == "" {
|
||||
version = defaultVersion
|
||||
}
|
||||
|
||||
if version != "" {
|
||||
SetVersion(ctx, version)
|
||||
}
|
||||
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// If reports whether the "got" matches the "expected" one.
|
||||
// the "expected" can be a constraint like ">=1.0.0 <2.0.0".
|
||||
// This function is just a helper, better use the Group instead.
|
||||
func If(got string, expected string) bool {
|
||||
v, err := semver.Make(got)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
validate, err := semver.ParseRange(expected)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return validate(v)
|
||||
}
|
||||
|
||||
// Match reports whether the request matches the expected version.
|
||||
// This function is just a helper, better use the Group instead.
|
||||
func Match(ctx *context.Context, expectedVersion string) bool {
|
||||
validate, err := semver.ParseRange(expectedVersion)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return matchVersionRange(ctx, validate)
|
||||
}
|
||||
|
||||
func matchVersionRange(ctx *context.Context, validate semver.Range) bool {
|
||||
gotVersion := GetVersion(ctx)
|
||||
|
||||
alias, aliasFound := GetVersionAlias(ctx, gotVersion)
|
||||
if aliasFound {
|
||||
SetVersion(ctx, alias) // set the version so next routes have it already.
|
||||
gotVersion = alias
|
||||
}
|
||||
|
||||
if gotVersion == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
v, err := semver.Make(gotVersion)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !validate(v) {
|
||||
return false
|
||||
}
|
||||
|
||||
versionString := v.String()
|
||||
|
||||
if !aliasFound { // don't lose any time to set if already set.
|
||||
SetVersion(ctx, versionString)
|
||||
}
|
||||
|
||||
ctx.Header(APIVersionResponseHeader, versionString)
|
||||
return true
|
||||
}
|
||||
|
||||
// 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"
|
||||
// - "Accept" header, i.e Accept: "application/json; version=1.0.0"
|
||||
// - "Accept-Version" header, i.e Accept-Version: "1.0.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).
|
||||
@@ -108,7 +188,7 @@ func GetVersion(ctx *context.Context) string {
|
||||
// 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
|
||||
// version := ctx.URLParamDefault("version", "1")
|
||||
// version := ctx.URLParamDefault("version", "1.0.0")
|
||||
// versioning.SetVersion(ctx, version)
|
||||
// ctx.Next()
|
||||
// }
|
||||
@@ -129,15 +209,15 @@ type AliasMap = map[string]string
|
||||
//
|
||||
// api := app.Party("/api")
|
||||
// api.Use(Aliases(map[string]string{
|
||||
// versioning.Empty: "1", // when no version was provided by the client.
|
||||
// versioning.Empty: "1.0.0", // when no version was provided by the client.
|
||||
// "beta": "4.0.0",
|
||||
// "stage": "5.0.0-alpha"
|
||||
// }))
|
||||
//
|
||||
// v1 := NewGroup(api, ">= 1, < 2")
|
||||
// v1 := NewGroup(api, ">=1.0.0 < 2.0.0")
|
||||
// v1.Get/Post...
|
||||
//
|
||||
// v4 := NewGroup(api, ">= 4, < 5")
|
||||
// v4 := NewGroup(api, ">=4.0.0 < 5.0.0")
|
||||
// v4.Get/Post...
|
||||
//
|
||||
// stage := NewGroup(api, "5.0.0-alpha")
|
||||
@@ -154,6 +234,23 @@ func Aliases(aliases AliasMap) context.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// Handler returns a handler which is only fired
|
||||
// when the "version" is matched with the requested one.
|
||||
// It is not meant to be used by end-developers
|
||||
// (exported for version controller feature).
|
||||
// Use `NewGroup` instead.
|
||||
func Handler(version string) context.Handler {
|
||||
validate, err := semver.ParseRange(version)
|
||||
if err != nil {
|
||||
return func(ctx *context.Context) {
|
||||
ctx.StopWithError(500, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return makeHandler(validate)
|
||||
}
|
||||
|
||||
// GetVersionAlias returns the version alias of the given "gotVersion"
|
||||
// or empty. It Reports whether the alias was found.
|
||||
// See `SetVersionAliases`, `Aliases` and `Match` for more.
|
||||
@@ -196,7 +293,7 @@ func GetVersionAlias(ctx *context.Context, gotVersion string) (string, bool) {
|
||||
//
|
||||
// The last "override" input argument indicates whether any
|
||||
// existing aliases, registered by previous handlers in the chain,
|
||||
// should be overriden or copied to the previous map one.
|
||||
// should be overridden or copied to the previous map one.
|
||||
func SetVersionAliases(ctx *context.Context, aliases AliasMap, override bool) {
|
||||
key := ctx.Application().ConfigurationReadOnly().GetVersionAliasesContextKey()
|
||||
if key == "" {
|
||||
|
||||
@@ -8,6 +8,15 @@ import (
|
||||
"github.com/kataras/iris/v12/versioning"
|
||||
)
|
||||
|
||||
func TestIf(t *testing.T) {
|
||||
if expected, got := true, versioning.If("1.0.0", ">=1.0.0"); expected != got {
|
||||
t.Fatalf("expected %s to be %s", "1.0.0", ">= 1.0.0")
|
||||
}
|
||||
if expected, got := true, versioning.If("1.2.3", "> 1.2.0"); expected != got {
|
||||
t.Fatalf("expected %s to be %s", "1.2.3", "> 1.2.0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVersion(t *testing.T) {
|
||||
app := iris.New()
|
||||
|
||||
@@ -23,16 +32,16 @@ func TestGetVersion(t *testing.T) {
|
||||
|
||||
e := httptest.New(t, app)
|
||||
|
||||
e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("1.0")
|
||||
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("2.1")
|
||||
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1 ;other=dsa").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("2.1")
|
||||
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=2.1").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("2.1")
|
||||
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=1").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("1")
|
||||
e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("1.0.0")
|
||||
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("2.1.0")
|
||||
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1.0 ;other=dsa").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("2.1.0")
|
||||
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=2.1.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("2.1.0")
|
||||
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=1.0.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("1.0.0")
|
||||
|
||||
// unknown versions.
|
||||
e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "").Expect().
|
||||
@@ -52,8 +61,8 @@ func TestVersionAliases(t *testing.T) {
|
||||
|
||||
api := app.Party("/api")
|
||||
api.Use(versioning.Aliases(map[string]string{
|
||||
versioning.Empty: "1",
|
||||
"stage": "2",
|
||||
versioning.Empty: "1.0.0",
|
||||
"stage": "2.0.0",
|
||||
}))
|
||||
|
||||
writeVesion := func(ctx iris.Context) {
|
||||
@@ -61,13 +70,13 @@ func TestVersionAliases(t *testing.T) {
|
||||
}
|
||||
|
||||
// A group without registration order.
|
||||
v3 := versioning.NewGroup(api, ">= 3, < 4")
|
||||
v3 := versioning.NewGroup(api, ">= 3.0.0 < 4.0.0")
|
||||
v3.Get("/", writeVesion)
|
||||
|
||||
v1 := versioning.NewGroup(api, ">= 1, < 2")
|
||||
v1 := versioning.NewGroup(api, ">= 1.0.0 < 2.0.0")
|
||||
v1.Get("/", writeVesion)
|
||||
|
||||
v2 := versioning.NewGroup(api, ">= 2, < 3")
|
||||
v2 := versioning.NewGroup(api, ">= 2.0.0 < 3.0.0")
|
||||
v2.Get("/", writeVesion)
|
||||
|
||||
api.Get("/manual", func(ctx iris.Context) {
|
||||
@@ -84,15 +93,15 @@ func TestVersionAliases(t *testing.T) {
|
||||
e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("1.0.0")
|
||||
// Test NotFound error, aliases are not responsible for that.
|
||||
e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "4").Expect().
|
||||
e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "4.0.0").Expect().
|
||||
Status(iris.StatusNotImplemented).Body().Equal("version not found")
|
||||
// Test "stage" alias.
|
||||
e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "stage").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("2.0.0")
|
||||
// Test version 2.
|
||||
e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "2").Expect().
|
||||
e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "2.0.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("2.0.0")
|
||||
// Test version 3 (registered first).
|
||||
e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "3.1").Expect().
|
||||
e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "3.1.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("3.1.0")
|
||||
}
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
package versioning
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/v12/context"
|
||||
|
||||
"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 {
|
||||
_, ok := check(v, is)
|
||||
return ok
|
||||
}
|
||||
|
||||
func check(v string, is string) (string, bool) {
|
||||
if v == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
ver, err := version.NewVersion(v)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
constraints, err := version.NewConstraint(is)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// return the extracted version from request, even if not matched.
|
||||
return ver.String(), 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.
|
||||
//
|
||||
// If matched then it sets the "X-Api-Version" response header and
|
||||
// stores the matched version into Context (see `GetVersion` too).
|
||||
//
|
||||
// See the `Aliases` function to register version constraint
|
||||
// aliases for a versioning Party, extremely useful when a Group is used.
|
||||
func Match(ctx *context.Context, expectedVersion string) bool {
|
||||
gotVersion := GetVersion(ctx)
|
||||
|
||||
alias, aliasFound := GetVersionAlias(ctx, gotVersion)
|
||||
if aliasFound {
|
||||
SetVersion(ctx, alias) // set the version so next routes have it already.
|
||||
gotVersion = alias
|
||||
}
|
||||
|
||||
versionString, matched := check(gotVersion, expectedVersion)
|
||||
if !matched {
|
||||
return false
|
||||
}
|
||||
|
||||
if !aliasFound { // don't lose any time to set if already set.
|
||||
SetVersion(ctx, versionString)
|
||||
}
|
||||
|
||||
ctx.Header(APIVersionResponseHeader, versionString)
|
||||
return true
|
||||
}
|
||||
|
||||
// Handler returns a handler which stop the execution
|
||||
// when the given "version" does not match with the requested one.
|
||||
func Handler(version string) context.Handler {
|
||||
return func(ctx *context.Context) {
|
||||
if !Match(ctx, version) {
|
||||
// The overlapped handler has an exception
|
||||
// of a type of context.NotFound (which versioning.ErrNotFound wraps)
|
||||
// to clear the status code
|
||||
// and the error to ignore this
|
||||
// when available match version exists (see `NewGroup`).
|
||||
NotFoundHandler(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
return func(ctx *context.Context) {
|
||||
versionString := GetVersion(ctx)
|
||||
|
||||
if versionString == "" || versionString == NotFound {
|
||||
notFoundHandler(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
ver, err := version.NewVersion(versionString)
|
||||
if err != nil {
|
||||
notFoundHandler(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
for _, ch := range constraintsHandlers {
|
||||
if ch.constraints.Check(ver) {
|
||||
ctx.Header(APIVersionResponseHeader, ver.String())
|
||||
ch.handler(ctx)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// pass the not matched version so the not found handler can have knowedge about it.
|
||||
// SetVersion(ctx, versionString)
|
||||
// or let a manual cal of GetVersion(ctx) do that instead.
|
||||
notFoundHandler(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
type constraintsHandler struct {
|
||||
constraints version.Constraints
|
||||
handler context.Handler
|
||||
}
|
||||
|
||||
func buildConstraints(versionsHandler Map) (constraintsHandlers []*constraintsHandler, notfoundHandler context.Handler) {
|
||||
for v, h := range versionsHandler {
|
||||
if v == NotFound {
|
||||
notfoundHandler = h
|
||||
continue
|
||||
}
|
||||
|
||||
constraints, err := version.NewConstraint(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
constraintsHandlers = append(constraintsHandlers, &constraintsHandler{
|
||||
constraints: constraints,
|
||||
handler: h,
|
||||
})
|
||||
}
|
||||
|
||||
if notfoundHandler == nil {
|
||||
notfoundHandler = NotFoundHandler
|
||||
}
|
||||
|
||||
// no sort, the end-dev should declare
|
||||
// all version constraint, i.e < 4.0 may be catch 1.0 if not something like
|
||||
// >= 3.0, < 4.0.
|
||||
// I can make it ordered but I do NOT like the final API of it:
|
||||
/*
|
||||
app.Get("/api/user", NewMatcher( // accepts an array, ordered, see last elem.
|
||||
V("1.0", vHandler("v1 here")),
|
||||
V("2.0", vHandler("v2 here")),
|
||||
V("< 4.0", vHandler("v3.x here")),
|
||||
))
|
||||
instead we have:
|
||||
|
||||
app.Get("/api/user", NewMatcher(Map{ // accepts a map, unordered, see last elem.
|
||||
"1.0": Deprecated(vHandler("v1 here")),
|
||||
"2.0": vHandler("v2 here"),
|
||||
">= 3.0, < 4.0": vHandler("v3.x here"),
|
||||
VersionUnknown: customHandlerForNotMatchingVersion,
|
||||
}))
|
||||
*/
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
package versioning_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/httptest"
|
||||
"github.com/kataras/iris/v12/versioning"
|
||||
)
|
||||
|
||||
func notFoundHandler(ctx iris.Context) {
|
||||
ctx.NotFound()
|
||||
}
|
||||
|
||||
const (
|
||||
v10Response = "v1.0 handler"
|
||||
v2Response = "v2.x handler"
|
||||
)
|
||||
|
||||
func sendHandler(contents string) iris.Handler {
|
||||
return func(ctx iris.Context) {
|
||||
ctx.WriteString(contents)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
userAPI := app.Party("/api/user")
|
||||
userAPI.Get("/", versioning.NewMatcher(versioning.Map{
|
||||
"1.0": sendHandler(v10Response),
|
||||
">= 2, < 3": sendHandler(v2Response),
|
||||
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().
|
||||
Status(iris.StatusOK).Body().Equal(v10Response)
|
||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1").Expect().
|
||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||
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")
|
||||
// [... static serving, middlewares and etc goes here].
|
||||
|
||||
userAPIV10 := versioning.NewGroup(userAPI, "1.0").Deprecated(versioning.DefaultDeprecationOptions)
|
||||
// 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(userAPI, ">= 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))
|
||||
|
||||
e := httptest.New(t, app)
|
||||
|
||||
ex := e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect()
|
||||
ex.Status(iris.StatusOK).Body().Equal(v10Response)
|
||||
ex.Header("X-API-Warn").Equal(versioning.DefaultDeprecationOptions.WarnMessage)
|
||||
|
||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1").Expect().
|
||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect().
|
||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||
e.POST("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2").Expect().
|
||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||
e.PUT("/api/user/other").WithHeader(versioning.AcceptVersionHeaderKey, "2.9").Expect().
|
||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||
|
||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect().
|
||||
Status(iris.StatusNotImplemented).Body().Equal("version not found")
|
||||
}
|
||||
Reference in New Issue
Block a user