mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01: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:
parent
b409f7807e
commit
240fdb6dc3
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,6 +3,7 @@
|
|||
coverage.out
|
||||
package-lock.json
|
||||
go.sum
|
||||
access.log
|
||||
node_modules
|
||||
issue-*/
|
||||
internalcode-*/
|
||||
|
|
|
@ -28,7 +28,7 @@ The codebase for Dependency Injection, Internationalization and localization and
|
|||
|
||||
## Fixes and Improvements
|
||||
|
||||
- New `versioning.Aliases` middleware. Example Code:
|
||||
- New `versioning.Aliases` middleware and up to 80% faster version resolve. Example Code:
|
||||
|
||||
```go
|
||||
app := iris.New()
|
||||
|
@ -40,10 +40,10 @@ api.Use(Aliases(map[string]string{
|
|||
"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")
|
||||
|
@ -723,6 +723,7 @@ Response:
|
|||
|
||||
## Breaking Changes
|
||||
|
||||
- Strict versions format on `versioning.NewGroup` is required. E.g. `"1"` is not valid anymore, you have to specify `"1.0.0"`. Example: `NewGroup(api, ">=1.0.0 <2.0.0")`. The [routing/versioning](_examples/routing/versioning) examples have been updated.
|
||||
- Now that `RegisterView` can be used to register different view engines per-Party, there is no need to support registering multiple engines under the same Party. The `app.RegisterView` now upserts the given Engine instead of append. You can now render templates **without file extension**, e.g. `index` instead of `index.ace`, both forms are valid now.
|
||||
- The `Context.ContentType` does not accept filenames to resolve the mime type anymore (caused issues with vendor-specific(vnd) MIME types).
|
||||
- The `Configuration.RemoteAddrPrivateSubnets.IPRange.Start and End` are now type of `string` instead of `net.IP`. The `WithRemoteAddrPrivateSubnet` option remains as it is, already accepts `string`s.
|
||||
|
|
6
NOTICE
6
NOTICE
|
@ -44,9 +44,9 @@ Revision ID: 5fc50a00491616d5cd0cbce3abd8b699838e25ca
|
|||
easyjson 8ab5ff9cd8e4e43 https://github.com/mailru/easyjson
|
||||
2e8b79f6c47d324
|
||||
a31dd803cf
|
||||
go-version 2b13044f5cdd383 https://github.com/hashicorp/go-version
|
||||
3370d41ce57d8bf
|
||||
3cec5e62b8
|
||||
semver 4487282d78122a2 https://github.com/blang/semver
|
||||
45e413d7515e7c5
|
||||
16b70c33fd
|
||||
golog f7561df84e64ab9 https://github.com/kataras/golog
|
||||
212f021923ce4ff
|
||||
db5df5594d
|
||||
|
|
|
@ -36,6 +36,7 @@ With your help, we can improve Open Source web development for everyone!
|
|||
> Donations from **China** are now accepted!
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/motogo"><img src="https://avatars1.githubusercontent.com/u/1704958?v=4" alt ="Horst Ender" title="motogo" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
|
||||
<a href="https://github.com/remopavithran"><img src="https://avatars1.githubusercontent.com/u/50388068?v=4" alt ="Pavithran" title="remopavithran" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
|
||||
<a href="https://github.com/mulyawansentosa"><img src="https://avatars1.githubusercontent.com/u/29946673?v=4" alt ="MULYAWAN SENTOSA" title="mulyawansentosa" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
|
||||
<a href="https://github.com/TianJIANG"><img src="https://avatars1.githubusercontent.com/u/158459?v=4" alt ="KIT UNITED" title="TianJIANG" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
|
||||
|
|
|
@ -4,16 +4,10 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/jwt"
|
||||
"github.com/kataras/iris/v12/middleware/jwt"
|
||||
)
|
||||
|
||||
/*
|
||||
Learn how to use any JWT 3rd-party package with Iris.
|
||||
In this example we use the kataras/jwt one.
|
||||
|
||||
Install with:
|
||||
go get -u github.com/kataras/jwt
|
||||
|
||||
Documentation:
|
||||
https://github.com/kataras/jwt#table-of-contents
|
||||
*/
|
||||
|
@ -71,6 +65,7 @@ func protected(ctx iris.Context) {
|
|||
|
||||
// Just an example on how you can retrieve all the standard claims (set by jwt.MaxAge, "exp").
|
||||
standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims
|
||||
|
||||
expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
|
||||
timeLeft := standardClaims.Timeleft()
|
||||
|
||||
|
|
|
@ -28,9 +28,9 @@ func newApp() *iris.Application {
|
|||
{
|
||||
m := mvc.New(dataRouter)
|
||||
|
||||
m.Handle(new(v1Controller), mvc.Version("1"), mvc.Deprecated(opts)) // 1 or 1.0, 1.0.0 ...
|
||||
m.Handle(new(v2Controller), mvc.Version("2.3")) // 2.3 or 2.3.0
|
||||
m.Handle(new(v3Controller), mvc.Version(">=3, <4")) // 3, 3.x, 3.x.x ...
|
||||
m.Handle(new(v1Controller), mvc.Version("1.0.0"), mvc.Deprecated(opts))
|
||||
m.Handle(new(v2Controller), mvc.Version("2.3.0"))
|
||||
m.Handle(new(v3Controller), mvc.Version(">=3.0.0 <4.0.0"))
|
||||
m.Handle(new(noVersionController)) // or if missing it will respond with 501 version not found.
|
||||
}
|
||||
|
||||
|
|
|
@ -12,21 +12,21 @@ func TestVersionedController(t *testing.T) {
|
|||
app := newApp()
|
||||
|
||||
e := httptest.New(t, app)
|
||||
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect().
|
||||
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("data (v1.x)")
|
||||
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "2.3.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("data (v2.x)")
|
||||
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "3.1").Expect().
|
||||
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "3.1.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("data (v3.x)")
|
||||
|
||||
// Test invalid version or no version at all.
|
||||
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "4").Expect().
|
||||
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "4.0.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("data")
|
||||
e.GET("/data").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("data")
|
||||
|
||||
// Test Deprecated (v1)
|
||||
ex := e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect()
|
||||
ex := e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect()
|
||||
ex.Status(iris.StatusOK).Body().Equal("data (v1.x)")
|
||||
ex.Header("X-API-Warn").Equal(opts.WarnMessage)
|
||||
expectedDateStr := opts.DeprecationDate.Format(app.ConfigurationReadOnly().GetTimeFormat())
|
||||
|
|
|
@ -8,84 +8,93 @@ import (
|
|||
func main() {
|
||||
app := iris.New()
|
||||
|
||||
examplePerRoute(app)
|
||||
examplePerParty(app)
|
||||
app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) {
|
||||
ctx.WriteString(`Root not found handler.
|
||||
This will be applied everywhere except the /api/* requests.`)
|
||||
})
|
||||
|
||||
api := app.Party("/api")
|
||||
// Optional, set version aliases (literal strings).
|
||||
// We use `UseRouter` instead of `Use`
|
||||
// to handle HTTP errors per version, but it's up to you.
|
||||
api.UseRouter(versioning.Aliases(versioning.AliasMap{
|
||||
// If no version provided by the client, default it to the "1.0.0".
|
||||
versioning.Empty: "1.0.0",
|
||||
// If a "latest" version is provided by the client,
|
||||
// set the version to be compared to "3.0.0".
|
||||
"latest": "3.0.0",
|
||||
}))
|
||||
|
||||
/*
|
||||
A version is extracted through the versioning.GetVersion function,
|
||||
request headers:
|
||||
- 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", ""))
|
||||
*/
|
||||
|
||||
// |----------------|
|
||||
// | The fun begins |
|
||||
// |----------------|
|
||||
|
||||
// Create a new Group, which is a compatible Party,
|
||||
// based on version constraints.
|
||||
v1 := versioning.NewGroup(api, ">=1.0.0 <2.0.0")
|
||||
|
||||
// Optionally, set custom view engine and path
|
||||
// for templates based on the version.
|
||||
v1.RegisterView(iris.HTML("./v1", ".html"))
|
||||
|
||||
// Optionally, set custom error handler(s) based on the version.
|
||||
// Keep in mind that if you do this, you will
|
||||
// have to register error handlers
|
||||
// for the rest of the parties as well.
|
||||
v1.OnErrorCode(iris.StatusNotFound, testError("v1"))
|
||||
|
||||
// Register resources based on the version.
|
||||
v1.Get("/", testHandler("v1"))
|
||||
v1.Get("/render", testView)
|
||||
|
||||
// Do the same for version 2 and version 3,
|
||||
// for the sake of the example.
|
||||
v2 := versioning.NewGroup(api, ">=2.0.0 <3.0.0")
|
||||
v2.RegisterView(iris.HTML("./v2", ".html"))
|
||||
v2.OnErrorCode(iris.StatusNotFound, testError("v2"))
|
||||
v2.Get("/", testHandler("v2"))
|
||||
v2.Get("/render", testView)
|
||||
|
||||
v3 := versioning.NewGroup(api, ">=3.0.0 <4.0.0")
|
||||
v3.RegisterView(iris.HTML("./v3", ".html"))
|
||||
v3.OnErrorCode(iris.StatusNotFound, testError("v3"))
|
||||
v3.Get("/", testHandler("v3"))
|
||||
v3.Get("/render", testView)
|
||||
|
||||
// Read the README.md before any action.
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
// How to test:
|
||||
// Open Postman
|
||||
// GET: localhost:8080/api/cats
|
||||
// Headers[1] = Accept-Version: "1" and repeat with
|
||||
// Headers[1] = Accept-Version: "2.5"
|
||||
// or even "Accept": "application/json; version=2.5"
|
||||
func examplePerRoute(app *iris.Application) {
|
||||
app.Get("/api/cats", versioning.NewMatcher(versioning.Map{
|
||||
"1": catsVersionExactly1Handler,
|
||||
">= 2, < 3": catsV2Handler,
|
||||
versioning.NotFound: versioning.NotFoundHandler,
|
||||
}))
|
||||
func testHandler(v string) iris.Handler {
|
||||
return func(ctx iris.Context) {
|
||||
ctx.JSON(iris.Map{
|
||||
"version": v,
|
||||
"message": "Hello, world!",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// How to test:
|
||||
// Open Postman
|
||||
// GET: localhost:8080/api/users
|
||||
// Headers[1] = Accept-Version: "1.9.9" and repeat with
|
||||
// Headers[1] = Accept-Version: "2.5"
|
||||
//
|
||||
// POST: localhost:8080/api/users/new
|
||||
// Headers[1] = Accept-Version: "1.8.3"
|
||||
//
|
||||
// POST: localhost:8080/api/users
|
||||
// Headers[1] = Accept-Version: "2"
|
||||
func examplePerParty(app *iris.Application) {
|
||||
usersAPI := app.Party("/api/users")
|
||||
// You can customize the way a version is extracting
|
||||
// via middleware, for example:
|
||||
// version url parameter, and, if it's missing we default it to "1".
|
||||
// usersAPI.Use(func(ctx iris.Context) {
|
||||
// versioning.SetVersion(ctx, ctx.URLParamDefault("version", "1"))
|
||||
// ctx.Next()
|
||||
// })
|
||||
// OR:
|
||||
usersAPI.Use(versioning.FromQuery("version", "1"))
|
||||
|
||||
// version 1.
|
||||
usersAPIV1 := versioning.NewGroup(usersAPI, ">= 1, < 2")
|
||||
usersAPIV1.Get("/", func(ctx iris.Context) {
|
||||
ctx.Writef("v1 resource: /api/users handler")
|
||||
})
|
||||
usersAPIV1.Post("/new", func(ctx iris.Context) {
|
||||
ctx.Writef("v1 resource: /api/users/new post handler")
|
||||
})
|
||||
|
||||
// version 2.
|
||||
usersAPIV2 := versioning.NewGroup(usersAPI, ">= 2, < 3")
|
||||
usersAPIV2.Get("/", func(ctx iris.Context) {
|
||||
ctx.Writef("v2 resource: /api/users handler")
|
||||
})
|
||||
usersAPIV2.Post("/", func(ctx iris.Context) {
|
||||
ctx.Writef("v2 resource: /api/users post handler")
|
||||
})
|
||||
|
||||
// version 3, pass it as a common iris.Party.
|
||||
usersAPIV3 := versioning.NewGroup(usersAPI, ">= 3, < 4")
|
||||
registerAPIV3(usersAPIV3)
|
||||
func testError(v string) iris.Handler {
|
||||
return func(ctx iris.Context) {
|
||||
ctx.Writef("not found: %s", v)
|
||||
}
|
||||
}
|
||||
|
||||
func catsVersionExactly1Handler(ctx iris.Context) {
|
||||
ctx.Writef("v1 exactly resource: /api/cats handler")
|
||||
}
|
||||
|
||||
func catsV2Handler(ctx iris.Context) {
|
||||
ctx.Writef("v2 resource: /api/cats handler")
|
||||
}
|
||||
|
||||
func registerAPIV3(p iris.Party) {
|
||||
p.Get("/", func(ctx iris.Context) {
|
||||
ctx.Writef("v3 resource: /api/users handler")
|
||||
})
|
||||
// [...]
|
||||
func testView(ctx iris.Context) {
|
||||
ctx.View("index.html")
|
||||
}
|
||||
|
|
1
_examples/routing/versioning/v1/index.html
Normal file
1
_examples/routing/versioning/v1/index.html
Normal file
|
@ -0,0 +1 @@
|
|||
<h1>This is the directory for version 1 templates</h1>
|
1
_examples/routing/versioning/v2/index.html
Normal file
1
_examples/routing/versioning/v2/index.html
Normal file
|
@ -0,0 +1 @@
|
|||
<h1>This is the directory for version 2 templates</h1>
|
1
_examples/routing/versioning/v3/index.html
Normal file
1
_examples/routing/versioning/v3/index.html
Normal file
|
@ -0,0 +1 @@
|
|||
<h1>This is the directory for version 3 templates</h1>
|
|
@ -764,7 +764,7 @@ type Configuration struct {
|
|||
// Defaults to "iris.locale.language.input".
|
||||
LanguageInputContextKey string `ini:"language_input_context_key" json:"languageInputContextKey,omitempty" yaml:"LanguageInputContextKey" toml:"LanguageInputContextKey"`
|
||||
// VersionContextKey is the context key which an API Version can be modified
|
||||
// via a middleware through `SetVersion` method, e.g. `versioning.SetVersion(ctx, "1.0, 1.1")`.
|
||||
// via a middleware through `SetVersion` method, e.g. `versioning.SetVersion(ctx, ">=1.0.0 <2.0.0")`.
|
||||
// Defaults to "iris.api.version".
|
||||
VersionContextKey string `ini:"version_context_key" json:"versionContextKey" yaml:"VersionContextKey" toml:"VersionContextKey"`
|
||||
// VersionAliasesContextKey is the context key which the versioning feature
|
||||
|
|
|
@ -164,7 +164,7 @@ func overlapRoute(r *Route, next *Route) {
|
|||
// Version was not found:
|
||||
// we need to be able to send the status on the last not found version
|
||||
// but reset the status code if a next available matched version was found.
|
||||
// see: versioning.Handler.
|
||||
// see the versioning package.
|
||||
if !errors.Is(ctx.GetErr(), context.ErrNotFound) {
|
||||
ctx.StatusCode(prevStatusCode)
|
||||
}
|
||||
|
@ -720,7 +720,7 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
|
|||
copy(allowMethods, api.allowMethods)
|
||||
|
||||
// make a copy of the parent properties.
|
||||
var properties map[string]interface{}
|
||||
var properties context.Map
|
||||
for k, v := range api.properties {
|
||||
properties[k] = v
|
||||
}
|
||||
|
|
2
go.mod
2
go.mod
|
@ -8,13 +8,13 @@ require (
|
|||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398
|
||||
github.com/andybalholm/brotli v1.0.1
|
||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible
|
||||
github.com/blang/semver/v4 v4.0.0
|
||||
github.com/dgraph-io/badger/v2 v2.2007.2
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/flosch/pongo2/v4 v4.0.1
|
||||
github.com/go-redis/redis/v8 v8.4.0
|
||||
github.com/google/uuid v1.1.2
|
||||
github.com/hashicorp/go-version v1.2.1
|
||||
github.com/iris-contrib/httpexpect/v2 v2.0.5
|
||||
github.com/iris-contrib/jade v1.1.4
|
||||
github.com/iris-contrib/schema v0.0.6
|
||||
|
|
|
@ -8,14 +8,14 @@ import (
|
|||
|
||||
// Version returns a valid `Option` that can be passed to the `Application.Handle` method.
|
||||
// It requires a specific "version" constraint for a Controller,
|
||||
// e.g. ">1, <=2" or just "1".
|
||||
// e.g. ">1.0.0 <=2.0.0".
|
||||
//
|
||||
//
|
||||
// Usage:
|
||||
// m := mvc.New(dataRouter)
|
||||
// m.Handle(new(v1Controller), mvc.Version("1"), mvc.Deprecated(mvc.DeprecationOptions{}))
|
||||
// m.Handle(new(v2Controller), mvc.Version("2.3"))
|
||||
// m.Handle(new(v3Controller), mvc.Version(">=3, <4"))
|
||||
// m.Handle(new(v1Controller), mvc.Version("1.0.0"), mvc.Deprecated(mvc.DeprecationOptions{}))
|
||||
// m.Handle(new(v2Controller), mvc.Version("2.3.0"))
|
||||
// m.Handle(new(v3Controller), mvc.Version(">=3.0.0 <4.0.0"))
|
||||
// m.Handle(new(noVersionController))
|
||||
//
|
||||
// See the `versioning` package's documentation for more information on
|
||||
|
|
|
@ -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,
|
||||
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 !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
|
||||
}
|
||||
|
||||
if version != "" {
|
||||
SetVersion(ctx, version)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
Loading…
Reference in New Issue
Block a user