Browse Source

Merge branch 'master' into master

tags/v12.2.0-alpha2
Gerasimos (Makis) Maropoulos GitHub 6 months ago
parent
commit
387eac8672
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 745 additions and 476 deletions
  1. +0
    -1
      .deepsource.toml
  2. +1
    -0
      .gitignore
  3. +24
    -0
      HISTORY.md
  4. +3
    -3
      NOTICE
  5. +13
    -1
      README.md
  6. +5
    -8
      _examples/auth/basicauth/basic/main.go
  7. +2
    -7
      _examples/auth/jwt/basic/main.go
  8. +4
    -4
      _examples/mvc/error-handler-preflight/main.go
  9. +4
    -4
      _examples/mvc/versioned-controller/main.go
  10. +4
    -4
      _examples/mvc/versioned-controller/main_test.go
  11. +1
    -1
      _examples/routing/basic/main.go
  12. +2
    -2
      _examples/routing/main.go
  13. +1
    -1
      _examples/routing/overview/main.go
  14. +1
    -1
      _examples/routing/subdomains/wildcard/main.go
  15. +82
    -58
      _examples/routing/versioning/main.go
  16. +1
    -0
      _examples/routing/versioning/v1/index.html
  17. +1
    -0
      _examples/routing/versioning/v2/index.html
  18. +1
    -0
      _examples/routing/versioning/v3/index.html
  19. +1
    -1
      _examples/sessions/overview/example/example.go
  20. +3
    -3
      _examples/sessions/securecookie/main_test.go
  21. +5
    -0
      _examples/testing/httptest/main.go
  22. +1
    -1
      _examples/view/template_html_4/main.go
  23. +26
    -11
      configuration.go
  24. +2
    -0
      context/configuration.go
  25. +7
    -0
      context/context.go
  26. +9
    -9
      context/context_user.go
  27. +4
    -0
      context/handler.go
  28. +4
    -0
      context/route.go
  29. +34
    -8
      core/router/api_builder.go
  30. +4
    -0
      core/router/party.go
  31. +17
    -1
      core/router/route.go
  32. +2
    -2
      go.mod
  33. +1
    -1
      hero/reflect.go
  34. +13
    -1
      middleware/accesslog/accesslog.go
  35. +4
    -4
      mvc/versioning.go
  36. +2
    -2
      sessions/sessiondb/boltdb/database.go
  37. +13
    -6
      versioning/deprecation.go
  38. +102
    -16
      versioning/group.go
  39. +56
    -0
      versioning/group_test.go
  40. +216
    -16
      versioning/version.go
  41. +69
    -10
      versioning/version_test.go
  42. +0
    -154
      versioning/versioning.go
  43. +0
    -135
      versioning/versioning_test.go

+ 0
- 1
.deepsource.toml View File

@@ -14,4 +14,3 @@ enabled = true

[analyzers.meta]
import_paths = ["github.com/kataras/iris"]


+ 1
- 0
.gitignore View File

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


+ 24
- 0
HISTORY.md View File

@@ -28,6 +28,29 @@ The codebase for Dependency Injection, Internationalization and localization and
## Fixes and Improvements
- New `versioning.Aliases` middleware and up to 80% faster version resolve. Example Code:
```go
app := iris.New()
api := app.Party("/api")
api.Use(Aliases(map[string]string{
versioning.Empty: "1", // when no version was provided by the client.
"beta": "4.0.0",
"stage": "5.0.0-alpha"
}))
v1 := NewGroup(api, ">=1.0.0 <2.0.0")
v1.Get/Post...
v4 := NewGroup(api, ">=4.0.0 <5.0.0")
v4.Get/Post...
stage := NewGroup(api, "5.0.0-alpha")
stage.Get/Post...
```
- New [Basic Authentication](https://github.com/kataras/iris/tree/master/middleware/basicauth) middleware. Its `Default` function has not changed, however, the rest, e.g. `New` contains breaking changes as the new middleware features new functionalities.
- Add `iris.DirOptions.SPA bool` field to allow [Single Page Applications](https://github.com/kataras/iris/tree/master/_examples/file-server/single-page-application/basic/main.go) under a file server.
- A generic User interface, see the `Context.SetUser/User` methods in the New Context Methods section for more. In-short, the basicauth middleware's stored user can now be retrieved through `Context.User()` which provides more information than the native `ctx.Request().BasicAuth()` method one. Third-party authentication middleware creators can benefit of these two methods, plus the Logout below.
- A `Context.Logout` method is added, can be used to invalidate [basicauth](https://github.com/kataras/iris/blob/master/_examples/auth/basicauth/basic/main.go) or [jwt](https://github.com/kataras/iris/blob/master/_examples/auth/jwt/blocklist/main.go) client credentials.
@@ -700,6 +723,7 @@ Response:
## Breaking Changes
- The `versioning.NewMatcher` has been removed entirely in favor of `NewGroup`. Strict versions format on `versioning.NewGroup` is required. E.g. `"1"` is not valid anymore, you have to specify `"1.0.0"`. Example: `NewGroup(api, ">=1.0.0 <2.0.0")`. The [routing/versioning](_examples/routing/versioning) examples have been updated.
- Now that `RegisterView` can be used to register different view engines per-Party, there is no need to support registering multiple engines under the same Party. The `app.RegisterView` now upserts the given Engine instead of append. You can now render templates **without file extension**, e.g. `index` instead of `index.ace`, both forms are valid now.
- The `Context.ContentType` does not accept filenames to resolve the mime type anymore (caused issues with vendor-specific(vnd) MIME types).
- The `Configuration.RemoteAddrPrivateSubnets.IPRange.Start and End` are now type of `string` instead of `net.IP`. The `WithRemoteAddrPrivateSubnet` option remains as it is, already accepts `string`s.


+ 3
- 3
NOTICE View File

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


+ 13
- 1
README.md View File

@@ -1,10 +1,15 @@
[![Black Lives Matter](https://iris-go.com/images/blacklivesmatter_banner.png)](https://support.eji.org/give/153413/#!/donation/checkout)

<!-- # 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.
>
> ![](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)! -->

@@ -31,6 +36,13 @@ With your help, we can improve Open Source web development for everyone!
> Donations from **China** are now accepted!

<p>
<a href="https://github.com/motogo"><img src="https://avatars1.githubusercontent.com/u/1704958?v=4" alt ="Horst Ender" title="motogo" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
<a href="https://github.com/remopavithran"><img src="https://avatars1.githubusercontent.com/u/50388068?v=4" alt ="Pavithran" title="remopavithran" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
<a href="https://github.com/mulyawansentosa"><img src="https://avatars1.githubusercontent.com/u/29946673?v=4" alt ="MULYAWAN SENTOSA" title="mulyawansentosa" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
<a href="https://github.com/TianJIANG"><img src="https://avatars1.githubusercontent.com/u/158459?v=4" alt ="KIT UNITED" title="TianJIANG" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
<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/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>


+ 5
- 8
_examples/auth/basicauth/basic/main.go View File

@@ -64,14 +64,11 @@ func main() {
}

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



+ 2
- 7
_examples/auth/jwt/basic/main.go View File

@@ -4,16 +4,10 @@ import (
"time"

"github.com/kataras/iris/v12"
"github.com/kataras/jwt"
"github.com/kataras/iris/v12/middleware/jwt"
)

/*
Learn how to use any JWT 3rd-party package with Iris.
In this example we use the kataras/jwt one.

Install with:
go get -u github.com/kataras/jwt

Documentation:
https://github.com/kataras/jwt#table-of-contents
*/
@@ -71,6 +65,7 @@ func protected(ctx iris.Context) {

// Just an example on how you can retrieve all the standard claims (set by jwt.MaxAge, "exp").
standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims

expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
timeLeft := standardClaims.Timeleft()



+ 4
- 4
_examples/mvc/error-handler-preflight/main.go View File

@@ -29,7 +29,7 @@ type response struct {
Timestamp int64 `json:"timestamp,omitempty"`
}

func (r response) Preflight(ctx iris.Context) error {
func (r *response) Preflight(ctx iris.Context) error {
if r.ID > 0 {
r.Timestamp = time.Now().Unix()
}
@@ -64,15 +64,15 @@ type user struct {
ID uint64 `json:"id"`
}

func (c *controller) GetBy(userid uint64) response {
func (c *controller) GetBy(userid uint64) *response {
if userid != 1 {
return response{
return &response{
Code: iris.StatusNotFound,
Message: "User Not Found",
}
}

return response{
return &response{
ID: userid,
Data: user{ID: userid},
}


+ 4
- 4
_examples/mvc/versioned-controller/main.go View File

@@ -28,10 +28,10 @@ func newApp() *iris.Application {
{
m := mvc.New(dataRouter)

m.Handle(new(v1Controller), mvc.Version("1"), mvc.Deprecated(opts)) // 1 or 1.0, 1.0.0 ...
m.Handle(new(v2Controller), mvc.Version("2.3")) // 2.3 or 2.3.0
m.Handle(new(v3Controller), mvc.Version(">=3, <4")) // 3, 3.x, 3.x.x ...
m.Handle(new(noVersionController)) // or if missing it will respond with 501 version not found.
m.Handle(new(v1Controller), mvc.Version("1.0.0"), mvc.Deprecated(opts))
m.Handle(new(v2Controller), mvc.Version("2.3.0"))
m.Handle(new(v3Controller), mvc.Version(">=3.0.0 <4.0.0"))
m.Handle(new(noVersionController)) // or if missing it will respond with 501 version not found.
}

return app


+ 4
- 4
_examples/mvc/versioned-controller/main_test.go View File

@@ -12,21 +12,21 @@ func TestVersionedController(t *testing.T) {
app := newApp()

e := httptest.New(t, app)
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect().
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect().
Status(iris.StatusOK).Body().Equal("data (v1.x)")
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "2.3.0").Expect().
Status(iris.StatusOK).Body().Equal("data (v2.x)")
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "3.1").Expect().
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "3.1.0").Expect().
Status(iris.StatusOK).Body().Equal("data (v3.x)")

// Test invalid version or no version at all.
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "4").Expect().
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "4.0.0").Expect().
Status(iris.StatusOK).Body().Equal("data")
e.GET("/data").Expect().
Status(iris.StatusOK).Body().Equal("data")

// Test Deprecated (v1)
ex := e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect()
ex := e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect()
ex.Status(iris.StatusOK).Body().Equal("data (v1.x)")
ex.Header("X-API-Warn").Equal(opts.WarnMessage)
expectedDateStr := opts.DeprecationDate.Format(app.ConfigurationReadOnly().GetTimeFormat())


+ 1
- 1
_examples/routing/basic/main.go View File

@@ -152,7 +152,7 @@ func newApp() *iris.Application {
}

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


+ 2
- 2
_examples/routing/main.go View File

@@ -82,11 +82,11 @@ func registerGamesRoutes(app *iris.Application) {
}

func registerSubdomains(app *iris.Application) {
mysubdomain := app.Party("mysubdomain.")
mysubdomain := app.Subdomain("mysubdomain")
// http://mysubdomain.myhost.com
mysubdomain.Get("/", h)

willdcardSubdomain := app.Party("*.")
willdcardSubdomain := app.WildcardSubdomain()
willdcardSubdomain.Get("/", h)
willdcardSubdomain.Party("/party").Get("/", h)
}


+ 1
- 1
_examples/routing/overview/main.go View File

@@ -116,7 +116,7 @@ func main() {
adminRoutes.Get("/settings", info)

// Wildcard/dynamic subdomain
dynamicSubdomainRoutes := app.Party("*.")
dynamicSubdomainRoutes := app.WildcardSubdomain()

// GET: http://any_thing_here.localhost:8080
dynamicSubdomainRoutes.Get("/", info)


+ 1
- 1
_examples/routing/subdomains/wildcard/main.go View File

@@ -33,7 +33,7 @@ func main() {
}*/

// no order, you can register subdomains at the end also.
dynamicSubdomains := app.Party("*.")
dynamicSubdomains := app.WildcardSubdomain()
{
dynamicSubdomains.Get("/", dynamicSubdomainHandler)



+ 82
- 58
_examples/routing/versioning/main.go View File

@@ -8,71 +8,95 @@ import (
func main() {
app := iris.New()

examplePerRoute(app)
examplePerParty(app)

// Read the README.md before any action.
app.Listen(":8080")
}
app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) {
ctx.WriteString(`Root not found handler.
This will be applied everywhere except the /api/* requests.`)
})

// How to test:
// Open Postman
// GET: localhost:8080/api/cats
// Headers[1] = Accept-Version: "1" and repeat with
// Headers[1] = Accept-Version: "2.5"
// or even "Accept": "application/json; version=2.5"
func examplePerRoute(app *iris.Application) {
app.Get("/api/cats", versioning.NewMatcher(versioning.Map{
"1": catsVersionExactly1Handler,
">= 2, < 3": catsV2Handler,
versioning.NotFound: versioning.NotFoundHandler,
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",
}))
}

// How to test:
// Open Postman
// GET: localhost:8080/api/users
// Headers[1] = Accept-Version: "1.9.9" and repeat with
// Headers[1] = Accept-Version: "2.5"
//
// POST: localhost:8080/api/users/new
// Headers[1] = Accept-Version: "1.8.3"
//
// POST: localhost:8080/api/users
// Headers[1] = Accept-Version: "2"
func examplePerParty(app *iris.Application) {
usersAPI := app.Party("/api/users")
// You can customize the way a version is extracting
// via middleware, for example:
// version url parameter, and, if it's missing we default it to "1".
usersAPI.Use(func(ctx iris.Context) {
versioning.SetVersion(ctx, ctx.URLParamDefault("version", "1"))
ctx.Next()
})
/*
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)
}

// 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")
})
ctx.Next()
})
OR: api.Use(versioning.FromQuery("version", ""))
*/

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

app.Listen(":8080")
}

func testHandler(v string) iris.Handler {
return func(ctx iris.Context) {
ctx.JSON(iris.Map{
"version": v,
"message": "Hello, world!",
})
}
}

func catsVersionExactly1Handler(ctx iris.Context) {
ctx.Writef("v1 exactly resource: /api/cats handler")
func testError(v string) iris.Handler {
return func(ctx iris.Context) {
ctx.Writef("not found: %s", v)
}
}

func catsV2Handler(ctx iris.Context) {
ctx.Writef("v2 resource: /api/cats handler")
func testView(ctx iris.Context) {
ctx.View("index.html")
}

+ 1
- 0
_examples/routing/versioning/v1/index.html View File

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

+ 1
- 0
_examples/routing/versioning/v2/index.html View File

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

+ 1
- 0
_examples/routing/versioning/v3/index.html View File

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

+ 1
- 1
_examples/sessions/overview/example/example.go View File

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

app.Get("/clear", func(ctx iris.Context) {


+ 3
- 3
_examples/sessions/securecookie/main_test.go View File

@@ -16,12 +16,12 @@ func TestSessionsEncodeDecode(t *testing.T) {
es.Cookies().NotEmpty()
es.Body().Equal("All ok session set to: iris [isNew=true]")

e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: iris")
e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The username on the /set was: iris")
// delete and re-get
e.GET("/delete").Expect().Status(iris.StatusOK)
e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: ")
e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The username on the /set was: ")
// set, clear and re-get
e.GET("/set").Expect().Body().Equal("All ok session set to: iris [isNew=false]")
e.GET("/clear").Expect().Status(iris.StatusOK)
e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: ")
e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The username on the /set was: ")
}

+ 5
- 0
_examples/testing/httptest/main.go View File

@@ -37,6 +37,11 @@ func h(ctx iris.Context) {
// third parameter it will be always true because the middleware
// makes sure for that, otherwise this handler will not be executed.
// OR:
//
// user := ctx.User().(*myUserType)
// ctx.Writef("%s %s:%s", ctx.Path(), user.Username, user.Password)
// OR if you don't have registered custom User structs:
//
// ctx.User().GetUsername()
// ctx.User().GetPassword()
ctx.Writef("%s %s:%s", ctx.Path(), username, password)


+ 1
- 1
_examples/view/template_html_4/main.go View File

@@ -27,7 +27,7 @@ func main() {
// wildcard subdomain, will catch username1.... username2.... username3... username4.... username5...
// that our below links are providing via page.html's first argument which is the subdomain.

subdomain := app.Party("*.")
subdomain := app.WildcardSubdomain()

mypathRoute := subdomain.Get("/mypath", emptyHandler)
mypathRoute.Name = "my-page1"


+ 26
- 11
configuration.go View File

@@ -764,9 +764,14 @@ type Configuration struct {
// Defaults to "iris.locale.language.input".
LanguageInputContextKey string `ini:"language_input_context_key" json:"languageInputContextKey,omitempty" yaml:"LanguageInputContextKey" toml:"LanguageInputContextKey"`
// VersionContextKey is the context key which an API Version can be modified
// via a middleware through `SetVersion` method, e.g. `versioning.SetVersion(ctx, "1.0, 1.1")`.
// via a middleware through `SetVersion` method, e.g. `versioning.SetVersion(ctx, ">=1.0.0 <2.0.0")`.
// Defaults to "iris.api.version".
VersionContextKey string `ini:"version_context_key" json:"versionContextKey" yaml:"VersionContextKey" toml:"VersionContextKey"`
// VersionAliasesContextKey is the context key which the versioning feature
// can look up for alternative values of a version and fallback to that.
// Head over to the versioning package for more.
// Defaults to "iris.api.version.aliases"
VersionAliasesContextKey string `ini:"version_aliases_context_key" json:"versionAliasesContextKey" yaml:"VersionAliasesContextKey" toml:"VersionAliasesContextKey"`
// ViewEngineContextKey is the context's values key
// responsible to store and retrieve(view.Engine) the current view engine.
// A middleware or a Party can modify its associated value to change
@@ -974,6 +979,11 @@ func (c Configuration) GetVersionContextKey() string {
return c.VersionContextKey
}

// GetVersionAliasesContextKey returns the VersionAliasesContextKey field.
func (c Configuration) GetVersionAliasesContextKey() string {
return c.VersionAliasesContextKey
}

// GetViewEngineContextKey returns the ViewEngineContextKey field.
func (c Configuration) GetViewEngineContextKey() string {
return c.ViewEngineContextKey
@@ -1132,6 +1142,10 @@ func WithConfiguration(c Configuration) Configurator {
main.VersionContextKey = v
}

if v := c.VersionAliasesContextKey; v != "" {
main.VersionAliasesContextKey = v
}

if v := c.ViewEngineContextKey; v != "" {
main.ViewEngineContextKey = v
}
@@ -1205,16 +1219,17 @@ func DefaultConfiguration() Configuration {
// The request body the size limit
// can be set by the middleware `LimitRequestBodySize`
// or `context#SetMaxRequestBodySize`.
PostMaxMemory: 32 << 20, // 32MB
LocaleContextKey: "iris.locale",
LanguageContextKey: "iris.locale.language",
LanguageInputContextKey: "iris.locale.language.input",
VersionContextKey: "iris.api.version",
ViewEngineContextKey: "iris.view.engine",
ViewLayoutContextKey: "iris.view.layout",
ViewDataContextKey: "iris.view.data",
RemoteAddrHeaders: nil,
RemoteAddrHeadersForce: false,
PostMaxMemory: 32 << 20, // 32MB
LocaleContextKey: "iris.locale",
LanguageContextKey: "iris.locale.language",
LanguageInputContextKey: "iris.locale.language.input",
VersionContextKey: "iris.api.version",
VersionAliasesContextKey: "iris.api.version.aliases",
ViewEngineContextKey: "iris.view.engine",
ViewLayoutContextKey: "iris.view.layout",
ViewDataContextKey: "iris.view.data",
RemoteAddrHeaders: nil,
RemoteAddrHeadersForce: false,
RemoteAddrPrivateSubnets: []netutil.IPRange{
{
Start: "10.0.0.0",


+ 2
- 0
context/configuration.go View File

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

// GetViewEngineContextKey returns the ViewEngineContextKey field.
GetViewEngineContextKey() string


+ 7
- 0
context/context.go View File

@@ -1974,6 +1974,13 @@ func (ctx *Context) UploadFormFiles(destDirectory string, before ...func(*Contex
for _, files := range fhs {
innerLoop:
for _, file := range files {
// Fix an issue that net/http has,
// an attacker can push a filename
// which could lead to override existing system files
// by ../../$file.
// Reported by Frank through security reports.
file.Filename = strings.TrimLeft(file.Filename, "../")
file.Filename = strings.TrimLeft(file.Filename, "..\\")

for _, b := range before {
if !b(ctx, file) {


+ 9
- 9
context/context_user.go View File

@@ -81,15 +81,15 @@ to the end-developer's custom implementations.

// SimpleUser is a simple implementation of the User interface.
type SimpleUser struct {
Authorization string `json:"authorization,omitempty"`
AuthorizedAt time.Time `json:"authorized_at,omitempty"`
ID string `json:"id,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Email string `json:"email,omitempty"`
Roles []string `json:"roles,omitempty"`
Token json.RawMessage `json:"token,omitempty"`
Fields Map `json:"fields,omitempty"`
Authorization string `json:"authorization,omitempty" db:"authorization"`
AuthorizedAt time.Time `json:"authorized_at,omitempty" db:"authorized_at"`
ID string `json:"id,omitempty" db:"id"`
Username string `json:"username,omitempty" db:"username"`
Password string `json:"password,omitempty" db:"password"`
Email string `json:"email,omitempty" db:"email"`
Roles []string `json:"roles,omitempty" db:"roles"`
Token json.RawMessage `json:"token,omitempty" db:"token"`
Fields Map `json:"fields,omitempty" db:"fields"`
}

var _ User = (*SimpleUser)(nil)


+ 4
- 0
context/handler.go View File

@@ -239,6 +239,10 @@ var ignoreMainHandlerNames = [...]string{
"iris.reCAPTCHA",
"iris.profiling",
"iris.recover",
"iris.accesslog",
"iris.grpc",
"iris.requestid",
"iris.rewrite",
}

// ingoreMainHandlerName reports whether a main handler of "name" should


+ 4
- 0
context/route.go View File

@@ -62,6 +62,10 @@ type RouteReadOnly interface {
// MainHandlerIndex returns the first registered handler's index for the route.
MainHandlerIndex() int

// Property returns a specific property based on its "key"
// of this route's Party owner.
Property(key string) (interface{}, bool)

// Sitemap properties: https://www.sitemaps.org/protocol.html

// GetLastMod returns the date of last modification of the file served by this route.


+ 34
- 8
core/router/api_builder.go View File

@@ -164,7 +164,7 @@ func overlapRoute(r *Route, next *Route) {
// Version was not found:
// we need to be able to send the status on the last not found version
// but reset the status code if a next available matched version was found.
// see: versioning.Handler.
// see the versioning package.
if !errors.Is(ctx.GetErr(), context.ErrNotFound) {
ctx.StatusCode(prevStatusCode)
}
@@ -196,6 +196,11 @@ type APIBuilder struct {

// the api builder global macros registry
macros *macro.Macros
// the per-party (and its children) values map
// that may help on building the API
// when source code is splitted between projects.
// Initialized on Properties method.
properties context.Map
// the api builder global routes repository
routes *repository
// disables the debug logging of routes under a per-party and its children.
@@ -624,7 +629,7 @@ func (api *APIBuilder) createRoutes(errorCode int, methods []string, relativePat
routes := make([]*Route, len(methods))

for i, m := range methods { // single, empty method for error handlers.
route, err := NewRoute(errorCode, m, subdomain, path, routeHandlers, *api.macros)
route, err := NewRoute(api, errorCode, m, subdomain, path, routeHandlers, *api.macros)
if err != nil { // template path parser errors:
api.logger.Errorf("[%s:%d] %v -> %s:%s:%s", filename, line, err, m, subdomain, path)
continue
@@ -668,19 +673,21 @@ func removeDuplicates(elements []string) (result []string) {

// Party returns a new child Party which inherites its
// parent's options and middlewares.
// If "relativePath" matches the parent's one then it returns the current Party.
// A Party groups routes which may have the same prefix or subdomain and share same middlewares.
//
// To create a group of routes for subdomains
// use the `Subdomain` or `WildcardSubdomain` methods
// or pass a "relativePath" as "admin." or "*." respectfully.
// or pass a "relativePath" of "admin." or "*." respectfully.
func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) Party {
// if app.Party("/"), root party or app.Party("/user") == app.Party("/user")
// then just add the middlewares and return itself.
if relativePath == "" || api.relativePath == relativePath {
api.Use(handlers...)
return api
}
// if relativePath == "" || api.relativePath == relativePath {
// api.Use(handlers...)
// return api
// }
// ^ No, this is wrong, let the developer do its job, if she/he wants a copy let have it,
// it's a pure check as well, a path can be the same even if it's the same as its parent, i.e.
// app.Party("/user").Party("/user") should result in a /user/user, not a /user.

parentPath := api.relativePath
dot := string(SubdomainPrefix[0])
@@ -712,10 +719,17 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
allowMethods := make([]string, len(api.allowMethods))
copy(allowMethods, api.allowMethods)

// make a copy of the parent properties.
var properties context.Map
for k, v := range api.properties {
properties[k] = v
}

childAPI := &APIBuilder{
// global/api builder
logger: api.logger,
macros: api.macros,
properties: properties,
routes: api.routes,
routesNoLog: api.routesNoLog,
beginGlobalHandlers: api.beginGlobalHandlers,
@@ -808,6 +822,16 @@ func (api *APIBuilder) Macros() *macro.Macros {
return api.macros
}

// Properties returns the original Party's properties map,
// it can be modified before server startup but not afterwards.
func (api *APIBuilder) Properties() context.Map {
if api.properties == nil {
api.properties = make(context.Map)
}

return api.properties
}

// GetRoutes returns the routes information,
// some of them can be changed at runtime some others not.
//
@@ -1096,6 +1120,8 @@ func (api *APIBuilder) DoneGlobal(handlers ...context.Handler) {

// RemoveHandler deletes a handler from begin and done handlers
// based on its name or the handler pc function.
// Note that UseGlobal and DoneGlobal handlers cannot be removed
// through this method as they were registered to the routes already.
//
// As an exception, if one of the arguments is a pointer to an int,
// then this is used to set the total amount of removed handlers.


+ 4
- 0
core/router/party.go View File

@@ -39,6 +39,10 @@ type Party interface {
// Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path
Macros() *macro.Macros

// Properties returns the original Party's properties map,
// it can be modified before server startup but not afterwards.
Properties() context.Map

// SetRoutesNoLog disables (true) the verbose logging for the next registered
// routes under this Party and its children.
//


+ 17
- 1
core/router/route.go View File

@@ -19,6 +19,8 @@ import (
// If any of the following fields are changed then the
// caller should Refresh the router.
type Route struct {
// The Party which this Route was created and registered on.
Party Party
Title string `json:"title"` // custom name to replace the method on debug logging.
Name string `json:"name"` // "userRoute"
Description string `json:"description"` // "lists a user"
@@ -86,7 +88,7 @@ type Route struct {
// handlers and the macro container which all routes should share.
// It parses the path based on the "macros",
// handlers are being changed to validate the macros at serve time, if needed.
func NewRoute(statusErrorCode int, method, subdomain, unparsedPath string,
func NewRoute(p Party, statusErrorCode int, method, subdomain, unparsedPath string,
handlers context.Handlers, macros macro.Macros) (*Route, error) {
tmpl, err := macro.Parse(unparsedPath, macros)
if err != nil {
@@ -110,6 +112,7 @@ func NewRoute(statusErrorCode int, method, subdomain, unparsedPath string,
formattedPath := formatPath(path)

route := &Route{
Party: p,
StatusCode: statusErrorCode,
Name: defaultName,
Method: method,
@@ -583,6 +586,8 @@ type routeReadOnlyWrapper struct {
*Route
}

var _ context.RouteReadOnly = routeReadOnlyWrapper{}

func (rd routeReadOnlyWrapper) StatusErrorCode() int {
return rd.Route.StatusCode
}
@@ -619,6 +624,17 @@ func (rd routeReadOnlyWrapper) MainHandlerIndex() int {
return rd.Route.MainHandlerIndex
}

func (rd routeReadOnlyWrapper) Property(key string) (interface{}, bool) {
properties := rd.Route.Party.Properties()
if properties != nil {
if property, ok := properties[key]; ok {
return property, true
}
}

return nil, false
}

func (rd routeReadOnlyWrapper) GetLastMod() time.Time {
return rd.Route.LastMod
}


+ 2
- 2
go.mod View File

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


+ 1
- 1
hero/reflect.go View File

@@ -18,7 +18,7 @@ func valueOf(v interface{}) reflect.Value {

// indirectType returns the value of a pointer-type "typ".
// If "typ" is a pointer, array, chan, map or slice it returns its Elem,
// otherwise returns the typ as it's.
// otherwise returns the "typ" as it is.
func indirectType(typ reflect.Type) reflect.Type {
switch typ.Kind() {
case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:


+ 13
- 1
middleware/accesslog/accesslog.go View File

@@ -323,6 +323,18 @@ func New(w io.Writer) *AccessLog {
//
// It panics on error.
func File(path string) *AccessLog {
f := mustOpenFile(path)
return New(bufio.NewReadWriter(bufio.NewReader(f), bufio.NewWriter(f)))
}

// FileUnbuffered same as File but it does not buffer the data,
// it flushes the loggers contents as soon as possible.
func FileUnbuffered(path string) *AccessLog {
f := mustOpenFile(path)
return New(f)
}

func mustOpenFile(path string) *os.File {
// Note: we add os.RDWR in order to be able to read from it,
// some formatters (e.g. CSV) needs that.
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
@@ -330,7 +342,7 @@ func File(path string) *AccessLog {
panic(err)
}

return New(bufio.NewReadWriter(bufio.NewReader(f), bufio.NewWriter(f)))
return f
}

// Broker creates or returns the broker.


+ 4
- 4
mvc/versioning.go View File

@@ -8,14 +8,14 @@ import (

// Version returns a valid `Option` that can be passed to the `Application.Handle` method.
// It requires a specific "version" constraint for a Controller,
// e.g. ">1, <=2" or just "1".
// e.g. ">1.0.0 <=2.0.0".
//
//
// Usage:
// m := mvc.New(dataRouter)
// m.Handle(new(v1Controller), mvc.Version("1"), mvc.Deprecated(mvc.DeprecationOptions{}))
// m.Handle(new(v2Controller), mvc.Version("2.3"))
// m.Handle(new(v3Controller), mvc.Version(">=3, <4"))
// m.Handle(new(v1Controller), mvc.Version("1.0.0"), mvc.Deprecated(mvc.DeprecationOptions{}))
// m.Handle(new(v2Controller), mvc.Version("2.3.0"))
// m.Handle(new(v3Controller), mvc.Version(">=3.0.0 <4.0.0"))
// m.Handle(new(noVersionController))
//
// See the `versioning` package's documentation for more information on


+ 2
- 2
sessions/sessiondb/boltdb/database.go View File

@@ -339,14 +339,14 @@ func (db *Database) Visit(sid string, cb func(key string, value interface{})) er
}

// Len returns the length of the session's entries (keys).
func (db *Database) Len(sid string) (n int64) {
func (db *Database) Len(sid string) (n int) {
err := db.Service.View(func(tx *bolt.Tx) error {
b := db.getBucketForSession(tx, sid)
if b == nil {
return nil
}

n = int64(b.Stats().KeyN)
n = int(int64(b.Stats().KeyN))
return nil
})



+ 13
- 6
versioning/deprecation.go View File

@@ -6,10 +6,17 @@ import (
"github.com/kataras/iris/v12/context"
)

// The response header keys when a resource is deprecated by the server.
const (
APIWarnHeader = "X-Api-Warn"
APIDeprecationDateHeader = "X-Api-Deprecation-Date"
APIDeprecationInfoHeader = "X-Api-Deprecation-Info"
)

// DeprecationOptions describes the deprecation headers key-values.
// - "X-API-Warn": options.WarnMessage
// - "X-API-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate))
// - "X-API-Deprecation-Info": options.DeprecationInfo
// - "X-Api-Warn": options.WarnMessage
// - "X-Api-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate))
// - "X-Api-Deprecation-Info": options.DeprecationInfo
type DeprecationOptions struct {
WarnMessage string
DeprecationDate time.Time
@@ -37,14 +44,14 @@ func WriteDeprecated(ctx *context.Context, options DeprecationOptions) {
options.WarnMessage = DefaultDeprecationOptions.WarnMessage
}

ctx.Header("X-API-Warn", options.WarnMessage)
ctx.Header(APIWarnHeader, options.WarnMessage)

if !options.DeprecationDate.IsZero() {
ctx.Header("X-API-Deprecation-Date", context.FormatTime(ctx, options.DeprecationDate))
ctx.Header(APIDeprecationDateHeader, context.FormatTime(ctx, options.DeprecationDate))
}

if options.DeprecationInfo != "" {
ctx.Header("X-API-Deprecation-Info", options.DeprecationInfo)
ctx.Header(APIDeprecationInfoHeader, options.DeprecationInfo)
}
}



+ 102
- 16
versioning/group.go View File

@@ -1,39 +1,107 @@
package versioning

import (
"strings"

"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/core/router"

"github.com/blang/semver/v4"
)

// Group is a group of version-based routes.
// One version per one or more routes.
// API is a type alias of router.Party.
// This is required in order for a Group instance
// to implement the Party interface without field conflict.
type API = router.Party

// Group represents a group of resources that should
// be handled based on a version requested by the client.
// See `NewGroup` for more.
type Group struct {
router.Party
API

// Information not currently in-use.
version string
validate semver.Range
deprecation DeprecationOptions
}

// NewGroup returns a ptr to Group based on the given "version".
// It sets the API Version for the "r" Party.
// NewGroup returns a version Group based on the given "version" constraint.
// Group completes the Party interface.
// The returned Group wraps a cloned Party of the given "r" Party therefore,
// any changes to its parent won't affect this one (e.g. register global middlewares afterwards).
//
// See `Handle` for more.
// A version is extracted through the versioning.GetVersion function:
// Accept-Version: 1.0.0
// Accept: application/json; version=1.0.0
// You can customize it by setting a version based on the request context:
// api.Use(func(ctx *context.Context) {
// if version := ctx.URLParam("version"); version != "" {
// SetVersion(ctx, version)
// }
//
// Example: _examples/routing/versioning
// ctx.Next()
// })
// OR:
// api.Use(versioning.FromQuery("version", ""))
//
// Examples at: _examples/routing/versioning
// Usage:
// api := versioning.NewGroup(Parent_Party, ">= 1, < 2")
// api.Get/Post/Put/Delete...
func NewGroup(r router.Party, version string) *Group {
// app := iris.New()
// api := app.Party("/api")
// v1 := versioning.NewGroup(api, ">=1.0.0 <2.0.0")
// v1.Get/Post/Put/Delete...
//
// Valid ranges are:
// - "<1.0.0"
// - "<=1.0.0"
// - ">1.0.0"
// - ">=1.0.0"
// - "1.0.0", "=1.0.0", "==1.0.0"
// - "!1.0.0", "!=1.0.0"
//
// A Range can consist of multiple ranges separated by space:
// Ranges can be linked by logical AND:
// - ">1.0.0 <2.0.0" would match between both ranges, so "1.1.1" and "1.8.7"
// but not "1.0.0" or "2.0.0"
// - ">1.0.0 <3.0.0 !2.0.3-beta.2" would match every version between 1.0.0 and 3.0.0
// except 2.0.3-beta.2
//
// Ranges can also be linked by logical OR:
// - "<2.0.0 || >=3.0.0" would match "1.x.x" and "3.x.x" but not "2.x.x"
//
// AND has a higher precedence than OR. It's not possible to use brackets.
//
// Ranges can be combined by both AND and OR
//
// - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`,
// but not `4.2.1`, `2.1.1`
func NewGroup(r API, version string) *Group {
version = strings.ReplaceAll(version, ",", " ")
version = strings.TrimSpace(version)

verRange, err := semver.ParseRange(version)
if err != nil {
r.Logger().Errorf("versioning: %s: %s", r.GetRelPath(), strings.ToLower(err.Error()))
return &Group{API: r}
}

// Clone this one.
r = r.Party("/")

// Note that this feature alters the RouteRegisterRule to RouteOverlap
// the RouteOverlap rule does not contain any performance downside
// but it's good to know that if you registered other mode, this wanna change it.
r.SetRegisterRule(router.RouteOverlap)
r.UseOnce(Handler(version)) // this is required in order to not populate this middleware to the next group.

handler := makeHandler(verRange)
// This is required in order to not populate this middleware to the next group.
r.UseOnce(handler)
// This is required for versioned custom error handlers,
// of course if the parent registered one then this will do nothing.
r.UseError(handler)

return &Group{
Party: r,
version: version,
API: r,
validate: verRange,
}
}

@@ -43,9 +111,27 @@ func (g *Group) Deprecated(options DeprecationOptions) *Group {
// store it for future use, e.g. collect all deprecated APIs and notify the developer.
g.deprecation = options

g.Party.UseOnce(func(ctx *context.Context) {
g.API.UseOnce(func(ctx *context.Context) {
WriteDeprecated(ctx, options)
ctx.Next()
})
return g
}

func makeHandler(validate semver.Range) context.Handler {
return func(ctx *context.Context) {
if !matchVersionRange(ctx, validate) {
// The overlapped handler has an exception
// of a type of context.NotFound (which versioning.ErrNotFound wraps)
// to clear the status code
// and the error to ignore this
// when available match version exists (see `NewGroup`).
if h := NotFoundHandler; h != nil {
h(ctx)
return
}
}

ctx.Next()
}
}

+ 56
- 0
versioning/group_test.go View File

@@ -0,0 +1,56 @@
package versioning_test

import (
"testing"

"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/httptest"
"github.com/kataras/iris/v12/versioning"
)

const (
v10Response = "v1.0 handler"
v2Response = "v2.x handler"
)

func sendHandler(contents string) iris.Handler {
return func(ctx iris.Context) {
ctx.WriteString(contents)
}
}

func TestNewGroup(t *testing.T) {
app := iris.New()

userAPI := app.Party("/api/user")
// [... static serving, middlewares and etc goes here].

userAPIV10 := versioning.NewGroup(userAPI, "1.0.0").Deprecated(versioning.DefaultDeprecationOptions)

userAPIV10.Get("/", sendHandler(v10Response))
userAPIV2 := versioning.NewGroup(userAPI, ">= 2.0.0 < 3.0.0")

userAPIV2.Get("/", sendHandler(v2Response))
userAPIV2.Post("/", sendHandler(v2Response))
userAPIV2.Put("/other", sendHandler(v2Response))

e := httptest.New(t, app)

ex := e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect()
ex.Status(iris.StatusOK).Body().Equal(v10Response)
ex.Header("X-API-Warn").Equal(versioning.DefaultDeprecationOptions.WarnMessage)

e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0.0").Expect().
Status(iris.StatusOK).Body().Equal(v2Response)
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1.0").Expect().
Status(iris.StatusOK).Body().Equal(v2Response)
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect().
Status(iris.StatusOK).Body().Equal(v2Response)
e.POST("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0.0").Expect().
Status(iris.StatusOK).Body().Equal(v2Response)
e.PUT("/api/user/other").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.0").Expect().
Status(iris.StatusOK).Body().Equal(v2Response)

e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect().
Status(iris.StatusNotImplemented).Body().Equal("version not found")
}

+ 216
- 16
versioning/version.go View File

@@ -5,31 +5,31 @@ import (
"strings"

"github.com/kataras/iris/v12/context"

"github.com/blang/semver/v4"
)

const (
// APIVersionResponseHeader the response header which its value contains
// the normalized semver matched version.
APIVersionResponseHeader = "X-Api-Version"
// AcceptVersionHeaderKey is the header key of "Accept-Version".
AcceptVersionHeaderKey = "Accept-Version"
// AcceptHeaderKey is the header key of "Accept".
AcceptHeaderKey = "Accept"
// AcceptHeaderVersionValue is the Accept's header value search term the requested version.
AcceptHeaderVersionValue = "version"

// Key is the context key of the version, can be used to manually modify the "requested" version.
// Example of how you can change the default behavior to extract a requested version (which is by headers)
// from a "version" url parameter instead:
// func(ctx iris.Context) { // &version=1
// ctx.Values().Set(versioning.Key, ctx.URLParamDefault("version", "1"))
// ctx.Next()
// }
//
// DEPRECATED: Use:
// version := ctx.URLParamDefault("version", "1")
// versioning.SetVersion(ctx, version) instead.
Key = "iris.api.version"
// NotFound is the key that can be used inside a `Map` or inside `ctx.SetVersion(versioning.NotFound)`
// to tell that a version wasn't found, therefore the not found handler should handle the request instead.
// to tell that a version wasn't found, therefore the `NotFoundHandler` should handle the request instead.
NotFound = "iris.api.version.notfound"
// Empty is just an empty string. Can be used as a key for a version alias
// when the requested version of a resource was not even specified by the client.
// The difference between NotFound and Empty is important when version aliases are registered:
// - A NotFound cannot be registered as version alias, it
// means that the client sent a version with its request
// but that version was not implemented by the server.
// - An Empty indicates that the client didn't send any version at all.
Empty = ""
)

// ErrNotFound reports whether a requested version
@@ -54,11 +54,89 @@ var NotFoundHandler = func(ctx *context.Context) {
ctx.StopWithPlainError(501, ErrNotFound)
}

// FromQuery is a simple helper which tries to
// set the version constraint from a given URL Query Parameter.
// The X-Api-Version is still valid.
func FromQuery(urlQueryParameterName string, defaultVersion string) context.Handler {
return func(ctx *context.Context) {
version := ctx.URLParam(urlQueryParameterName)
if version == "" {
version = defaultVersion
}

if version != "" {
SetVersion(ctx, version)
}

ctx.Next()
}
}

// If reports whether the "got" matches the "expected" one.
// the "expected" can be a constraint like ">=1.0.0 <2.0.0".
// This function is just a helper, better use the Group instead.
func If(got string, expected string) bool {
v, err := semver.Make(got)
if err != nil {
return false
}

validate, err := semver.ParseRange(expected)
if err != nil {
return false
}

return validate(v)
}

// Match reports whether the request matches the expected version.
// This function is just a helper, better use the Group instead.
func Match(ctx *context.Context, expectedVersion string) bool {
validate, err := semver.ParseRange(expectedVersion)
if err != nil {
return false
}

return matchVersionRange(ctx, validate)
}

func matchVersionRange(ctx *context.Context, validate semver.Range) bool {
gotVersion := GetVersion(ctx)

alias, aliasFound := GetVersionAlias(ctx, gotVersion)
if aliasFound {
SetVersion(ctx, alias) // set the version so next routes have it already.
gotVersion = alias
}

if gotVersion == "" {
return false
}

v, err := semver.Make(gotVersion)
if err != nil {
return false
}

if !validate(v) {
return false
}

versionString := v.String()

if !aliasFound { // don't lose any time to set if already set.
SetVersion(ctx, versionString)
}

ctx.Header(APIVersionResponseHeader, versionString)
return true
}

// GetVersion returns the current request version.
//
// By default the `GetVersion` will try to read from:
// - "Accept" header, i.e Accept: "application/json; version=1.0"
// - "Accept-Version" header, i.e Accept-Version: "1.0"
// - "Accept" header, i.e Accept: "application/json; version=1.0.0"
// - "Accept-Version" header, i.e Accept-Version: "1.0.0"
//
// However, the end developer can also set a custom version for a handler via a middleware by using the context's store key
// for versions (see `Key` for further details on that).
@@ -107,7 +185,129 @@ func GetVersion(ctx *context.Context) string {

// SetVersion force-sets the API Version.
// It can be used inside a middleware.
// Example of how you can change the default behavior to extract a requested version (which is by headers)
// from a "version" url parameter instead:
// func(ctx iris.Context) { // &version=1
// version := ctx.URLParamDefault("version", "1.0.0")
// versioning.SetVersion(ctx, version)
// ctx.Next()
// }
// See `GetVersion` too.
func SetVersion(ctx *context.Context, constraint string) {
ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetVersionContextKey(), constraint)
}

// AliasMap is just a type alias of the standard map[string]string.
// Head over to the `Aliases` function below for more.
type AliasMap = map[string]string

// Aliases is a middleware which registers version constraint aliases
// for the children Parties(routers). It's respected by versioning Groups.
//
// Example Code:
// app := iris.New()
//
// api := app.Party("/api")
// api.Use(Aliases(map[string]string{
// versioning.Empty: "1.0.0", // when no version was provided by the client.
// "beta": "4.0.0",
// "stage": "5.0.0-alpha"
// }))
//
// v1 := NewGroup(api, ">=1.0.0 < 2.0.0")
// v1.Get/Post...
//
// v4 := NewGroup(api, ">=4.0.0 < 5.0.0")
// v4.Get/Post...
//
// stage := NewGroup(api, "5.0.0-alpha")
// stage.Get/Post...
func Aliases(aliases AliasMap) context.Handler {
cp := make(AliasMap, len(aliases)) // copy the map here so we are safe of later modifications by end-dev.
for k, v := range aliases {
cp[k] = v
}

return func(ctx *context.Context) {
SetVersionAliases(ctx, cp, true)
ctx.Next()
}
}

// Handler returns a handler which is only fired
// when the "version" is matched with the requested one.
// It is not meant to be used by end-developers
// (exported for version controller feature).
// Use `NewGroup` instead.
func Handler(version string) context.Handler {
validate, err := semver.ParseRange(version)
if err != nil {
return func(ctx *context.Context) {
ctx.StopWithError(500, err)
}
}

return makeHandler(validate)
}

// GetVersionAlias returns the version alias of the given "gotVersion"
// or empty. It Reports whether the alias was found.
// See `SetVersionAliases`, `Aliases` and `Match` for more.
func GetVersionAlias(ctx *context.Context, gotVersion string) (string, bool) {
key := ctx.Application().ConfigurationReadOnly().GetVersionAliasesContextKey()
if key == "" {
return "", false
}

v := ctx.Values().Get(key)
if v == nil {
return "", false
}

aliases, ok := v.(AliasMap)
if !ok {
return "", false
}

version, ok := aliases[gotVersion]
if !ok {
return "", false
}

return strings.TrimSpace(version), true
}

// SetVersionAliases sets a map of version aliases when a requested
// version of a resource was not implemented by the server.
// Can be used inside a middleware to the parent Party
// and always before the child versioning groups (see `Aliases` function).
//
// The map's key (string) should be the "got version" (by the client)
// and the value should be the "version constraint to match" instead.
// The map's value(string) should be a registered version
// otherwise it will hit the NotFoundHandler (501, "version not found" by default).
//
// The given "aliases" is a type of standard map[string]string and
// should NOT be modified afterwards.
//
// The last "override" input argument indicates whether any
// existing aliases, registered by previous handlers in the chain,
// should be overridden or copied to the previous map one.
func SetVersionAliases(ctx *context.Context, aliases AliasMap, override bool) {
key := ctx.Application().ConfigurationReadOnly().GetVersionAliasesContextKey()
if key == "" {
return
}

v := ctx.Values().Get(key)
if v == nil || override {
ctx.Values().Set(key, aliases)
return
}

if existing, ok := v.(AliasMap); ok {
for k, v := range aliases {
existing[k] = v
}
}
}

+ 69
- 10
versioning/version_test.go View File

@@ -8,6 +8,15 @@ import (
"github.com/kataras/iris/v12/versioning"
)

func TestIf(t *testing.T) {
if expected, got := true, versioning.If("1.0.0", ">=1.0.0"); expected != got {
t.Fatalf("expected %s to be %s", "1.0.0", ">= 1.0.0")
}
if expected, got := true, versioning.If("1.2.3", "> 1.2.0"); expected != got {
t.Fatalf("expected %s to be %s", "1.2.3", "> 1.2.0")
}
}

func TestGetVersion(t *testing.T) {
app := iris.New()

@@ -23,16 +32,16 @@ func TestGetVersion(t *testing.T) {

e := httptest.New(t, app)

e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect().
Status(iris.StatusOK).Body().Equal("1.0")
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1").Expect().
Status(iris.StatusOK).Body().Equal("2.1")
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1 ;other=dsa").Expect().
Status(iris.StatusOK).Body().Equal("2.1")
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=2.1").Expect().
Status(iris.StatusOK).Body().Equal("2.1")
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=1").Expect().
Status(iris.StatusOK).Body().Equal("1")
e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect().
Status(iris.StatusOK).Body().Equal("1.0.0")
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1.0").Expect().
Status(iris.StatusOK).Body().Equal("2.1.0")
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1.0 ;other=dsa").Expect().
Status(iris.StatusOK).Body().Equal("2.1.0")
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=2.1.0").Expect().
Status(iris.StatusOK).Body().Equal("2.1.0")
e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=1.0.0").Expect().
Status(iris.StatusOK).Body().Equal("1.0.0")

// unknown versions.
e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "").Expect().
@@ -46,3 +55,53 @@ func TestGetVersion(t *testing.T) {

e.GET("/manual").Expect().Status(iris.StatusOK).Body().Equal("11.0.5")
}

func TestVersionAliases(t *testing.T) {
app := iris.New()

api := app.Party("/api")
api.Use(versioning.Aliases(map[string]string{
versioning.Empty: "1.0.0",
"stage": "2.0.0",
}))

writeVesion := func(ctx iris.Context) {
ctx.WriteString(versioning.GetVersion(ctx))
}

// A group without registration order.
v3 := versioning.NewGroup(api, ">= 3.0.0 < 4.0.0")
v3.Get("/", writeVesion)

v1 := versioning.NewGroup(api, ">= 1.0.0 < 2.0.0")
v1.Get("/", writeVesion)

v2 := versioning.NewGroup(api, ">= 2.0.0 < 3.0.0")
v2.Get("/", writeVesion)

api.Get("/manual", func(ctx iris.Context) {
versioning.SetVersion(ctx, "12.0.0")
ctx.Next()
}, writeVesion)

e := httptest.New(t, app)

// Make sure the SetVersion still works.
e.GET("/api/manual").Expect().Status(iris.StatusOK).Body().Equal("12.0.0")

// Test Empty default.
e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "").Expect().
Status(iris.StatusOK).Body().Equal("1.0.0")
// Test NotFound error, aliases are not responsible for that.
e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "4.0.0").Expect().
Status(iris.StatusNotImplemented).Body().Equal("version not found")
// Test "stage" alias.
e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "stage").Expect().
Status(iris.StatusOK).Body().Equal("2.0.0")
// Test version 2.
e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "2.0.0").Expect().
Status(iris.StatusOK).Body().Equal("2.0.0")
// Test version 3 (registered first).
e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "3.1.0").Expect().
Status(iris.StatusOK).Body().Equal("3.1.0")
}

+ 0
- 154
versioning/versioning.go View File

@@ -1,154 +0,0 @@
package versioning

import (
"github.com/kataras/iris/v12/context"

"github.com/hashicorp/go-version"
)

// If reports whether the "version" is matching to the "is".
// the "is" can be a constraint like ">= 1, < 3".
func If(v string, is string) bool {
_, ok := check(v, is)
return ok
}

func check(v string, is string) (string, bool) {
ver, err := version.NewVersion(v)
if err != nil {
return "", false
}

constraints, err := version.NewConstraint(is)
if err != nil {
return "", false
}

// return the extracted version from request, even if not matched.
return ver.String(), constraints.Check(ver)
}

// Match acts exactly the same as `If` does but instead it accepts
// a Context, so it can be called by a handler to determinate the requested version.
//
// If matched then it sets the "X-API-Version" response header and
// stores the matched version into Context (see `GetVersion` too).
func Match(ctx *context.Context, expectedVersion string) bool {
versionString, matched := check(GetVersion(ctx), expectedVersion)
if !matched {
return false
}

SetVersion(ctx, versionString)
ctx.Header("X-API-Version", versionString)
return true
}

// Handler returns a handler which stop the execution
// when the given "version" does not match with the requested one.
func Handler(version string) context.Handler {
return func(ctx *context.Context) {
if !Match(ctx, version) {
// The overlapped handler has an exception
// of a type of context.NotFound (which versioning.ErrNotFound wraps)
// to clear the status code
// and the error to ignore this
// when available match version exists (see `NewGroup`).
NotFoundHandler(ctx)
return
}

ctx.Next()
}
}

// Map is a map of versions targets to a handlers,
// a handler per version or constraint, the key can be something like ">1, <=2" or just "1".
type Map map[string]context.Handler

// NewMatcher creates a single handler which decides what handler
// should be executed based on the requested version.
//
// Use the `NewGroup` if you want to add many routes under a specific version.
//
// See `Map` and `NewGroup` too.
func NewMatcher(versions Map) context.Handler {
constraintsHandlers, notFoundHandler := buildConstraints(versions)

return func(ctx *context.Context) {
versionString := GetVersion(ctx)
if versionString == "" || versionString == NotFound {
notFoundHandler(ctx)
return
}

ver, err := version.NewVersion(versionString)
if err != nil {
notFoundHandler(ctx)
return
}

for _, ch := range constraintsHandlers {
if ch.constraints.Check(ver) {
ctx.Header("X-API-Version", ver.String())
ch.handler(ctx)
return
}
}

// pass the not matched version so the not found handler can have knowedge about it.
// SetVersion(ctx, versionString)
// or let a manual cal of GetVersion(ctx) do that instead.
notFoundHandler(ctx)
}
}

type constraintsHandler struct {
constraints version.Constraints
handler context.Handler
}

func buildConstraints(versionsHandler Map) (constraintsHandlers []*constraintsHandler, notfoundHandler context.Handler) {
for v, h := range versionsHandler {
if v == NotFound {
notfoundHandler = h
continue
}

constraints, err := version.NewConstraint(v)
if err != nil {
panic(err)
}

constraintsHandlers = append(constraintsHandlers, &constraintsHandler{
constraints: constraints,
handler: h,
})
}

if notfoundHandler == nil {
notfoundHandler = NotFoundHandler
}

// no sort, the end-dev should declare
// all version constraint, i.e < 4.0 may be catch 1.0 if not something like
// >= 3.0, < 4.0.
// I can make it ordered but I do NOT like the final API of it:
/*
app.Get("/api/user", NewMatcher( // accepts an array, ordered, see last elem.
V("1.0", vHandler("v1 here")),
V("2.0", vHandler("v2 here")),
V("< 4.0", vHandler("v3.x here")),
))
instead we have:

app.Get("/api/user", NewMatcher(Map{ // accepts a map, unordered, see last elem.
"1.0": Deprecated(vHandler("v1 here")),
"2.0": vHandler("v2 here"),
">= 3.0, < 4.0": vHandler("v3.x here"),
VersionUnknown: customHandlerForNotMatchingVersion,
}))
*/

return
}

+ 0
- 135
versioning/versioning_test.go View File

@@ -1,135 +0,0 @@
package versioning_test

import (
"testing"

"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/httptest"
"github.com/kataras/iris/v12/versioning"
)

func notFoundHandler(ctx iris.Context) {
ctx.NotFound()
}

const (
v10Response = "v1.0 handler"
v2Response = "v2.x handler"
)

func sendHandler(contents string) iris.Handler {
return func(ctx iris.Context) {
ctx.WriteString(contents)
}
}

func TestIf(t *testing.T) {
if expected, got := true, versioning.If("1.0", ">=1"); expected != got {
t.Fatalf("expected %s to be %s", "1.0", ">= 1")
}
if expected, got := true, versioning.If("1.2.3", "> 1.2"); expected != got {
t.Fatalf("expected %s to be %s", "1.2.3", "> 1.2")
}
}

func TestNewMatcher(t *testing.T) {
app := iris.New()

userAPI := app.Party("/api/user")
userAPI.Get("/", versioning.NewMatcher(versioning.Map{
"1.0": sendHandler(v10Response),
">= 2, < 3": sendHandler(v2Response),
versioning.NotFound: notFoundHandler,
}))

// middleware as usual.
myMiddleware := func(ctx iris.Context) {
ctx.Header("X-Custom", "something")
ctx.Next()
}
myVersions := versioning.Map{
"1.0": sendHandler(v10Response),
}

userAPI.Get("/with_middleware", myMiddleware, versioning.NewMatcher(myVersions))

e := httptest.New(t, app)

e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect().
Status(iris.StatusOK).Body().Equal(v10Response)
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0").Expect().
Status(iris.StatusOK).Body().Equal(v2Response)
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1").Expect().
Status(iris.StatusOK).Body().Equal(v2Response)
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect().
Status(iris.StatusOK).Body().Equal(v2Response)

// middleware as usual.
ex := e.GET("/api/user/with_middleware").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect()
ex.Status(iris.StatusOK).Body().Equal(v10Response)
ex.Header("X-Custom").Equal("something")

e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect().
Status(iris.StatusNotFound).Body().Equal("Not Found")
}

func TestNewGroup(t *testing.T) {
app := iris.New()

userAPI := app.Party("/api/user")
// [... static serving, middlewares and etc goes here].

userAPIV10 := versioning.NewGroup(userAPI, "1.0").Deprecated(versioning.DefaultDeprecationOptions)
// V10middlewareResponse := "m1"
// userAPIV10.Use(func(ctx iris.Context) {
// println("exec userAPIV10.Use - midl1")
// sendHandler(V10middlewareResponse)(ctx)
// ctx.Next()
// })
// userAPIV10.Use(func(ctx iris.Context) {
// println("exec userAPIV10.Use - midl2")
// sendHandler(V10middlewareResponse + "midl2")(ctx)
// ctx.Next()
// })
// userAPIV10.Use(func(ctx iris.Context) {
// println("exec userAPIV10.Use - midl3")
// ctx.Next()
// })

userAPIV10.Get("/", sendHandler(v10Response))
userAPIV2 := versioning.NewGroup(userAPI, ">= 2, < 3")
// V2middlewareResponse := "m2"
// userAPIV2.Use(func(ctx iris.Context) {
// println("exec userAPIV2.Use - midl1")
// sendHandler(V2middlewareResponse)(ctx)
// ctx.Next()
// })
// userAPIV2.Use(func(ctx iris.Context) {
// println("exec userAPIV2.Use - midl2")
// ctx.Next()
// })

userAPIV2.Get("/", sendHandler(v2Response))
userAPIV2.Post("/", sendHandler(v2Response))
userAPIV2.Put("/other", sendHandler(v2Response))

e := httptest.New(t, app)

ex := e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect()
ex.Status(iris.StatusOK).Body().Equal(v10Response)
ex.Header("X-API-Warn").Equal(versioning.DefaultDeprecationOptions.WarnMessage)

e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0").Expect().
Status(iris.StatusOK).Body().Equal(v2Response)
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1").Expect().
Status(iris.StatusOK).Body().Equal(v2Response)
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect().
Status(iris.StatusOK).Body().Equal(v2Response)
e.POST("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2").Expect().
Status(iris.StatusOK).Body().Equal(v2Response)
e.PUT("/api/user/other").WithHeader(versioning.AcceptVersionHeaderKey, "2.9").Expect().
Status(iris.StatusOK).Body().Equal(v2Response)

e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect().
Status(iris.StatusNotImplemented).Body().Equal("version not found")
}

Loading…
Cancel
Save