mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
Merge branch 'master' into master
This commit is contained in:
commit
387eac8672
|
@ -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
1
.gitignore
vendored
|
@ -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-*/
|
||||||
|
|
24
HISTORY.md
24
HISTORY.md
|
@ -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
6
NOTICE
|
@ -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
|
||||||
|
|
14
README.md
14
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)
|
[![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>
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
}
|
||||||
|
|
1
_examples/routing/versioning/v1/index.html
Normal file
1
_examples/routing/versioning/v1/index.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<h1>This is the directory for version 1 templates</h1>
|
1
_examples/routing/versioning/v2/index.html
Normal file
1
_examples/routing/versioning/v2/index.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<h1>This is the directory for version 2 templates</h1>
|
1
_examples/routing/versioning/v3/index.html
Normal file
1
_examples/routing/versioning/v3/index.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<h1>This is the directory for version 3 templates</h1>
|
|
@ -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) {
|
||||||
|
|
|
@ -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: ")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
//
|
//
|
||||||
|
|
|
@ -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
4
go.mod
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
56
versioning/group_test.go
Normal 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")
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,135 +0,0 @@
|
||||||
package versioning_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/v12"
|
|
||||||
"github.com/kataras/iris/v12/httptest"
|
|
||||||
"github.com/kataras/iris/v12/versioning"
|
|
||||||
)
|
|
||||||
|
|
||||||
func notFoundHandler(ctx iris.Context) {
|
|
||||||
ctx.NotFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
v10Response = "v1.0 handler"
|
|
||||||
v2Response = "v2.x handler"
|
|
||||||
)
|
|
||||||
|
|
||||||
func sendHandler(contents string) iris.Handler {
|
|
||||||
return func(ctx iris.Context) {
|
|
||||||
ctx.WriteString(contents)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIf(t *testing.T) {
|
|
||||||
if expected, got := true, versioning.If("1.0", ">=1"); expected != got {
|
|
||||||
t.Fatalf("expected %s to be %s", "1.0", ">= 1")
|
|
||||||
}
|
|
||||||
if expected, got := true, versioning.If("1.2.3", "> 1.2"); expected != got {
|
|
||||||
t.Fatalf("expected %s to be %s", "1.2.3", "> 1.2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewMatcher(t *testing.T) {
|
|
||||||
app := iris.New()
|
|
||||||
|
|
||||||
userAPI := app.Party("/api/user")
|
|
||||||
userAPI.Get("/", versioning.NewMatcher(versioning.Map{
|
|
||||||
"1.0": sendHandler(v10Response),
|
|
||||||
">= 2, < 3": sendHandler(v2Response),
|
|
||||||
versioning.NotFound: notFoundHandler,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// middleware as usual.
|
|
||||||
myMiddleware := func(ctx iris.Context) {
|
|
||||||
ctx.Header("X-Custom", "something")
|
|
||||||
ctx.Next()
|
|
||||||
}
|
|
||||||
myVersions := versioning.Map{
|
|
||||||
"1.0": sendHandler(v10Response),
|
|
||||||
}
|
|
||||||
|
|
||||||
userAPI.Get("/with_middleware", myMiddleware, versioning.NewMatcher(myVersions))
|
|
||||||
|
|
||||||
e := httptest.New(t, app)
|
|
||||||
|
|
||||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect().
|
|
||||||
Status(iris.StatusOK).Body().Equal(v10Response)
|
|
||||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0").Expect().
|
|
||||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
|
||||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1").Expect().
|
|
||||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
|
||||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect().
|
|
||||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
|
||||||
|
|
||||||
// middleware as usual.
|
|
||||||
ex := e.GET("/api/user/with_middleware").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect()
|
|
||||||
ex.Status(iris.StatusOK).Body().Equal(v10Response)
|
|
||||||
ex.Header("X-Custom").Equal("something")
|
|
||||||
|
|
||||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect().
|
|
||||||
Status(iris.StatusNotFound).Body().Equal("Not Found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewGroup(t *testing.T) {
|
|
||||||
app := iris.New()
|
|
||||||
|
|
||||||
userAPI := app.Party("/api/user")
|
|
||||||
// [... static serving, middlewares and etc goes here].
|
|
||||||
|
|
||||||
userAPIV10 := versioning.NewGroup(userAPI, "1.0").Deprecated(versioning.DefaultDeprecationOptions)
|
|
||||||
// V10middlewareResponse := "m1"
|
|
||||||
// userAPIV10.Use(func(ctx iris.Context) {
|
|
||||||
// println("exec userAPIV10.Use - midl1")
|
|
||||||
// sendHandler(V10middlewareResponse)(ctx)
|
|
||||||
// ctx.Next()
|
|
||||||
// })
|
|
||||||
// userAPIV10.Use(func(ctx iris.Context) {
|
|
||||||
// println("exec userAPIV10.Use - midl2")
|
|
||||||
// sendHandler(V10middlewareResponse + "midl2")(ctx)
|
|
||||||
// ctx.Next()
|
|
||||||
// })
|
|
||||||
// userAPIV10.Use(func(ctx iris.Context) {
|
|
||||||
// println("exec userAPIV10.Use - midl3")
|
|
||||||
// ctx.Next()
|
|
||||||
// })
|
|
||||||
|
|
||||||
userAPIV10.Get("/", sendHandler(v10Response))
|
|
||||||
userAPIV2 := versioning.NewGroup(userAPI, ">= 2, < 3")
|
|
||||||
// V2middlewareResponse := "m2"
|
|
||||||
// userAPIV2.Use(func(ctx iris.Context) {
|
|
||||||
// println("exec userAPIV2.Use - midl1")
|
|
||||||
// sendHandler(V2middlewareResponse)(ctx)
|
|
||||||
// ctx.Next()
|
|
||||||
// })
|
|
||||||
// userAPIV2.Use(func(ctx iris.Context) {
|
|
||||||
// println("exec userAPIV2.Use - midl2")
|
|
||||||
// ctx.Next()
|
|
||||||
// })
|
|
||||||
|
|
||||||
userAPIV2.Get("/", sendHandler(v2Response))
|
|
||||||
userAPIV2.Post("/", sendHandler(v2Response))
|
|
||||||
userAPIV2.Put("/other", sendHandler(v2Response))
|
|
||||||
|
|
||||||
e := httptest.New(t, app)
|
|
||||||
|
|
||||||
ex := e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect()
|
|
||||||
ex.Status(iris.StatusOK).Body().Equal(v10Response)
|
|
||||||
ex.Header("X-API-Warn").Equal(versioning.DefaultDeprecationOptions.WarnMessage)
|
|
||||||
|
|
||||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0").Expect().
|
|
||||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
|
||||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1").Expect().
|
|
||||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
|
||||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect().
|
|
||||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
|
||||||
e.POST("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2").Expect().
|
|
||||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
|
||||||
e.PUT("/api/user/other").WithHeader(versioning.AcceptVersionHeaderKey, "2.9").Expect().
|
|
||||||
Status(iris.StatusOK).Body().Equal(v2Response)
|
|
||||||
|
|
||||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect().
|
|
||||||
Status(iris.StatusNotImplemented).Body().Equal("version not found")
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user