Merge branch 'master' into master

This commit is contained in:
Gerasimos (Makis) Maropoulos 2021-01-09 03:37:54 +02:00 committed by GitHub
commit 387eac8672
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 747 additions and 478 deletions

View File

@ -14,4 +14,3 @@ enabled = true
[analyzers.meta] [analyzers.meta]
import_paths = ["github.com/kataras/iris"] import_paths = ["github.com/kataras/iris"]

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
coverage.out coverage.out
package-lock.json package-lock.json
go.sum go.sum
access.log
node_modules node_modules
issue-*/ issue-*/
internalcode-*/ internalcode-*/

View File

@ -28,6 +28,29 @@ The codebase for Dependency Injection, Internationalization and localization and
## Fixes and Improvements ## 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. - 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 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. - 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 ## 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. - 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 `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. - 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
View File

@ -44,9 +44,9 @@ Revision ID: 5fc50a00491616d5cd0cbce3abd8b699838e25ca
easyjson 8ab5ff9cd8e4e43 https://github.com/mailru/easyjson easyjson 8ab5ff9cd8e4e43 https://github.com/mailru/easyjson
2e8b79f6c47d324 2e8b79f6c47d324
a31dd803cf a31dd803cf
go-version 2b13044f5cdd383 https://github.com/hashicorp/go-version semver 4487282d78122a2 https://github.com/blang/semver
3370d41ce57d8bf 45e413d7515e7c5
3cec5e62b8 16b70c33fd
golog f7561df84e64ab9 https://github.com/kataras/golog golog f7561df84e64ab9 https://github.com/kataras/golog
212f021923ce4ff 212f021923ce4ff
db5df5594d db5df5594d

View File

@ -1,10 +1,15 @@
[![Black Lives Matter](https://iris-go.com/images/blacklivesmatter_banner.png)](https://support.eji.org/give/153413/#!/donation/checkout) [![Black Lives Matter](https://iris-go.com/images/blacklivesmatter_banner.png)](https://support.eji.org/give/153413/#!/donation/checkout)
<!-- # News --> <!-- # News
> This is the under-**development branch**. Stay tuned for the upcoming release [v12.2.0](HISTORY.md#Next). Looking for a stable release? Head over to the [v12.1.8 branch](https://github.com/kataras/iris/tree/v12.1.8) instead. > This is the under-**development branch**. Stay tuned for the upcoming release [v12.2.0](HISTORY.md#Next). Looking for a stable release? Head over to the [v12.1.8 branch](https://github.com/kataras/iris/tree/v12.1.8) instead.
> >
> ![](https://iris-go.com/images/cli.png) Try the official [Iris Command Line Interface](https://github.com/kataras/iris-cli) today! > ![](https://iris-go.com/images/cli.png) Try the official [Iris Command Line Interface](https://github.com/kataras/iris-cli) today!
-->
# 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.
<!-- ![](https://iris-go.com/images/release.png) Iris version **12.1.8** has been [released](HISTORY.md#su-16-february-2020--v1218)! --> <!-- ![](https://iris-go.com/images/release.png) Iris version **12.1.8** has been [released](HISTORY.md#su-16-february-2020--v1218)! -->
@ -31,6 +36,13 @@ With your help, we can improve Open Source web development for everyone!
> Donations from **China** are now accepted! > Donations from **China** are now accepted!
<p> <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>
<a href="https://github.com/rhernandez-itemsoft"><img src="https://avatars1.githubusercontent.com/u/4327356?v=4" alt ="Ricardo Hernandez Lopez" title="rhernandez-itemsoft" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
<a href="https://github.com/ChinChuanKuo"><img src="https://avatars1.githubusercontent.com/u/11756978?v=4" alt ="ChinChuanKuo" title="ChinChuanKuo" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
<a href="https://github.com/nikharsaxena"><img src="https://avatars1.githubusercontent.com/u/8684362?v=4" alt ="Nikhar Saxena" title="nikharsaxena" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
<a href="https://github.com/fenriz07"><img src="https://avatars1.githubusercontent.com/u/9199380?v=4" alt ="Servio Zambrano" title="fenriz07" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a> <a href="https://github.com/fenriz07"><img src="https://avatars1.githubusercontent.com/u/9199380?v=4" alt ="Servio Zambrano" title="fenriz07" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
<a href="https://github.com/NA"><img src="https://avatars1.githubusercontent.com/u/1600?v=4" alt ="Nate Anderson" title="NA" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a> <a href="https://github.com/NA"><img src="https://avatars1.githubusercontent.com/u/1600?v=4" alt ="Nate Anderson" title="NA" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
<a href="https://github.com/claudemuller"><img src="https://avatars1.githubusercontent.com/u/8104894?v=4" alt ="Claude Muller" title="claudemuller" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a> <a href="https://github.com/claudemuller"><img src="https://avatars1.githubusercontent.com/u/8104894?v=4" alt ="Claude Muller" title="claudemuller" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>

View File

@ -64,14 +64,11 @@ func main() {
} }
func handler(ctx iris.Context) { func handler(ctx iris.Context) {
// username, password, _ := ctx.Request().BasicAuth() // user := ctx.User().(*myUserType)
// third parameter it will be always true because the middleware // or ctx.User().GetRaw().(*myUserType)
// makes sure for that, otherwise this handler will not be executed. // ctx.Writef("%s %s:%s", ctx.Path(), user.Username, user.Password)
// OR: // OR if you don't have registered custom User structs:
user := ctx.User() username, password, _ := ctx.Request().BasicAuth()
// OR ctx.User().GetRaw() to get the underline value.
username, _ := user.GetUsername()
password, _ := user.GetPassword()
ctx.Writef("%s %s:%s", ctx.Path(), username, password) ctx.Writef("%s %s:%s", ctx.Path(), username, password)
} }

View File

@ -4,16 +4,10 @@ import (
"time" "time"
"github.com/kataras/iris/v12" "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: Documentation:
https://github.com/kataras/jwt#table-of-contents 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"). // Just an example on how you can retrieve all the standard claims (set by jwt.MaxAge, "exp").
standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims
expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
timeLeft := standardClaims.Timeleft() timeLeft := standardClaims.Timeleft()

View File

@ -29,7 +29,7 @@ type response struct {
Timestamp int64 `json:"timestamp,omitempty"` 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 { if r.ID > 0 {
r.Timestamp = time.Now().Unix() r.Timestamp = time.Now().Unix()
} }
@ -64,15 +64,15 @@ type user struct {
ID uint64 `json:"id"` ID uint64 `json:"id"`
} }
func (c *controller) GetBy(userid uint64) response { func (c *controller) GetBy(userid uint64) *response {
if userid != 1 { if userid != 1 {
return response{ return &response{
Code: iris.StatusNotFound, Code: iris.StatusNotFound,
Message: "User Not Found", Message: "User Not Found",
} }
} }
return response{ return &response{
ID: userid, ID: userid,
Data: user{ID: userid}, Data: user{ID: userid},
} }

View File

@ -28,9 +28,9 @@ func newApp() *iris.Application {
{ {
m := mvc.New(dataRouter) m := mvc.New(dataRouter)
m.Handle(new(v1Controller), mvc.Version("1"), mvc.Deprecated(opts)) // 1 or 1.0, 1.0.0 ... m.Handle(new(v1Controller), mvc.Version("1.0.0"), mvc.Deprecated(opts))
m.Handle(new(v2Controller), mvc.Version("2.3")) // 2.3 or 2.3.0 m.Handle(new(v2Controller), mvc.Version("2.3.0"))
m.Handle(new(v3Controller), mvc.Version(">=3, <4")) // 3, 3.x, 3.x.x ... 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. m.Handle(new(noVersionController)) // or if missing it will respond with 501 version not found.
} }

View File

@ -12,21 +12,21 @@ func TestVersionedController(t *testing.T) {
app := newApp() app := newApp()
e := httptest.New(t, app) 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)") Status(iris.StatusOK).Body().Equal("data (v1.x)")
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "2.3.0").Expect(). e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "2.3.0").Expect().
Status(iris.StatusOK).Body().Equal("data (v2.x)") 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)") Status(iris.StatusOK).Body().Equal("data (v3.x)")
// Test invalid version or no version at all. // 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") Status(iris.StatusOK).Body().Equal("data")
e.GET("/data").Expect(). e.GET("/data").Expect().
Status(iris.StatusOK).Body().Equal("data") Status(iris.StatusOK).Body().Equal("data")
// Test Deprecated (v1) // 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.Status(iris.StatusOK).Body().Equal("data (v1.x)")
ex.Header("X-API-Warn").Equal(opts.WarnMessage) ex.Header("X-API-Warn").Equal(opts.WarnMessage)
expectedDateStr := opts.DeprecationDate.Format(app.ConfigurationReadOnly().GetTimeFormat()) expectedDateStr := opts.DeprecationDate.Format(app.ConfigurationReadOnly().GetTimeFormat())

View File

@ -152,7 +152,7 @@ func newApp() *iris.Application {
} }
// wildcard subdomains. // wildcard subdomains.
wildcardSubdomain := app.Party("*.") wildcardSubdomain := app.WildcardSubdomain()
{ {
wildcardSubdomain.Get("/", func(ctx iris.Context) { wildcardSubdomain.Get("/", func(ctx iris.Context) {
ctx.Writef("Subdomain can be anything, now you're here from: %s", ctx.Subdomain()) ctx.Writef("Subdomain can be anything, now you're here from: %s", ctx.Subdomain())

View File

@ -82,11 +82,11 @@ func registerGamesRoutes(app *iris.Application) {
} }
func registerSubdomains(app *iris.Application) { func registerSubdomains(app *iris.Application) {
mysubdomain := app.Party("mysubdomain.") mysubdomain := app.Subdomain("mysubdomain")
// http://mysubdomain.myhost.com // http://mysubdomain.myhost.com
mysubdomain.Get("/", h) mysubdomain.Get("/", h)
willdcardSubdomain := app.Party("*.") willdcardSubdomain := app.WildcardSubdomain()
willdcardSubdomain.Get("/", h) willdcardSubdomain.Get("/", h)
willdcardSubdomain.Party("/party").Get("/", h) willdcardSubdomain.Party("/party").Get("/", h)
} }

View File

@ -116,7 +116,7 @@ func main() {
adminRoutes.Get("/settings", info) adminRoutes.Get("/settings", info)
// Wildcard/dynamic subdomain // Wildcard/dynamic subdomain
dynamicSubdomainRoutes := app.Party("*.") dynamicSubdomainRoutes := app.WildcardSubdomain()
// GET: http://any_thing_here.localhost:8080 // GET: http://any_thing_here.localhost:8080
dynamicSubdomainRoutes.Get("/", info) dynamicSubdomainRoutes.Get("/", info)

View File

@ -33,7 +33,7 @@ func main() {
}*/ }*/
// no order, you can register subdomains at the end also. // no order, you can register subdomains at the end also.
dynamicSubdomains := app.Party("*.") dynamicSubdomains := app.WildcardSubdomain()
{ {
dynamicSubdomains.Get("/", dynamicSubdomainHandler) dynamicSubdomains.Get("/", dynamicSubdomainHandler)

View File

@ -8,71 +8,95 @@ import (
func main() { func main() {
app := iris.New() app := iris.New()
examplePerRoute(app) app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) {
examplePerParty(app) 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") app.Listen(":8080")
} }
// How to test: func testHandler(v string) iris.Handler {
// Open Postman return func(ctx iris.Context) {
// GET: localhost:8080/api/cats ctx.JSON(iris.Map{
// Headers[1] = Accept-Version: "1" and repeat with "version": v,
// Headers[1] = Accept-Version: "2.5" "message": "Hello, world!",
// 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,
}))
} }
// How to test: func testError(v string) iris.Handler {
// Open Postman return func(ctx iris.Context) {
// GET: localhost:8080/api/users ctx.Writef("not found: %s", v)
// 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 catsVersionExactly1Handler(ctx iris.Context) { func testView(ctx iris.Context) {
ctx.Writef("v1 exactly resource: /api/cats handler") ctx.View("index.html")
}
func catsV2Handler(ctx iris.Context) {
ctx.Writef("v2 resource: /api/cats handler")
} }

View File

@ -0,0 +1 @@
<h1>This is the directory for version 1 templates</h1>

View File

@ -0,0 +1 @@
<h1>This is the directory for version 2 templates</h1>

View File

@ -0,0 +1 @@
<h1>This is the directory for version 3 templates</h1>

View File

@ -114,7 +114,7 @@ func NewApp(sess *sessions.Sessions) *iris.Application {
app.Get("/delete", func(ctx iris.Context) { app.Get("/delete", func(ctx iris.Context) {
session := sessions.Get(ctx) session := sessions.Get(ctx)
// delete a specific key // delete a specific key
session.Delete("name") session.Delete("username")
}) })
app.Get("/clear", func(ctx iris.Context) { app.Get("/clear", func(ctx iris.Context) {

View File

@ -16,12 +16,12 @@ func TestSessionsEncodeDecode(t *testing.T) {
es.Cookies().NotEmpty() es.Cookies().NotEmpty()
es.Body().Equal("All ok session set to: iris [isNew=true]") 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 // delete and re-get
e.GET("/delete").Expect().Status(iris.StatusOK) 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 // set, clear and re-get
e.GET("/set").Expect().Body().Equal("All ok session set to: iris [isNew=false]") e.GET("/set").Expect().Body().Equal("All ok session set to: iris [isNew=false]")
e.GET("/clear").Expect().Status(iris.StatusOK) 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: ")
} }

View File

@ -37,6 +37,11 @@ func h(ctx iris.Context) {
// third parameter it will be always true because the middleware // third parameter it will be always true because the middleware
// makes sure for that, otherwise this handler will not be executed. // makes sure for that, otherwise this handler will not be executed.
// OR: // 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().GetUsername()
// ctx.User().GetPassword() // ctx.User().GetPassword()
ctx.Writef("%s %s:%s", ctx.Path(), username, password) ctx.Writef("%s %s:%s", ctx.Path(), username, password)

View File

@ -27,7 +27,7 @@ func main() {
// wildcard subdomain, will catch username1.... username2.... username3... username4.... username5... // 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. // 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 := subdomain.Get("/mypath", emptyHandler)
mypathRoute.Name = "my-page1" mypathRoute.Name = "my-page1"

View File

@ -764,9 +764,14 @@ type Configuration struct {
// Defaults to "iris.locale.language.input". // Defaults to "iris.locale.language.input".
LanguageInputContextKey string `ini:"language_input_context_key" json:"languageInputContextKey,omitempty" yaml:"LanguageInputContextKey" toml:"LanguageInputContextKey"` 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 // 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". // Defaults to "iris.api.version".
VersionContextKey string `ini:"version_context_key" json:"versionContextKey" yaml:"VersionContextKey" toml:"VersionContextKey"` 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 // ViewEngineContextKey is the context's values key
// responsible to store and retrieve(view.Engine) the current view engine. // responsible to store and retrieve(view.Engine) the current view engine.
// A middleware or a Party can modify its associated value to change // A middleware or a Party can modify its associated value to change
@ -974,6 +979,11 @@ func (c Configuration) GetVersionContextKey() string {
return c.VersionContextKey return c.VersionContextKey
} }
// GetVersionAliasesContextKey returns the VersionAliasesContextKey field.
func (c Configuration) GetVersionAliasesContextKey() string {
return c.VersionAliasesContextKey
}
// GetViewEngineContextKey returns the ViewEngineContextKey field. // GetViewEngineContextKey returns the ViewEngineContextKey field.
func (c Configuration) GetViewEngineContextKey() string { func (c Configuration) GetViewEngineContextKey() string {
return c.ViewEngineContextKey return c.ViewEngineContextKey
@ -1132,6 +1142,10 @@ func WithConfiguration(c Configuration) Configurator {
main.VersionContextKey = v main.VersionContextKey = v
} }
if v := c.VersionAliasesContextKey; v != "" {
main.VersionAliasesContextKey = v
}
if v := c.ViewEngineContextKey; v != "" { if v := c.ViewEngineContextKey; v != "" {
main.ViewEngineContextKey = v main.ViewEngineContextKey = v
} }
@ -1210,6 +1224,7 @@ func DefaultConfiguration() Configuration {
LanguageContextKey: "iris.locale.language", LanguageContextKey: "iris.locale.language",
LanguageInputContextKey: "iris.locale.language.input", LanguageInputContextKey: "iris.locale.language.input",
VersionContextKey: "iris.api.version", VersionContextKey: "iris.api.version",
VersionAliasesContextKey: "iris.api.version.aliases",
ViewEngineContextKey: "iris.view.engine", ViewEngineContextKey: "iris.view.engine",
ViewLayoutContextKey: "iris.view.layout", ViewLayoutContextKey: "iris.view.layout",
ViewDataContextKey: "iris.view.data", ViewDataContextKey: "iris.view.data",

View File

@ -53,6 +53,8 @@ type ConfigurationReadOnly interface {
GetLanguageInputContextKey() string GetLanguageInputContextKey() string
// GetVersionContextKey returns the VersionContextKey field. // GetVersionContextKey returns the VersionContextKey field.
GetVersionContextKey() string GetVersionContextKey() string
// GetVersionAliasesContextKey returns the VersionAliasesContextKey field.
GetVersionAliasesContextKey() string
// GetViewEngineContextKey returns the ViewEngineContextKey field. // GetViewEngineContextKey returns the ViewEngineContextKey field.
GetViewEngineContextKey() string GetViewEngineContextKey() string

View File

@ -1974,6 +1974,13 @@ func (ctx *Context) UploadFormFiles(destDirectory string, before ...func(*Contex
for _, files := range fhs { for _, files := range fhs {
innerLoop: innerLoop:
for _, file := range files { 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 { for _, b := range before {
if !b(ctx, file) { if !b(ctx, file) {

View File

@ -81,15 +81,15 @@ to the end-developer's custom implementations.
// SimpleUser is a simple implementation of the User interface. // SimpleUser is a simple implementation of the User interface.
type SimpleUser struct { type SimpleUser struct {
Authorization string `json:"authorization,omitempty"` Authorization string `json:"authorization,omitempty" db:"authorization"`
AuthorizedAt time.Time `json:"authorized_at,omitempty"` AuthorizedAt time.Time `json:"authorized_at,omitempty" db:"authorized_at"`
ID string `json:"id,omitempty"` ID string `json:"id,omitempty" db:"id"`
Username string `json:"username,omitempty"` Username string `json:"username,omitempty" db:"username"`
Password string `json:"password,omitempty"` Password string `json:"password,omitempty" db:"password"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty" db:"email"`
Roles []string `json:"roles,omitempty"` Roles []string `json:"roles,omitempty" db:"roles"`
Token json.RawMessage `json:"token,omitempty"` Token json.RawMessage `json:"token,omitempty" db:"token"`
Fields Map `json:"fields,omitempty"` Fields Map `json:"fields,omitempty" db:"fields"`
} }
var _ User = (*SimpleUser)(nil) var _ User = (*SimpleUser)(nil)

View File

@ -239,6 +239,10 @@ var ignoreMainHandlerNames = [...]string{
"iris.reCAPTCHA", "iris.reCAPTCHA",
"iris.profiling", "iris.profiling",
"iris.recover", "iris.recover",
"iris.accesslog",
"iris.grpc",
"iris.requestid",
"iris.rewrite",
} }
// ingoreMainHandlerName reports whether a main handler of "name" should // ingoreMainHandlerName reports whether a main handler of "name" should

View File

@ -62,6 +62,10 @@ type RouteReadOnly interface {
// MainHandlerIndex returns the first registered handler's index for the route. // MainHandlerIndex returns the first registered handler's index for the route.
MainHandlerIndex() int 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 // Sitemap properties: https://www.sitemaps.org/protocol.html
// GetLastMod returns the date of last modification of the file served by this route. // GetLastMod returns the date of last modification of the file served by this route.

View File

@ -164,7 +164,7 @@ func overlapRoute(r *Route, next *Route) {
// Version was not found: // Version was not found:
// we need to be able to send the status on the last not found version // 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. // 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) { if !errors.Is(ctx.GetErr(), context.ErrNotFound) {
ctx.StatusCode(prevStatusCode) ctx.StatusCode(prevStatusCode)
} }
@ -196,6 +196,11 @@ type APIBuilder struct {
// the api builder global macros registry // the api builder global macros registry
macros *macro.Macros 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 // the api builder global routes repository
routes *repository routes *repository
// disables the debug logging of routes under a per-party and its children. // 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)) routes := make([]*Route, len(methods))
for i, m := range methods { // single, empty method for error handlers. 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: if err != nil { // template path parser errors:
api.logger.Errorf("[%s:%d] %v -> %s:%s:%s", filename, line, err, m, subdomain, path) api.logger.Errorf("[%s:%d] %v -> %s:%s:%s", filename, line, err, m, subdomain, path)
continue continue
@ -668,19 +673,21 @@ func removeDuplicates(elements []string) (result []string) {
// Party returns a new child Party which inherites its // Party returns a new child Party which inherites its
// parent's options and middlewares. // 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. // A Party groups routes which may have the same prefix or subdomain and share same middlewares.
// //
// To create a group of routes for subdomains // To create a group of routes for subdomains
// use the `Subdomain` or `WildcardSubdomain` methods // 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 { func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) Party {
// if app.Party("/"), root party or app.Party("/user") == app.Party("/user") // if app.Party("/"), root party or app.Party("/user") == app.Party("/user")
// then just add the middlewares and return itself. // then just add the middlewares and return itself.
if relativePath == "" || api.relativePath == relativePath { // if relativePath == "" || api.relativePath == relativePath {
api.Use(handlers...) // api.Use(handlers...)
return api // 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 parentPath := api.relativePath
dot := string(SubdomainPrefix[0]) dot := string(SubdomainPrefix[0])
@ -712,10 +719,17 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
allowMethods := make([]string, len(api.allowMethods)) allowMethods := make([]string, len(api.allowMethods))
copy(allowMethods, 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{ childAPI := &APIBuilder{
// global/api builder // global/api builder
logger: api.logger, logger: api.logger,
macros: api.macros, macros: api.macros,
properties: properties,
routes: api.routes, routes: api.routes,
routesNoLog: api.routesNoLog, routesNoLog: api.routesNoLog,
beginGlobalHandlers: api.beginGlobalHandlers, beginGlobalHandlers: api.beginGlobalHandlers,
@ -808,6 +822,16 @@ func (api *APIBuilder) Macros() *macro.Macros {
return api.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, // GetRoutes returns the routes information,
// some of them can be changed at runtime some others not. // 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 // RemoveHandler deletes a handler from begin and done handlers
// based on its name or the handler pc function. // 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, // 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. // then this is used to set the total amount of removed handlers.

View File

@ -39,6 +39,10 @@ type Party interface {
// Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path // Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path
Macros() *macro.Macros 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 // SetRoutesNoLog disables (true) the verbose logging for the next registered
// routes under this Party and its children. // routes under this Party and its children.
// //

View File

@ -19,6 +19,8 @@ import (
// If any of the following fields are changed then the // If any of the following fields are changed then the
// caller should Refresh the router. // caller should Refresh the router.
type Route struct { 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. Title string `json:"title"` // custom name to replace the method on debug logging.
Name string `json:"name"` // "userRoute" Name string `json:"name"` // "userRoute"
Description string `json:"description"` // "lists a user" Description string `json:"description"` // "lists a user"
@ -86,7 +88,7 @@ type Route struct {
// handlers and the macro container which all routes should share. // handlers and the macro container which all routes should share.
// It parses the path based on the "macros", // It parses the path based on the "macros",
// handlers are being changed to validate the macros at serve time, if needed. // 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) { handlers context.Handlers, macros macro.Macros) (*Route, error) {
tmpl, err := macro.Parse(unparsedPath, macros) tmpl, err := macro.Parse(unparsedPath, macros)
if err != nil { if err != nil {
@ -110,6 +112,7 @@ func NewRoute(statusErrorCode int, method, subdomain, unparsedPath string,
formattedPath := formatPath(path) formattedPath := formatPath(path)
route := &Route{ route := &Route{
Party: p,
StatusCode: statusErrorCode, StatusCode: statusErrorCode,
Name: defaultName, Name: defaultName,
Method: method, Method: method,
@ -583,6 +586,8 @@ type routeReadOnlyWrapper struct {
*Route *Route
} }
var _ context.RouteReadOnly = routeReadOnlyWrapper{}
func (rd routeReadOnlyWrapper) StatusErrorCode() int { func (rd routeReadOnlyWrapper) StatusErrorCode() int {
return rd.Route.StatusCode return rd.Route.StatusCode
} }
@ -619,6 +624,17 @@ func (rd routeReadOnlyWrapper) MainHandlerIndex() int {
return rd.Route.MainHandlerIndex 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 { func (rd routeReadOnlyWrapper) GetLastMod() time.Time {
return rd.Route.LastMod return rd.Route.LastMod
} }

4
go.mod
View File

@ -8,20 +8,20 @@ require (
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398
github.com/andybalholm/brotli v1.0.1 github.com/andybalholm/brotli v1.0.1
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible 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/dgraph-io/badger/v2 v2.2007.2
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385
github.com/fatih/structs v1.1.0 github.com/fatih/structs v1.1.0
github.com/flosch/pongo2/v4 v4.0.1 github.com/flosch/pongo2/v4 v4.0.1
github.com/go-redis/redis/v8 v8.4.0 github.com/go-redis/redis/v8 v8.4.0
github.com/google/uuid v1.1.2 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/httpexpect/v2 v2.0.5
github.com/iris-contrib/jade v1.1.4 github.com/iris-contrib/jade v1.1.4
github.com/iris-contrib/schema v0.0.6 github.com/iris-contrib/schema v0.0.6
github.com/json-iterator/go v1.1.10 github.com/json-iterator/go v1.1.10
github.com/kataras/blocks v0.0.4 github.com/kataras/blocks v0.0.4
github.com/kataras/golog v0.1.6 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/neffos v0.0.18
github.com/kataras/pio v0.0.10 github.com/kataras/pio v0.0.10
github.com/kataras/sitemap v0.0.5 github.com/kataras/sitemap v0.0.5

View File

@ -18,7 +18,7 @@ func valueOf(v interface{}) reflect.Value {
// indirectType returns the value of a pointer-type "typ". // indirectType returns the value of a pointer-type "typ".
// If "typ" is a pointer, array, chan, map or slice it returns its Elem, // 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 { func indirectType(typ reflect.Type) reflect.Type {
switch typ.Kind() { switch typ.Kind() {
case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:

View File

@ -323,6 +323,18 @@ func New(w io.Writer) *AccessLog {
// //
// It panics on error. // It panics on error.
func File(path string) *AccessLog { 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, // Note: we add os.RDWR in order to be able to read from it,
// some formatters (e.g. CSV) needs that. // some formatters (e.g. CSV) needs that.
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 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) panic(err)
} }
return New(bufio.NewReadWriter(bufio.NewReader(f), bufio.NewWriter(f))) return f
} }
// Broker creates or returns the broker. // Broker creates or returns the broker.

View File

@ -8,14 +8,14 @@ import (
// Version returns a valid `Option` that can be passed to the `Application.Handle` method. // Version returns a valid `Option` that can be passed to the `Application.Handle` method.
// It requires a specific "version" constraint for a Controller, // 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: // Usage:
// m := mvc.New(dataRouter) // m := mvc.New(dataRouter)
// m.Handle(new(v1Controller), mvc.Version("1"), mvc.Deprecated(mvc.DeprecationOptions{})) // m.Handle(new(v1Controller), mvc.Version("1.0.0"), mvc.Deprecated(mvc.DeprecationOptions{}))
// m.Handle(new(v2Controller), mvc.Version("2.3")) // m.Handle(new(v2Controller), mvc.Version("2.3.0"))
// m.Handle(new(v3Controller), mvc.Version(">=3, <4")) // m.Handle(new(v3Controller), mvc.Version(">=3.0.0 <4.0.0"))
// m.Handle(new(noVersionController)) // m.Handle(new(noVersionController))
// //
// See the `versioning` package's documentation for more information on // See the `versioning` package's documentation for more information on

View File

@ -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). // 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 { err := db.Service.View(func(tx *bolt.Tx) error {
b := db.getBucketForSession(tx, sid) b := db.getBucketForSession(tx, sid)
if b == nil { if b == nil {
return nil return nil
} }
n = int64(b.Stats().KeyN) n = int(int64(b.Stats().KeyN))
return nil return nil
}) })

View File

@ -6,10 +6,17 @@ import (
"github.com/kataras/iris/v12/context" "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. // DeprecationOptions describes the deprecation headers key-values.
// - "X-API-Warn": options.WarnMessage // - "X-Api-Warn": options.WarnMessage
// - "X-API-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate)) // - "X-Api-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate))
// - "X-API-Deprecation-Info": options.DeprecationInfo // - "X-Api-Deprecation-Info": options.DeprecationInfo
type DeprecationOptions struct { type DeprecationOptions struct {
WarnMessage string WarnMessage string
DeprecationDate time.Time DeprecationDate time.Time
@ -37,14 +44,14 @@ func WriteDeprecated(ctx *context.Context, options DeprecationOptions) {
options.WarnMessage = DefaultDeprecationOptions.WarnMessage options.WarnMessage = DefaultDeprecationOptions.WarnMessage
} }
ctx.Header("X-API-Warn", options.WarnMessage) ctx.Header(APIWarnHeader, options.WarnMessage)
if !options.DeprecationDate.IsZero() { 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 != "" { if options.DeprecationInfo != "" {
ctx.Header("X-API-Deprecation-Info", options.DeprecationInfo) ctx.Header(APIDeprecationInfoHeader, options.DeprecationInfo)
} }
} }

View File

@ -1,39 +1,107 @@
package versioning package versioning
import ( import (
"strings"
"github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/core/router"
"github.com/blang/semver/v4"
) )
// Group is a group of version-based routes. // API is a type alias of router.Party.
// One version per one or more routes. // This is required in order for a Group instance
type Group struct { // to implement the Party interface without field conflict.
router.Party type API = router.Party
// Information not currently in-use. // Group represents a group of resources that should
version string // be handled based on a version requested by the client.
// See `NewGroup` for more.
type Group struct {
API
validate semver.Range
deprecation DeprecationOptions deprecation DeprecationOptions
} }
// NewGroup returns a ptr to Group based on the given "version". // NewGroup returns a version Group based on the given "version" constraint.
// It sets the API Version for the "r" Party. // 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: // Usage:
// api := versioning.NewGroup(Parent_Party, ">= 1, < 2") // app := iris.New()
// api.Get/Post/Put/Delete... // api := app.Party("/api")
func NewGroup(r router.Party, version string) *Group { // 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 // Note that this feature alters the RouteRegisterRule to RouteOverlap
// the RouteOverlap rule does not contain any performance downside // 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. // but it's good to know that if you registered other mode, this wanna change it.
r.SetRegisterRule(router.RouteOverlap) 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{ return &Group{
Party: r, API: r,
version: version, 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. // store it for future use, e.g. collect all deprecated APIs and notify the developer.
g.deprecation = options g.deprecation = options
g.Party.UseOnce(func(ctx *context.Context) { g.API.UseOnce(func(ctx *context.Context) {
WriteDeprecated(ctx, options) WriteDeprecated(ctx, options)
ctx.Next() ctx.Next()
}) })
return g 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()
}
}

56
versioning/group_test.go Normal file
View File

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

View File

@ -5,31 +5,31 @@ import (
"strings" "strings"
"github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/context"
"github.com/blang/semver/v4"
) )
const ( 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 is the header key of "Accept-Version".
AcceptVersionHeaderKey = "Accept-Version" AcceptVersionHeaderKey = "Accept-Version"
// AcceptHeaderKey is the header key of "Accept". // AcceptHeaderKey is the header key of "Accept".
AcceptHeaderKey = "Accept" AcceptHeaderKey = "Accept"
// AcceptHeaderVersionValue is the Accept's header value search term the requested version. // AcceptHeaderVersionValue is the Accept's header value search term the requested version.
AcceptHeaderVersionValue = "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)` // 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" 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 // ErrNotFound reports whether a requested version
@ -54,11 +54,89 @@ var NotFoundHandler = func(ctx *context.Context) {
ctx.StopWithPlainError(501, ErrNotFound) 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. // GetVersion returns the current request version.
// //
// By default the `GetVersion` will try to read from: // By default the `GetVersion` will try to read from:
// - "Accept" header, i.e Accept: "application/json; version=1.0" // - "Accept" header, i.e Accept: "application/json; version=1.0.0"
// - "Accept-Version" header, i.e Accept-Version: "1.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 // 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). // 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. // SetVersion force-sets the API Version.
// It can be used inside a middleware. // 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. // See `GetVersion` too.
func SetVersion(ctx *context.Context, constraint string) { func SetVersion(ctx *context.Context, constraint string) {
ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetVersionContextKey(), constraint) 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
}
}
}

View File

@ -8,6 +8,15 @@ import (
"github.com/kataras/iris/v12/versioning" "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) { func TestGetVersion(t *testing.T) {
app := iris.New() app := iris.New()
@ -23,16 +32,16 @@ func TestGetVersion(t *testing.T) {
e := httptest.New(t, app) e := httptest.New(t, app)
e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect(). e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect().
Status(iris.StatusOK).Body().Equal("1.0") Status(iris.StatusOK).Body().Equal("1.0.0")
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1").Expect(). e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1.0").Expect().
Status(iris.StatusOK).Body().Equal("2.1") Status(iris.StatusOK).Body().Equal("2.1.0")
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1 ;other=dsa").Expect(). e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1.0 ;other=dsa").Expect().
Status(iris.StatusOK).Body().Equal("2.1") Status(iris.StatusOK).Body().Equal("2.1.0")
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=2.1").Expect(). e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=2.1.0").Expect().
Status(iris.StatusOK).Body().Equal("2.1") Status(iris.StatusOK).Body().Equal("2.1.0")
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=1").Expect(). e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=1.0.0").Expect().
Status(iris.StatusOK).Body().Equal("1") Status(iris.StatusOK).Body().Equal("1.0.0")
// unknown versions. // unknown versions.
e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "").Expect(). 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") 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")
}

View File

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

View File

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