diff --git a/.deepsource.toml b/.deepsource.toml
index b767baab..17377110 100644
--- a/.deepsource.toml
+++ b/.deepsource.toml
@@ -14,4 +14,3 @@ enabled = true
[analyzers.meta]
import_paths = ["github.com/kataras/iris"]
-
diff --git a/.gitignore b/.gitignore
index 37739136..8ddb0de7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
coverage.out
package-lock.json
go.sum
+access.log
node_modules
issue-*/
internalcode-*/
diff --git a/HISTORY.md b/HISTORY.md
index 6a333341..307e4b65 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -28,6 +28,29 @@ The codebase for Dependency Injection, Internationalization and localization and
## Fixes and Improvements
+- New `versioning.Aliases` middleware and up to 80% faster version resolve. Example Code:
+
+```go
+app := iris.New()
+
+api := app.Party("/api")
+api.Use(Aliases(map[string]string{
+ versioning.Empty: "1", // when no version was provided by the client.
+ "beta": "4.0.0",
+ "stage": "5.0.0-alpha"
+}))
+
+v1 := NewGroup(api, ">=1.0.0 <2.0.0")
+v1.Get/Post...
+
+v4 := NewGroup(api, ">=4.0.0 <5.0.0")
+v4.Get/Post...
+
+stage := NewGroup(api, "5.0.0-alpha")
+stage.Get/Post...
+```
+
+- New [Basic Authentication](https://github.com/kataras/iris/tree/master/middleware/basicauth) middleware. Its `Default` function has not changed, however, the rest, e.g. `New` contains breaking changes as the new middleware features new functionalities.
- Add `iris.DirOptions.SPA bool` field to allow [Single Page Applications](https://github.com/kataras/iris/tree/master/_examples/file-server/single-page-application/basic/main.go) under a file server.
- A generic User interface, see the `Context.SetUser/User` methods in the New Context Methods section for more. In-short, the basicauth middleware's stored user can now be retrieved through `Context.User()` which provides more information than the native `ctx.Request().BasicAuth()` method one. Third-party authentication middleware creators can benefit of these two methods, plus the Logout below.
- A `Context.Logout` method is added, can be used to invalidate [basicauth](https://github.com/kataras/iris/blob/master/_examples/auth/basicauth/basic/main.go) or [jwt](https://github.com/kataras/iris/blob/master/_examples/auth/jwt/blocklist/main.go) client credentials.
@@ -700,6 +723,7 @@ Response:
## Breaking Changes
+- The `versioning.NewMatcher` has been removed entirely in favor of `NewGroup`. 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.
diff --git a/NOTICE b/NOTICE
index f5f25113..aaf04975 100644
--- a/NOTICE
+++ b/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
diff --git a/README.md b/README.md
index c8e78abb..f13cbefe 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,15 @@
[![Black Lives Matter](https://iris-go.com/images/blacklivesmatter_banner.png)](https://support.eji.org/give/153413/#!/donation/checkout)
-
+
+
+# Happy New Year 🎉
+
+Thank you for being with us every step of the way in 2020. I hope the next years brings you only good luck and great joy.
@@ -31,6 +36,13 @@ With your help, we can improve Open Source web development for everyone!
> Donations from **China** are now accepted!
+
+
+
+
+
+
+
diff --git a/_examples/auth/basicauth/basic/main.go b/_examples/auth/basicauth/basic/main.go
index 36465501..7c663cfd 100644
--- a/_examples/auth/basicauth/basic/main.go
+++ b/_examples/auth/basicauth/basic/main.go
@@ -64,14 +64,11 @@ func main() {
}
func handler(ctx iris.Context) {
- // username, password, _ := ctx.Request().BasicAuth()
- // third parameter it will be always true because the middleware
- // makes sure for that, otherwise this handler will not be executed.
- // OR:
- user := ctx.User()
- // OR ctx.User().GetRaw() to get the underline value.
- username, _ := user.GetUsername()
- password, _ := user.GetPassword()
+ // user := ctx.User().(*myUserType)
+ // or ctx.User().GetRaw().(*myUserType)
+ // ctx.Writef("%s %s:%s", ctx.Path(), user.Username, user.Password)
+ // OR if you don't have registered custom User structs:
+ username, password, _ := ctx.Request().BasicAuth()
ctx.Writef("%s %s:%s", ctx.Path(), username, password)
}
diff --git a/_examples/auth/jwt/basic/main.go b/_examples/auth/jwt/basic/main.go
index f37ce681..ddcb9a58 100644
--- a/_examples/auth/jwt/basic/main.go
+++ b/_examples/auth/jwt/basic/main.go
@@ -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()
diff --git a/_examples/mvc/error-handler-preflight/main.go b/_examples/mvc/error-handler-preflight/main.go
index 34289a95..e25ab5e2 100644
--- a/_examples/mvc/error-handler-preflight/main.go
+++ b/_examples/mvc/error-handler-preflight/main.go
@@ -29,7 +29,7 @@ type response struct {
Timestamp int64 `json:"timestamp,omitempty"`
}
-func (r response) Preflight(ctx iris.Context) error {
+func (r *response) Preflight(ctx iris.Context) error {
if r.ID > 0 {
r.Timestamp = time.Now().Unix()
}
@@ -64,15 +64,15 @@ type user struct {
ID uint64 `json:"id"`
}
-func (c *controller) GetBy(userid uint64) response {
+func (c *controller) GetBy(userid uint64) *response {
if userid != 1 {
- return response{
+ return &response{
Code: iris.StatusNotFound,
Message: "User Not Found",
}
}
- return response{
+ return &response{
ID: userid,
Data: user{ID: userid},
}
diff --git a/_examples/mvc/versioned-controller/main.go b/_examples/mvc/versioned-controller/main.go
index 9c8bb950..56cedb47 100644
--- a/_examples/mvc/versioned-controller/main.go
+++ b/_examples/mvc/versioned-controller/main.go
@@ -28,10 +28,10 @@ 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(noVersionController)) // or if missing it will respond with 501 version not found.
+ 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.
}
return app
diff --git a/_examples/mvc/versioned-controller/main_test.go b/_examples/mvc/versioned-controller/main_test.go
index 309a14f1..33cedd42 100644
--- a/_examples/mvc/versioned-controller/main_test.go
+++ b/_examples/mvc/versioned-controller/main_test.go
@@ -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())
diff --git a/_examples/routing/basic/main.go b/_examples/routing/basic/main.go
index 1ba67a82..a22c1eb6 100644
--- a/_examples/routing/basic/main.go
+++ b/_examples/routing/basic/main.go
@@ -152,7 +152,7 @@ func newApp() *iris.Application {
}
// wildcard subdomains.
- wildcardSubdomain := app.Party("*.")
+ wildcardSubdomain := app.WildcardSubdomain()
{
wildcardSubdomain.Get("/", func(ctx iris.Context) {
ctx.Writef("Subdomain can be anything, now you're here from: %s", ctx.Subdomain())
diff --git a/_examples/routing/main.go b/_examples/routing/main.go
index d7537ea4..5dd843c0 100644
--- a/_examples/routing/main.go
+++ b/_examples/routing/main.go
@@ -82,11 +82,11 @@ func registerGamesRoutes(app *iris.Application) {
}
func registerSubdomains(app *iris.Application) {
- mysubdomain := app.Party("mysubdomain.")
+ mysubdomain := app.Subdomain("mysubdomain")
// http://mysubdomain.myhost.com
mysubdomain.Get("/", h)
- willdcardSubdomain := app.Party("*.")
+ willdcardSubdomain := app.WildcardSubdomain()
willdcardSubdomain.Get("/", h)
willdcardSubdomain.Party("/party").Get("/", h)
}
diff --git a/_examples/routing/overview/main.go b/_examples/routing/overview/main.go
index 2e327a2a..0c42bfcd 100644
--- a/_examples/routing/overview/main.go
+++ b/_examples/routing/overview/main.go
@@ -116,7 +116,7 @@ func main() {
adminRoutes.Get("/settings", info)
// Wildcard/dynamic subdomain
- dynamicSubdomainRoutes := app.Party("*.")
+ dynamicSubdomainRoutes := app.WildcardSubdomain()
// GET: http://any_thing_here.localhost:8080
dynamicSubdomainRoutes.Get("/", info)
diff --git a/_examples/routing/subdomains/wildcard/main.go b/_examples/routing/subdomains/wildcard/main.go
index 0eb3dc30..fb12f8f0 100644
--- a/_examples/routing/subdomains/wildcard/main.go
+++ b/_examples/routing/subdomains/wildcard/main.go
@@ -33,7 +33,7 @@ func main() {
}*/
// no order, you can register subdomains at the end also.
- dynamicSubdomains := app.Party("*.")
+ dynamicSubdomains := app.WildcardSubdomain()
{
dynamicSubdomains.Get("/", dynamicSubdomainHandler)
diff --git a/_examples/routing/versioning/main.go b/_examples/routing/versioning/main.go
index 3502173f..3e18aa7e 100644
--- a/_examples/routing/versioning/main.go
+++ b/_examples/routing/versioning/main.go
@@ -8,71 +8,95 @@ 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 != "" {
+ versioning.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")
+ // To mark an API version as deprecated use the Deprecated method.
+ // v1.Deprecated(versioning.DefaultDeprecationOptions)
+
+ // 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()
- })
-
- // 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")
- })
+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 testView(ctx iris.Context) {
+ ctx.View("index.html")
}
diff --git a/_examples/routing/versioning/v1/index.html b/_examples/routing/versioning/v1/index.html
new file mode 100644
index 00000000..efe99490
--- /dev/null
+++ b/_examples/routing/versioning/v1/index.html
@@ -0,0 +1 @@
+
This is the directory for version 1 templates
\ No newline at end of file
diff --git a/_examples/routing/versioning/v2/index.html b/_examples/routing/versioning/v2/index.html
new file mode 100644
index 00000000..802d7081
--- /dev/null
+++ b/_examples/routing/versioning/v2/index.html
@@ -0,0 +1 @@
+This is the directory for version 2 templates
\ No newline at end of file
diff --git a/_examples/routing/versioning/v3/index.html b/_examples/routing/versioning/v3/index.html
new file mode 100644
index 00000000..b1e3fdc7
--- /dev/null
+++ b/_examples/routing/versioning/v3/index.html
@@ -0,0 +1 @@
+This is the directory for version 3 templates
\ No newline at end of file
diff --git a/_examples/sessions/overview/example/example.go b/_examples/sessions/overview/example/example.go
index e47624d7..7472e571 100644
--- a/_examples/sessions/overview/example/example.go
+++ b/_examples/sessions/overview/example/example.go
@@ -114,7 +114,7 @@ func NewApp(sess *sessions.Sessions) *iris.Application {
app.Get("/delete", func(ctx iris.Context) {
session := sessions.Get(ctx)
// delete a specific key
- session.Delete("name")
+ session.Delete("username")
})
app.Get("/clear", func(ctx iris.Context) {
diff --git a/_examples/sessions/securecookie/main_test.go b/_examples/sessions/securecookie/main_test.go
index 775c698d..6b0304b1 100644
--- a/_examples/sessions/securecookie/main_test.go
+++ b/_examples/sessions/securecookie/main_test.go
@@ -16,12 +16,12 @@ func TestSessionsEncodeDecode(t *testing.T) {
es.Cookies().NotEmpty()
es.Body().Equal("All ok session set to: iris [isNew=true]")
- e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: iris")
+ e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The username on the /set was: iris")
// delete and re-get
e.GET("/delete").Expect().Status(iris.StatusOK)
- e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: ")
+ e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The username on the /set was: ")
// set, clear and re-get
e.GET("/set").Expect().Body().Equal("All ok session set to: iris [isNew=false]")
e.GET("/clear").Expect().Status(iris.StatusOK)
- e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: ")
+ e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The username on the /set was: ")
}
diff --git a/_examples/testing/httptest/main.go b/_examples/testing/httptest/main.go
index a2bb3101..35dcf78c 100644
--- a/_examples/testing/httptest/main.go
+++ b/_examples/testing/httptest/main.go
@@ -37,6 +37,11 @@ func h(ctx iris.Context) {
// third parameter it will be always true because the middleware
// makes sure for that, otherwise this handler will not be executed.
// OR:
+ //
+ // user := ctx.User().(*myUserType)
+ // ctx.Writef("%s %s:%s", ctx.Path(), user.Username, user.Password)
+ // OR if you don't have registered custom User structs:
+ //
// ctx.User().GetUsername()
// ctx.User().GetPassword()
ctx.Writef("%s %s:%s", ctx.Path(), username, password)
diff --git a/_examples/view/template_html_4/main.go b/_examples/view/template_html_4/main.go
index ef0b97b8..9c0e4b2b 100644
--- a/_examples/view/template_html_4/main.go
+++ b/_examples/view/template_html_4/main.go
@@ -27,7 +27,7 @@ func main() {
// wildcard subdomain, will catch username1.... username2.... username3... username4.... username5...
// that our below links are providing via page.html's first argument which is the subdomain.
- subdomain := app.Party("*.")
+ subdomain := app.WildcardSubdomain()
mypathRoute := subdomain.Get("/mypath", emptyHandler)
mypathRoute.Name = "my-page1"
diff --git a/configuration.go b/configuration.go
index bc9f04e8..7e96aac9 100644
--- a/configuration.go
+++ b/configuration.go
@@ -764,9 +764,14 @@ 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
+ // can look up for alternative values of a version and fallback to that.
+ // Head over to the versioning package for more.
+ // Defaults to "iris.api.version.aliases"
+ VersionAliasesContextKey string `ini:"version_aliases_context_key" json:"versionAliasesContextKey" yaml:"VersionAliasesContextKey" toml:"VersionAliasesContextKey"`
// ViewEngineContextKey is the context's values key
// responsible to store and retrieve(view.Engine) the current view engine.
// A middleware or a Party can modify its associated value to change
@@ -974,6 +979,11 @@ func (c Configuration) GetVersionContextKey() string {
return c.VersionContextKey
}
+// GetVersionAliasesContextKey returns the VersionAliasesContextKey field.
+func (c Configuration) GetVersionAliasesContextKey() string {
+ return c.VersionAliasesContextKey
+}
+
// GetViewEngineContextKey returns the ViewEngineContextKey field.
func (c Configuration) GetViewEngineContextKey() string {
return c.ViewEngineContextKey
@@ -1132,6 +1142,10 @@ func WithConfiguration(c Configuration) Configurator {
main.VersionContextKey = v
}
+ if v := c.VersionAliasesContextKey; v != "" {
+ main.VersionAliasesContextKey = v
+ }
+
if v := c.ViewEngineContextKey; v != "" {
main.ViewEngineContextKey = v
}
@@ -1205,16 +1219,17 @@ func DefaultConfiguration() Configuration {
// The request body the size limit
// can be set by the middleware `LimitRequestBodySize`
// or `context#SetMaxRequestBodySize`.
- PostMaxMemory: 32 << 20, // 32MB
- LocaleContextKey: "iris.locale",
- LanguageContextKey: "iris.locale.language",
- LanguageInputContextKey: "iris.locale.language.input",
- VersionContextKey: "iris.api.version",
- ViewEngineContextKey: "iris.view.engine",
- ViewLayoutContextKey: "iris.view.layout",
- ViewDataContextKey: "iris.view.data",
- RemoteAddrHeaders: nil,
- RemoteAddrHeadersForce: false,
+ PostMaxMemory: 32 << 20, // 32MB
+ LocaleContextKey: "iris.locale",
+ LanguageContextKey: "iris.locale.language",
+ LanguageInputContextKey: "iris.locale.language.input",
+ VersionContextKey: "iris.api.version",
+ VersionAliasesContextKey: "iris.api.version.aliases",
+ ViewEngineContextKey: "iris.view.engine",
+ ViewLayoutContextKey: "iris.view.layout",
+ ViewDataContextKey: "iris.view.data",
+ RemoteAddrHeaders: nil,
+ RemoteAddrHeadersForce: false,
RemoteAddrPrivateSubnets: []netutil.IPRange{
{
Start: "10.0.0.0",
diff --git a/context/configuration.go b/context/configuration.go
index ea0e7f25..c2dcb6e6 100644
--- a/context/configuration.go
+++ b/context/configuration.go
@@ -53,6 +53,8 @@ type ConfigurationReadOnly interface {
GetLanguageInputContextKey() string
// GetVersionContextKey returns the VersionContextKey field.
GetVersionContextKey() string
+ // GetVersionAliasesContextKey returns the VersionAliasesContextKey field.
+ GetVersionAliasesContextKey() string
// GetViewEngineContextKey returns the ViewEngineContextKey field.
GetViewEngineContextKey() string
diff --git a/context/context.go b/context/context.go
index b5e139b2..efedee3f 100644
--- a/context/context.go
+++ b/context/context.go
@@ -1974,6 +1974,13 @@ func (ctx *Context) UploadFormFiles(destDirectory string, before ...func(*Contex
for _, files := range fhs {
innerLoop:
for _, file := range files {
+ // Fix an issue that net/http has,
+ // an attacker can push a filename
+ // which could lead to override existing system files
+ // by ../../$file.
+ // Reported by Frank through security reports.
+ file.Filename = strings.TrimLeft(file.Filename, "../")
+ file.Filename = strings.TrimLeft(file.Filename, "..\\")
for _, b := range before {
if !b(ctx, file) {
diff --git a/context/context_user.go b/context/context_user.go
index 794b78a9..d76ba6d4 100644
--- a/context/context_user.go
+++ b/context/context_user.go
@@ -81,15 +81,15 @@ to the end-developer's custom implementations.
// SimpleUser is a simple implementation of the User interface.
type SimpleUser struct {
- Authorization string `json:"authorization,omitempty"`
- AuthorizedAt time.Time `json:"authorized_at,omitempty"`
- ID string `json:"id,omitempty"`
- Username string `json:"username,omitempty"`
- Password string `json:"password,omitempty"`
- Email string `json:"email,omitempty"`
- Roles []string `json:"roles,omitempty"`
- Token json.RawMessage `json:"token,omitempty"`
- Fields Map `json:"fields,omitempty"`
+ Authorization string `json:"authorization,omitempty" db:"authorization"`
+ AuthorizedAt time.Time `json:"authorized_at,omitempty" db:"authorized_at"`
+ ID string `json:"id,omitempty" db:"id"`
+ Username string `json:"username,omitempty" db:"username"`
+ Password string `json:"password,omitempty" db:"password"`
+ Email string `json:"email,omitempty" db:"email"`
+ Roles []string `json:"roles,omitempty" db:"roles"`
+ Token json.RawMessage `json:"token,omitempty" db:"token"`
+ Fields Map `json:"fields,omitempty" db:"fields"`
}
var _ User = (*SimpleUser)(nil)
diff --git a/context/handler.go b/context/handler.go
index d8744ee6..13f94128 100644
--- a/context/handler.go
+++ b/context/handler.go
@@ -239,6 +239,10 @@ var ignoreMainHandlerNames = [...]string{
"iris.reCAPTCHA",
"iris.profiling",
"iris.recover",
+ "iris.accesslog",
+ "iris.grpc",
+ "iris.requestid",
+ "iris.rewrite",
}
// ingoreMainHandlerName reports whether a main handler of "name" should
diff --git a/context/route.go b/context/route.go
index 8f3db812..4cc4794e 100644
--- a/context/route.go
+++ b/context/route.go
@@ -62,6 +62,10 @@ type RouteReadOnly interface {
// MainHandlerIndex returns the first registered handler's index for the route.
MainHandlerIndex() int
+ // Property returns a specific property based on its "key"
+ // of this route's Party owner.
+ Property(key string) (interface{}, bool)
+
// Sitemap properties: https://www.sitemaps.org/protocol.html
// GetLastMod returns the date of last modification of the file served by this route.
diff --git a/core/router/api_builder.go b/core/router/api_builder.go
index 34f3769a..4da5024d 100644
--- a/core/router/api_builder.go
+++ b/core/router/api_builder.go
@@ -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)
}
@@ -196,6 +196,11 @@ type APIBuilder struct {
// the api builder global macros registry
macros *macro.Macros
+ // the per-party (and its children) values map
+ // that may help on building the API
+ // when source code is splitted between projects.
+ // Initialized on Properties method.
+ properties context.Map
// the api builder global routes repository
routes *repository
// disables the debug logging of routes under a per-party and its children.
@@ -624,7 +629,7 @@ func (api *APIBuilder) createRoutes(errorCode int, methods []string, relativePat
routes := make([]*Route, len(methods))
for i, m := range methods { // single, empty method for error handlers.
- route, err := NewRoute(errorCode, m, subdomain, path, routeHandlers, *api.macros)
+ route, err := NewRoute(api, errorCode, m, subdomain, path, routeHandlers, *api.macros)
if err != nil { // template path parser errors:
api.logger.Errorf("[%s:%d] %v -> %s:%s:%s", filename, line, err, m, subdomain, path)
continue
@@ -668,19 +673,21 @@ func removeDuplicates(elements []string) (result []string) {
// Party returns a new child Party which inherites its
// parent's options and middlewares.
-// If "relativePath" matches the parent's one then it returns the current Party.
// A Party groups routes which may have the same prefix or subdomain and share same middlewares.
//
// To create a group of routes for subdomains
// use the `Subdomain` or `WildcardSubdomain` methods
-// or pass a "relativePath" as "admin." or "*." respectfully.
+// or pass a "relativePath" of "admin." or "*." respectfully.
func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) Party {
// if app.Party("/"), root party or app.Party("/user") == app.Party("/user")
// then just add the middlewares and return itself.
- if relativePath == "" || api.relativePath == relativePath {
- api.Use(handlers...)
- return api
- }
+ // if relativePath == "" || api.relativePath == relativePath {
+ // api.Use(handlers...)
+ // return api
+ // }
+ // ^ No, this is wrong, let the developer do its job, if she/he wants a copy let have it,
+ // it's a pure check as well, a path can be the same even if it's the same as its parent, i.e.
+ // app.Party("/user").Party("/user") should result in a /user/user, not a /user.
parentPath := api.relativePath
dot := string(SubdomainPrefix[0])
@@ -712,10 +719,17 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
allowMethods := make([]string, len(api.allowMethods))
copy(allowMethods, api.allowMethods)
+ // make a copy of the parent properties.
+ var properties context.Map
+ for k, v := range api.properties {
+ properties[k] = v
+ }
+
childAPI := &APIBuilder{
// global/api builder
logger: api.logger,
macros: api.macros,
+ properties: properties,
routes: api.routes,
routesNoLog: api.routesNoLog,
beginGlobalHandlers: api.beginGlobalHandlers,
@@ -808,6 +822,16 @@ func (api *APIBuilder) Macros() *macro.Macros {
return api.macros
}
+// Properties returns the original Party's properties map,
+// it can be modified before server startup but not afterwards.
+func (api *APIBuilder) Properties() context.Map {
+ if api.properties == nil {
+ api.properties = make(context.Map)
+ }
+
+ return api.properties
+}
+
// GetRoutes returns the routes information,
// some of them can be changed at runtime some others not.
//
@@ -1096,6 +1120,8 @@ func (api *APIBuilder) DoneGlobal(handlers ...context.Handler) {
// RemoveHandler deletes a handler from begin and done handlers
// based on its name or the handler pc function.
+// Note that UseGlobal and DoneGlobal handlers cannot be removed
+// through this method as they were registered to the routes already.
//
// As an exception, if one of the arguments is a pointer to an int,
// then this is used to set the total amount of removed handlers.
diff --git a/core/router/party.go b/core/router/party.go
index 016c6d7e..06fe1f66 100644
--- a/core/router/party.go
+++ b/core/router/party.go
@@ -39,6 +39,10 @@ type Party interface {
// Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path
Macros() *macro.Macros
+ // Properties returns the original Party's properties map,
+ // it can be modified before server startup but not afterwards.
+ Properties() context.Map
+
// SetRoutesNoLog disables (true) the verbose logging for the next registered
// routes under this Party and its children.
//
diff --git a/core/router/route.go b/core/router/route.go
index 1208bf5d..6df3190f 100644
--- a/core/router/route.go
+++ b/core/router/route.go
@@ -19,6 +19,8 @@ import (
// If any of the following fields are changed then the
// caller should Refresh the router.
type Route struct {
+ // The Party which this Route was created and registered on.
+ Party Party
Title string `json:"title"` // custom name to replace the method on debug logging.
Name string `json:"name"` // "userRoute"
Description string `json:"description"` // "lists a user"
@@ -86,7 +88,7 @@ type Route struct {
// handlers and the macro container which all routes should share.
// It parses the path based on the "macros",
// handlers are being changed to validate the macros at serve time, if needed.
-func NewRoute(statusErrorCode int, method, subdomain, unparsedPath string,
+func NewRoute(p Party, statusErrorCode int, method, subdomain, unparsedPath string,
handlers context.Handlers, macros macro.Macros) (*Route, error) {
tmpl, err := macro.Parse(unparsedPath, macros)
if err != nil {
@@ -110,6 +112,7 @@ func NewRoute(statusErrorCode int, method, subdomain, unparsedPath string,
formattedPath := formatPath(path)
route := &Route{
+ Party: p,
StatusCode: statusErrorCode,
Name: defaultName,
Method: method,
@@ -583,6 +586,8 @@ type routeReadOnlyWrapper struct {
*Route
}
+var _ context.RouteReadOnly = routeReadOnlyWrapper{}
+
func (rd routeReadOnlyWrapper) StatusErrorCode() int {
return rd.Route.StatusCode
}
@@ -619,6 +624,17 @@ func (rd routeReadOnlyWrapper) MainHandlerIndex() int {
return rd.Route.MainHandlerIndex
}
+func (rd routeReadOnlyWrapper) Property(key string) (interface{}, bool) {
+ properties := rd.Route.Party.Properties()
+ if properties != nil {
+ if property, ok := properties[key]; ok {
+ return property, true
+ }
+ }
+
+ return nil, false
+}
+
func (rd routeReadOnlyWrapper) GetLastMod() time.Time {
return rd.Route.LastMod
}
diff --git a/go.mod b/go.mod
index ba0f98a0..9b1ffea7 100644
--- a/go.mod
+++ b/go.mod
@@ -8,20 +8,20 @@ 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
github.com/json-iterator/go v1.1.10
github.com/kataras/blocks v0.0.4
github.com/kataras/golog v0.1.6
- github.com/kataras/jwt v0.0.8
+ github.com/kataras/jwt v0.0.9
github.com/kataras/neffos v0.0.18
github.com/kataras/pio v0.0.10
github.com/kataras/sitemap v0.0.5
diff --git a/hero/reflect.go b/hero/reflect.go
index 64c53eba..4619c91b 100644
--- a/hero/reflect.go
+++ b/hero/reflect.go
@@ -18,7 +18,7 @@ func valueOf(v interface{}) reflect.Value {
// indirectType returns the value of a pointer-type "typ".
// If "typ" is a pointer, array, chan, map or slice it returns its Elem,
-// otherwise returns the typ as it's.
+// otherwise returns the "typ" as it is.
func indirectType(typ reflect.Type) reflect.Type {
switch typ.Kind() {
case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
diff --git a/middleware/accesslog/accesslog.go b/middleware/accesslog/accesslog.go
index 195669b3..99b89415 100644
--- a/middleware/accesslog/accesslog.go
+++ b/middleware/accesslog/accesslog.go
@@ -323,6 +323,18 @@ func New(w io.Writer) *AccessLog {
//
// It panics on error.
func File(path string) *AccessLog {
+ f := mustOpenFile(path)
+ return New(bufio.NewReadWriter(bufio.NewReader(f), bufio.NewWriter(f)))
+}
+
+// FileUnbuffered same as File but it does not buffer the data,
+// it flushes the loggers contents as soon as possible.
+func FileUnbuffered(path string) *AccessLog {
+ f := mustOpenFile(path)
+ return New(f)
+}
+
+func mustOpenFile(path string) *os.File {
// Note: we add os.RDWR in order to be able to read from it,
// some formatters (e.g. CSV) needs that.
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
@@ -330,7 +342,7 @@ func File(path string) *AccessLog {
panic(err)
}
- return New(bufio.NewReadWriter(bufio.NewReader(f), bufio.NewWriter(f)))
+ return f
}
// Broker creates or returns the broker.
diff --git a/mvc/versioning.go b/mvc/versioning.go
index c16c2889..fcaf3078 100644
--- a/mvc/versioning.go
+++ b/mvc/versioning.go
@@ -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
diff --git a/sessions/sessiondb/boltdb/database.go b/sessions/sessiondb/boltdb/database.go
index a2b2335c..11b667e9 100644
--- a/sessions/sessiondb/boltdb/database.go
+++ b/sessions/sessiondb/boltdb/database.go
@@ -339,14 +339,14 @@ func (db *Database) Visit(sid string, cb func(key string, value interface{})) er
}
// Len returns the length of the session's entries (keys).
-func (db *Database) Len(sid string) (n int64) {
+func (db *Database) Len(sid string) (n int) {
err := db.Service.View(func(tx *bolt.Tx) error {
b := db.getBucketForSession(tx, sid)
if b == nil {
return nil
}
- n = int64(b.Stats().KeyN)
+ n = int(int64(b.Stats().KeyN))
return nil
})
diff --git a/versioning/deprecation.go b/versioning/deprecation.go
index 7e28d421..a67a9f1e 100644
--- a/versioning/deprecation.go
+++ b/versioning/deprecation.go
@@ -6,10 +6,17 @@ import (
"github.com/kataras/iris/v12/context"
)
+// The response header keys when a resource is deprecated by the server.
+const (
+ APIWarnHeader = "X-Api-Warn"
+ APIDeprecationDateHeader = "X-Api-Deprecation-Date"
+ APIDeprecationInfoHeader = "X-Api-Deprecation-Info"
+)
+
// 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
+// - "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
@@ -37,14 +44,14 @@ func WriteDeprecated(ctx *context.Context, options DeprecationOptions) {
options.WarnMessage = DefaultDeprecationOptions.WarnMessage
}
- ctx.Header("X-API-Warn", options.WarnMessage)
+ ctx.Header(APIWarnHeader, options.WarnMessage)
if !options.DeprecationDate.IsZero() {
- ctx.Header("X-API-Deprecation-Date", context.FormatTime(ctx, options.DeprecationDate))
+ ctx.Header(APIDeprecationDateHeader, context.FormatTime(ctx, options.DeprecationDate))
}
if options.DeprecationInfo != "" {
- ctx.Header("X-API-Deprecation-Info", options.DeprecationInfo)
+ ctx.Header(APIDeprecationInfoHeader, options.DeprecationInfo)
}
}
diff --git a/versioning/group.go b/versioning/group.go
index 9e7f0e06..10390133 100644
--- a/versioning/group.go
+++ b/versioning/group.go
@@ -1,39 +1,107 @@
package versioning
import (
+ "strings"
+
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/core/router"
+
+ "github.com/blang/semver/v4"
)
-// Group is a group of version-based routes.
-// One version per one or more routes.
-type Group struct {
- router.Party
+// 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
- // Information not currently in-use.
- version string
+// 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
+
+ validate semver.Range
deprecation DeprecationOptions
}
-// NewGroup returns a ptr to Group based on the given "version".
-// It sets the API Version for the "r" Party.
+// 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).
//
-// See `Handle` for more.
+// 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)
+// }
//
-// Example: _examples/routing/versioning
+// ctx.Next()
+// })
+// OR:
+// api.Use(versioning.FromQuery("version", ""))
+//
+// Examples at: _examples/routing/versioning
// Usage:
-// api := versioning.NewGroup(Parent_Party, ">= 1, < 2")
-// api.Get/Post/Put/Delete...
-func NewGroup(r router.Party, version string) *Group {
+// app := iris.New()
+// api := app.Party("/api")
+// v1 := versioning.NewGroup(api, ">=1.0.0 <2.0.0")
+// v1.Get/Post/Put/Delete...
+//
+// 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("/")
+
// 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{
- Party: r,
- version: version,
+ API: r,
+ validate: verRange,
}
}
@@ -43,9 +111,27 @@ func (g *Group) Deprecated(options DeprecationOptions) *Group {
// store it for future use, e.g. collect all deprecated APIs and notify the developer.
g.deprecation = options
- g.Party.UseOnce(func(ctx *context.Context) {
+ g.API.UseOnce(func(ctx *context.Context) {
WriteDeprecated(ctx, options)
ctx.Next()
})
return g
}
+
+func makeHandler(validate semver.Range) context.Handler {
+ return func(ctx *context.Context) {
+ 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()
+ }
+}
diff --git a/versioning/group_test.go b/versioning/group_test.go
new file mode 100644
index 00000000..8512bc16
--- /dev/null
+++ b/versioning/group_test.go
@@ -0,0 +1,56 @@
+package versioning_test
+
+import (
+ "testing"
+
+ "github.com/kataras/iris/v12"
+ "github.com/kataras/iris/v12/httptest"
+ "github.com/kataras/iris/v12/versioning"
+)
+
+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")
+}
diff --git a/versioning/version.go b/versioning/version.go
index 26be5541..cf2adc87 100644
--- a/versioning/version.go
+++ b/versioning/version.go
@@ -5,31 +5,31 @@ import (
"strings"
"github.com/kataras/iris/v12/context"
+
+ "github.com/blang/semver/v4"
)
const (
+ // APIVersionResponseHeader the response header which its value contains
+ // the normalized semver matched version.
+ APIVersionResponseHeader = "X-Api-Version"
// 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 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()
- // }
- //
- // DEPRECATED: Use:
- // version := ctx.URLParamDefault("version", "1")
- // versioning.SetVersion(ctx, version) instead.
- Key = "iris.api.version"
// NotFound is the key that can be used inside a `Map` or inside `ctx.SetVersion(versioning.NotFound)`
- // to tell that a version wasn't found, therefore the not found handler should handle the request instead.
+ // to tell that a version wasn't found, therefore the `NotFoundHandler` should handle the request instead.
NotFound = "iris.api.version.notfound"
+ // Empty is just an empty string. Can be used as a key for a version alias
+ // when the requested version of a resource was not even specified by the client.
+ // The difference between NotFound and Empty is important when version aliases are registered:
+ // - A NotFound cannot be registered as version alias, it
+ // means that the client sent a version with its request
+ // but that version was not implemented by the server.
+ // - An Empty indicates that the client didn't send any version at all.
+ Empty = ""
)
// ErrNotFound reports whether a requested version
@@ -54,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).
@@ -107,7 +185,129 @@ func GetVersion(ctx *context.Context) string {
// SetVersion force-sets the API Version.
// It can be used inside a middleware.
+// 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.0.0")
+// versioning.SetVersion(ctx, version)
+// ctx.Next()
+// }
// See `GetVersion` too.
func SetVersion(ctx *context.Context, constraint string) {
ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetVersionContextKey(), constraint)
}
+
+// AliasMap is just a type alias of the standard map[string]string.
+// Head over to the `Aliases` function below for more.
+type AliasMap = map[string]string
+
+// Aliases is a middleware which registers version constraint aliases
+// for the children Parties(routers). It's respected by versioning Groups.
+//
+// Example Code:
+// app := iris.New()
+//
+// api := app.Party("/api")
+// api.Use(Aliases(map[string]string{
+// 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.0.0 < 2.0.0")
+// v1.Get/Post...
+//
+// v4 := NewGroup(api, ">=4.0.0 < 5.0.0")
+// v4.Get/Post...
+//
+// stage := NewGroup(api, "5.0.0-alpha")
+// stage.Get/Post...
+func Aliases(aliases AliasMap) context.Handler {
+ cp := make(AliasMap, len(aliases)) // copy the map here so we are safe of later modifications by end-dev.
+ for k, v := range aliases {
+ cp[k] = v
+ }
+
+ return func(ctx *context.Context) {
+ SetVersionAliases(ctx, cp, true)
+ ctx.Next()
+ }
+}
+
+// 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 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.
+func GetVersionAlias(ctx *context.Context, gotVersion string) (string, bool) {
+ key := ctx.Application().ConfigurationReadOnly().GetVersionAliasesContextKey()
+ if key == "" {
+ return "", false
+ }
+
+ v := ctx.Values().Get(key)
+ if v == nil {
+ return "", false
+ }
+
+ aliases, ok := v.(AliasMap)
+ if !ok {
+ return "", false
+ }
+
+ version, ok := aliases[gotVersion]
+ if !ok {
+ return "", false
+ }
+
+ return strings.TrimSpace(version), true
+}
+
+// SetVersionAliases sets a map of version aliases when a requested
+// version of a resource was not implemented by the server.
+// Can be used inside a middleware to the parent Party
+// and always before the child versioning groups (see `Aliases` function).
+//
+// The map's key (string) should be the "got version" (by the client)
+// and the value should be the "version constraint to match" instead.
+// The map's value(string) should be a registered version
+// otherwise it will hit the NotFoundHandler (501, "version not found" by default).
+//
+// The given "aliases" is a type of standard map[string]string and
+// should NOT be modified afterwards.
+//
+// The last "override" input argument indicates whether any
+// existing aliases, registered by previous handlers in the chain,
+// 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 == "" {
+ return
+ }
+
+ v := ctx.Values().Get(key)
+ if v == nil || override {
+ ctx.Values().Set(key, aliases)
+ return
+ }
+
+ if existing, ok := v.(AliasMap); ok {
+ for k, v := range aliases {
+ existing[k] = v
+ }
+ }
+}
diff --git a/versioning/version_test.go b/versioning/version_test.go
index eab10ea9..f403fae2 100644
--- a/versioning/version_test.go
+++ b/versioning/version_test.go
@@ -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().
@@ -46,3 +55,53 @@ func TestGetVersion(t *testing.T) {
e.GET("/manual").Expect().Status(iris.StatusOK).Body().Equal("11.0.5")
}
+
+func TestVersionAliases(t *testing.T) {
+ app := iris.New()
+
+ api := app.Party("/api")
+ api.Use(versioning.Aliases(map[string]string{
+ versioning.Empty: "1.0.0",
+ "stage": "2.0.0",
+ }))
+
+ writeVesion := func(ctx iris.Context) {
+ ctx.WriteString(versioning.GetVersion(ctx))
+ }
+
+ // A group without registration order.
+ v3 := versioning.NewGroup(api, ">= 3.0.0 < 4.0.0")
+ v3.Get("/", writeVesion)
+
+ v1 := versioning.NewGroup(api, ">= 1.0.0 < 2.0.0")
+ v1.Get("/", writeVesion)
+
+ v2 := versioning.NewGroup(api, ">= 2.0.0 < 3.0.0")
+ v2.Get("/", writeVesion)
+
+ api.Get("/manual", func(ctx iris.Context) {
+ versioning.SetVersion(ctx, "12.0.0")
+ ctx.Next()
+ }, writeVesion)
+
+ e := httptest.New(t, app)
+
+ // Make sure the SetVersion still works.
+ e.GET("/api/manual").Expect().Status(iris.StatusOK).Body().Equal("12.0.0")
+
+ // Test Empty default.
+ 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.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.0.0").Expect().
+ Status(iris.StatusOK).Body().Equal("2.0.0")
+ // Test version 3 (registered first).
+ e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "3.1.0").Expect().
+ Status(iris.StatusOK).Body().Equal("3.1.0")
+}
diff --git a/versioning/versioning.go b/versioning/versioning.go
deleted file mode 100644
index 4041f304..00000000
--- a/versioning/versioning.go
+++ /dev/null
@@ -1,154 +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) {
- 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).
-func Match(ctx *context.Context, expectedVersion string) bool {
- versionString, matched := check(GetVersion(ctx), expectedVersion)
- if !matched {
- return false
- }
-
- SetVersion(ctx, versionString)
- ctx.Header("X-API-Version", 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("X-API-Version", 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
-}
diff --git a/versioning/versioning_test.go b/versioning/versioning_test.go
deleted file mode 100644
index 536b39bc..00000000
--- a/versioning/versioning_test.go
+++ /dev/null
@@ -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")
-}