From 56f35ccf00a09ef9ecca1e7e211fac762ccd2c98 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 23 Dec 2020 10:23:56 +0200 Subject: [PATCH 01/12] Thanks for your kind donation @nikharsaxena to the Iris Web Framework :heart: --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c8e78abb..abb4344d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ With your help, we can improve Open Source web development for everyone! > Donations from **China** are now accepted!

+ Nikhar Saxena Servio Zambrano Nate Anderson Claude Muller From e2b481dea1ef1f3958cf78ac758b742a749ab33f Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 24 Dec 2020 23:49:49 +0200 Subject: [PATCH 02/12] Merry Christmas :santa: :christmas_tree: --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index abb4344d..6dba82fa 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,15 @@ [![Black Lives Matter](https://iris-go.com/images/blacklivesmatter_banner.png)](https://support.eji.org/give/153413/#!/donation/checkout) - + + +## 🎅 + +Let me take this opportunity to thank you for all your faith and dedication to the Iris Open Source Project. We have achieved so many things together. Hope your bond lasts forever! **Merry Christmas🌳** From 8fef8fd04c785f9a976e5ce325fd751f90eb92b2 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 27 Dec 2020 13:23:14 +0200 Subject: [PATCH 03/12] context.UploadFormFiles: security fix --- README.md | 2 +- context/context.go | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6dba82fa..53010c35 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ## 🎅 -Let me take this opportunity to thank you for all your faith and dedication to the Iris Open Source Project. We have achieved so many things together. Hope your bond lasts forever! **Merry Christmas🌳** +Let me take this opportunity to thank you for all your faith and dedication to the Iris Open Source Project. We have achieved so many things together. Hope your bond lasts forever! **Merry Christmas🎄** diff --git a/context/context.go b/context/context.go index b5e139b2..efedee3f 100644 --- a/context/context.go +++ b/context/context.go @@ -1974,6 +1974,13 @@ func (ctx *Context) UploadFormFiles(destDirectory string, before ...func(*Contex for _, files := range fhs { innerLoop: for _, file := range files { + // Fix an issue that net/http has, + // an attacker can push a filename + // which could lead to override existing system files + // by ../../$file. + // Reported by Frank through security reports. + file.Filename = strings.TrimLeft(file.Filename, "../") + file.Filename = strings.TrimLeft(file.Filename, "..\\") for _, b := range before { if !b(ctx, file) { From 3e6fd799066f3b7af939b66c362c672333c0bfb6 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 27 Dec 2020 18:51:06 +0200 Subject: [PATCH 04/12] add deepsource.toml --- .deepsource.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 00000000..9f17eca9 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,16 @@ +version = 1 + +test_patterns = ["*_test.go"] + +exclude_patterns = [ + "_examples/**", + "_benchmarks/**", + ".github/**" +] + +[[analyzers]] +name = "go" +enabled = true + + [analyzers.meta] + import_paths = ["github.com/kataras/iris"] From b4400d94dfe0a5384ef580fcb5f3bab8684673ed Mon Sep 17 00:00:00 2001 From: yanghuiwen Date: Tue, 29 Dec 2020 15:45:57 +0800 Subject: [PATCH 05/12] fix response.timestamp not appear in correct request. --- _examples/mvc/error-handler-preflight/main.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/_examples/mvc/error-handler-preflight/main.go b/_examples/mvc/error-handler-preflight/main.go index 34289a95..1ffd8fb6 100644 --- a/_examples/mvc/error-handler-preflight/main.go +++ b/_examples/mvc/error-handler-preflight/main.go @@ -29,9 +29,9 @@ type response struct { Timestamp int64 `json:"timestamp,omitempty"` } -func (r response) Preflight(ctx iris.Context) error { - if r.ID > 0 { - r.Timestamp = time.Now().Unix() +func (r *response) Preflight(ctx iris.Context) error { + if (*r).ID > 0 { + (*r).Timestamp = time.Now().Unix() } if code := r.Code; code > 0 { @@ -46,7 +46,7 @@ func (r response) Preflight(ctx iris.Context) error { Code: code, /* use any r.Data as the template data OR the whole "response" as its data. */ - Data: r, + Data: *r, /* automatically pick the template per error (just for the sake of the example) */ Name: fmt.Sprintf("%d", code), }.Dispatch(ctx) @@ -54,7 +54,7 @@ func (r response) Preflight(ctx iris.Context) error { return iris.ErrStopExecution } - ctx.StatusCode(r.Code) + ctx.StatusCode((*r).Code) } return nil @@ -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}, } From a2d0b868151807c7503dc967da136f917c92f692 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Tue, 29 Dec 2020 16:38:24 +0200 Subject: [PATCH 06/12] accesslog: add FileUnbuffered helper --- go.mod | 2 +- middleware/accesslog/accesslog.go | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index ba0f98a0..6d07aa51 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/json-iterator/go v1.1.10 github.com/kataras/blocks v0.0.4 github.com/kataras/golog v0.1.6 - github.com/kataras/jwt v0.0.8 + github.com/kataras/jwt v0.0.9 github.com/kataras/neffos v0.0.18 github.com/kataras/pio v0.0.10 github.com/kataras/sitemap v0.0.5 diff --git a/middleware/accesslog/accesslog.go b/middleware/accesslog/accesslog.go index 195669b3..2adb23d3 100644 --- a/middleware/accesslog/accesslog.go +++ b/middleware/accesslog/accesslog.go @@ -323,6 +323,18 @@ func New(w io.Writer) *AccessLog { // // It panics on error. func File(path string) *AccessLog { + f := mustOpenFile(path) + return New(bufio.NewReadWriter(bufio.NewReader(f), bufio.NewWriter(f))) +} + +// FileUnbuffered same as File but it does not buffers 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. From 8b710b13025a27df8feb15b59382e50d7a51b80b Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Fri, 1 Jan 2021 23:01:27 +0200 Subject: [PATCH 07/12] Happy New Year :heart: --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 53010c35..ddf7ea10 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ > ![](https://iris-go.com/images/cli.png) Try the official [Iris Command Line Interface](https://github.com/kataras/iris-cli) today! --> -## 🎅 +# Happy New Year 🎉 -Let me take this opportunity to thank you for all your faith and dedication to the Iris Open Source Project. We have achieved so many things together. Hope your bond lasts forever! **Merry Christmas🎄** +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. @@ -36,6 +36,9 @@ With your help, we can improve Open Source web development for everyone! > Donations from **China** are now accepted!

+ KIT UNITED + Ricardo Hernandez Lopez + ChinChuanKuo Nikhar Saxena Servio Zambrano Nate Anderson From 7aa2d1f9d50c98efdd8a8a96e4d86629114b69f7 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Tue, 5 Jan 2021 18:16:32 +0200 Subject: [PATCH 08/12] make versioning.Group a Party compatible by using a type alias on its embedded field was reported as missing of the a Party method, the type alias is a good hack to solve that --- _examples/routing/versioning/main.go | 11 +++++++++++ versioning/group.go | 11 ++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/_examples/routing/versioning/main.go b/_examples/routing/versioning/main.go index 3502173f..f234c9af 100644 --- a/_examples/routing/versioning/main.go +++ b/_examples/routing/versioning/main.go @@ -67,6 +67,10 @@ func examplePerParty(app *iris.Application) { usersAPIV2.Post("/", func(ctx iris.Context) { ctx.Writef("v2 resource: /api/users post handler") }) + + // version 3, pass it as a common iris.Party. + usersAPIV3 := versioning.NewGroup(usersAPI, ">= 3, < 4") + registerAPIV3(usersAPIV3) } func catsVersionExactly1Handler(ctx iris.Context) { @@ -76,3 +80,10 @@ func catsVersionExactly1Handler(ctx iris.Context) { func catsV2Handler(ctx iris.Context) { ctx.Writef("v2 resource: /api/cats handler") } + +func registerAPIV3(p iris.Party) { + p.Get("/", func(ctx iris.Context) { + ctx.Writef("v3 resource: /api/users handler") + }) + // [...] +} diff --git a/versioning/group.go b/versioning/group.go index 9e7f0e06..d74d96c1 100644 --- a/versioning/group.go +++ b/versioning/group.go @@ -5,10 +5,15 @@ import ( "github.com/kataras/iris/v12/core/router" ) +// API is a type alias of router.Party. +// This is required in order for a Group instance +// to implement the Party interface without field conflict. +type API = router.Party + // Group is a group of version-based routes. // One version per one or more routes. type Group struct { - router.Party + API // Information not currently in-use. version string @@ -32,7 +37,7 @@ func NewGroup(r router.Party, version string) *Group { r.UseOnce(Handler(version)) // this is required in order to not populate this middleware to the next group. return &Group{ - Party: r, + API: r, version: version, } } @@ -43,7 +48,7 @@ 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() }) From b409f7807e548bcd7b67eccd9015c18b14399c80 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 6 Jan 2021 01:52:39 +0200 Subject: [PATCH 09/12] New feature: versioning.Aliases Thanks @mulyawansentosa and @remopavithran for your donates :heart: --- HISTORY.md | 23 +++ README.md | 2 + _examples/auth/basicauth/basic/main.go | 13 +- _examples/routing/basic/main.go | 2 +- _examples/routing/main.go | 4 +- _examples/routing/overview/main.go | 2 +- _examples/routing/subdomains/wildcard/main.go | 2 +- _examples/routing/versioning/main.go | 10 +- .../sessions/overview/example/example.go | 2 +- _examples/sessions/securecookie/main_test.go | 6 +- _examples/testing/httptest/main.go | 5 + _examples/view/template_html_4/main.go | 2 +- configuration.go | 35 +++-- context/configuration.go | 2 + context/context_user.go | 18 +-- context/handler.go | 4 + context/route.go | 4 + core/router/api_builder.go | 40 +++++- core/router/party.go | 4 + core/router/route.go | 18 ++- hero/reflect.go | 2 +- middleware/accesslog/accesslog.go | 2 +- sessions/sessiondb/boltdb/database.go | 4 +- versioning/deprecation.go | 19 ++- versioning/group.go | 45 +++++- versioning/version.go | 132 ++++++++++++++++-- versioning/version_test.go | 50 +++++++ versioning/versioning.go | 29 +++- 28 files changed, 396 insertions(+), 85 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 6a333341..dd6f5a25 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,29 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- New `versioning.Aliases` middleware. 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, < 2") +v1.Get/Post... + +v4 := NewGroup(api, ">= 4, < 5") +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. diff --git a/README.md b/README.md index ddf7ea10..39c5cae0 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ With your help, we can improve Open Source web development for everyone! > Donations from **China** are now accepted!

+ Pavithran + MULYAWAN SENTOSA KIT UNITED Ricardo Hernandez Lopez ChinChuanKuo diff --git a/_examples/auth/basicauth/basic/main.go b/_examples/auth/basicauth/basic/main.go index 36465501..7c663cfd 100644 --- a/_examples/auth/basicauth/basic/main.go +++ b/_examples/auth/basicauth/basic/main.go @@ -64,14 +64,11 @@ func main() { } func handler(ctx iris.Context) { - // username, password, _ := ctx.Request().BasicAuth() - // third parameter it will be always true because the middleware - // makes sure for that, otherwise this handler will not be executed. - // OR: - user := ctx.User() - // OR ctx.User().GetRaw() to get the underline value. - username, _ := user.GetUsername() - password, _ := user.GetPassword() + // user := ctx.User().(*myUserType) + // or ctx.User().GetRaw().(*myUserType) + // ctx.Writef("%s %s:%s", ctx.Path(), user.Username, user.Password) + // OR if you don't have registered custom User structs: + username, password, _ := ctx.Request().BasicAuth() ctx.Writef("%s %s:%s", ctx.Path(), username, password) } diff --git a/_examples/routing/basic/main.go b/_examples/routing/basic/main.go index 1ba67a82..a22c1eb6 100644 --- a/_examples/routing/basic/main.go +++ b/_examples/routing/basic/main.go @@ -152,7 +152,7 @@ func newApp() *iris.Application { } // wildcard subdomains. - wildcardSubdomain := app.Party("*.") + wildcardSubdomain := app.WildcardSubdomain() { wildcardSubdomain.Get("/", func(ctx iris.Context) { ctx.Writef("Subdomain can be anything, now you're here from: %s", ctx.Subdomain()) diff --git a/_examples/routing/main.go b/_examples/routing/main.go index d7537ea4..5dd843c0 100644 --- a/_examples/routing/main.go +++ b/_examples/routing/main.go @@ -82,11 +82,11 @@ func registerGamesRoutes(app *iris.Application) { } func registerSubdomains(app *iris.Application) { - mysubdomain := app.Party("mysubdomain.") + mysubdomain := app.Subdomain("mysubdomain") // http://mysubdomain.myhost.com mysubdomain.Get("/", h) - willdcardSubdomain := app.Party("*.") + willdcardSubdomain := app.WildcardSubdomain() willdcardSubdomain.Get("/", h) willdcardSubdomain.Party("/party").Get("/", h) } diff --git a/_examples/routing/overview/main.go b/_examples/routing/overview/main.go index 2e327a2a..0c42bfcd 100644 --- a/_examples/routing/overview/main.go +++ b/_examples/routing/overview/main.go @@ -116,7 +116,7 @@ func main() { adminRoutes.Get("/settings", info) // Wildcard/dynamic subdomain - dynamicSubdomainRoutes := app.Party("*.") + dynamicSubdomainRoutes := app.WildcardSubdomain() // GET: http://any_thing_here.localhost:8080 dynamicSubdomainRoutes.Get("/", info) diff --git a/_examples/routing/subdomains/wildcard/main.go b/_examples/routing/subdomains/wildcard/main.go index 0eb3dc30..fb12f8f0 100644 --- a/_examples/routing/subdomains/wildcard/main.go +++ b/_examples/routing/subdomains/wildcard/main.go @@ -33,7 +33,7 @@ func main() { }*/ // no order, you can register subdomains at the end also. - dynamicSubdomains := app.Party("*.") + dynamicSubdomains := app.WildcardSubdomain() { dynamicSubdomains.Get("/", dynamicSubdomainHandler) diff --git a/_examples/routing/versioning/main.go b/_examples/routing/versioning/main.go index f234c9af..1e621f33 100644 --- a/_examples/routing/versioning/main.go +++ b/_examples/routing/versioning/main.go @@ -45,10 +45,12 @@ func examplePerParty(app *iris.Application) { // 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() - }) + // usersAPI.Use(func(ctx iris.Context) { + // versioning.SetVersion(ctx, ctx.URLParamDefault("version", "1")) + // ctx.Next() + // }) + // OR: + usersAPI.Use(versioning.FromQuery("version", "1")) // version 1. usersAPIV1 := versioning.NewGroup(usersAPI, ">= 1, < 2") diff --git a/_examples/sessions/overview/example/example.go b/_examples/sessions/overview/example/example.go index e47624d7..7472e571 100644 --- a/_examples/sessions/overview/example/example.go +++ b/_examples/sessions/overview/example/example.go @@ -114,7 +114,7 @@ func NewApp(sess *sessions.Sessions) *iris.Application { app.Get("/delete", func(ctx iris.Context) { session := sessions.Get(ctx) // delete a specific key - session.Delete("name") + session.Delete("username") }) app.Get("/clear", func(ctx iris.Context) { diff --git a/_examples/sessions/securecookie/main_test.go b/_examples/sessions/securecookie/main_test.go index 775c698d..6b0304b1 100644 --- a/_examples/sessions/securecookie/main_test.go +++ b/_examples/sessions/securecookie/main_test.go @@ -16,12 +16,12 @@ func TestSessionsEncodeDecode(t *testing.T) { es.Cookies().NotEmpty() es.Body().Equal("All ok session set to: iris [isNew=true]") - e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: iris") + e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The username on the /set was: iris") // delete and re-get e.GET("/delete").Expect().Status(iris.StatusOK) - e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: ") + e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The username on the /set was: ") // set, clear and re-get e.GET("/set").Expect().Body().Equal("All ok session set to: iris [isNew=false]") e.GET("/clear").Expect().Status(iris.StatusOK) - e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: ") + e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The username on the /set was: ") } diff --git a/_examples/testing/httptest/main.go b/_examples/testing/httptest/main.go index a2bb3101..35dcf78c 100644 --- a/_examples/testing/httptest/main.go +++ b/_examples/testing/httptest/main.go @@ -37,6 +37,11 @@ func h(ctx iris.Context) { // third parameter it will be always true because the middleware // makes sure for that, otherwise this handler will not be executed. // OR: + // + // user := ctx.User().(*myUserType) + // ctx.Writef("%s %s:%s", ctx.Path(), user.Username, user.Password) + // OR if you don't have registered custom User structs: + // // ctx.User().GetUsername() // ctx.User().GetPassword() ctx.Writef("%s %s:%s", ctx.Path(), username, password) diff --git a/_examples/view/template_html_4/main.go b/_examples/view/template_html_4/main.go index ef0b97b8..9c0e4b2b 100644 --- a/_examples/view/template_html_4/main.go +++ b/_examples/view/template_html_4/main.go @@ -27,7 +27,7 @@ func main() { // wildcard subdomain, will catch username1.... username2.... username3... username4.... username5... // that our below links are providing via page.html's first argument which is the subdomain. - subdomain := app.Party("*.") + subdomain := app.WildcardSubdomain() mypathRoute := subdomain.Get("/mypath", emptyHandler) mypathRoute.Name = "my-page1" diff --git a/configuration.go b/configuration.go index bc9f04e8..7d76de91 100644 --- a/configuration.go +++ b/configuration.go @@ -767,6 +767,11 @@ type Configuration struct { // via a middleware through `SetVersion` method, e.g. `versioning.SetVersion(ctx, "1.0, 1.1")`. // Defaults to "iris.api.version". VersionContextKey string `ini:"version_context_key" json:"versionContextKey" yaml:"VersionContextKey" toml:"VersionContextKey"` + // VersionAliasesContextKey is the context key which the versioning feature + // can look up for alternative values of a version and fallback to that. + // Head over to the versioning package for more. + // Defaults to "iris.api.version.aliases" + VersionAliasesContextKey string `ini:"version_aliases_context_key" json:"versionAliasesContextKey" yaml:"VersionAliasesContextKey" toml:"VersionAliasesContextKey"` // ViewEngineContextKey is the context's values key // responsible to store and retrieve(view.Engine) the current view engine. // A middleware or a Party can modify its associated value to change @@ -974,6 +979,11 @@ func (c Configuration) GetVersionContextKey() string { return c.VersionContextKey } +// GetVersionAliasesContextKey returns the VersionAliasesContextKey field. +func (c Configuration) GetVersionAliasesContextKey() string { + return c.VersionAliasesContextKey +} + // GetViewEngineContextKey returns the ViewEngineContextKey field. func (c Configuration) GetViewEngineContextKey() string { return c.ViewEngineContextKey @@ -1132,6 +1142,10 @@ func WithConfiguration(c Configuration) Configurator { main.VersionContextKey = v } + if v := c.VersionAliasesContextKey; v != "" { + main.VersionAliasesContextKey = v + } + if v := c.ViewEngineContextKey; v != "" { main.ViewEngineContextKey = v } @@ -1205,16 +1219,17 @@ func DefaultConfiguration() Configuration { // The request body the size limit // can be set by the middleware `LimitRequestBodySize` // or `context#SetMaxRequestBodySize`. - PostMaxMemory: 32 << 20, // 32MB - LocaleContextKey: "iris.locale", - LanguageContextKey: "iris.locale.language", - LanguageInputContextKey: "iris.locale.language.input", - VersionContextKey: "iris.api.version", - ViewEngineContextKey: "iris.view.engine", - ViewLayoutContextKey: "iris.view.layout", - ViewDataContextKey: "iris.view.data", - RemoteAddrHeaders: nil, - RemoteAddrHeadersForce: false, + PostMaxMemory: 32 << 20, // 32MB + LocaleContextKey: "iris.locale", + LanguageContextKey: "iris.locale.language", + LanguageInputContextKey: "iris.locale.language.input", + VersionContextKey: "iris.api.version", + VersionAliasesContextKey: "iris.api.version.aliases", + ViewEngineContextKey: "iris.view.engine", + ViewLayoutContextKey: "iris.view.layout", + ViewDataContextKey: "iris.view.data", + RemoteAddrHeaders: nil, + RemoteAddrHeadersForce: false, RemoteAddrPrivateSubnets: []netutil.IPRange{ { Start: "10.0.0.0", diff --git a/context/configuration.go b/context/configuration.go index ea0e7f25..c2dcb6e6 100644 --- a/context/configuration.go +++ b/context/configuration.go @@ -53,6 +53,8 @@ type ConfigurationReadOnly interface { GetLanguageInputContextKey() string // GetVersionContextKey returns the VersionContextKey field. GetVersionContextKey() string + // GetVersionAliasesContextKey returns the VersionAliasesContextKey field. + GetVersionAliasesContextKey() string // GetViewEngineContextKey returns the ViewEngineContextKey field. GetViewEngineContextKey() string diff --git a/context/context_user.go b/context/context_user.go index 794b78a9..d76ba6d4 100644 --- a/context/context_user.go +++ b/context/context_user.go @@ -81,15 +81,15 @@ to the end-developer's custom implementations. // SimpleUser is a simple implementation of the User interface. type SimpleUser struct { - Authorization string `json:"authorization,omitempty"` - AuthorizedAt time.Time `json:"authorized_at,omitempty"` - ID string `json:"id,omitempty"` - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Email string `json:"email,omitempty"` - Roles []string `json:"roles,omitempty"` - Token json.RawMessage `json:"token,omitempty"` - Fields Map `json:"fields,omitempty"` + Authorization string `json:"authorization,omitempty" db:"authorization"` + AuthorizedAt time.Time `json:"authorized_at,omitempty" db:"authorized_at"` + ID string `json:"id,omitempty" db:"id"` + Username string `json:"username,omitempty" db:"username"` + Password string `json:"password,omitempty" db:"password"` + Email string `json:"email,omitempty" db:"email"` + Roles []string `json:"roles,omitempty" db:"roles"` + Token json.RawMessage `json:"token,omitempty" db:"token"` + Fields Map `json:"fields,omitempty" db:"fields"` } var _ User = (*SimpleUser)(nil) diff --git a/context/handler.go b/context/handler.go index d8744ee6..13f94128 100644 --- a/context/handler.go +++ b/context/handler.go @@ -239,6 +239,10 @@ var ignoreMainHandlerNames = [...]string{ "iris.reCAPTCHA", "iris.profiling", "iris.recover", + "iris.accesslog", + "iris.grpc", + "iris.requestid", + "iris.rewrite", } // ingoreMainHandlerName reports whether a main handler of "name" should diff --git a/context/route.go b/context/route.go index 8f3db812..4cc4794e 100644 --- a/context/route.go +++ b/context/route.go @@ -62,6 +62,10 @@ type RouteReadOnly interface { // MainHandlerIndex returns the first registered handler's index for the route. MainHandlerIndex() int + // Property returns a specific property based on its "key" + // of this route's Party owner. + Property(key string) (interface{}, bool) + // Sitemap properties: https://www.sitemaps.org/protocol.html // GetLastMod returns the date of last modification of the file served by this route. diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 34f3769a..9b4ccac6 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -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 map[string]interface{} + for k, v := range api.properties { + properties[k] = v + } + childAPI := &APIBuilder{ // global/api builder logger: api.logger, macros: api.macros, + properties: properties, routes: api.routes, routesNoLog: api.routesNoLog, beginGlobalHandlers: api.beginGlobalHandlers, @@ -808,6 +822,16 @@ func (api *APIBuilder) Macros() *macro.Macros { return api.macros } +// Properties returns the original Party's properties map, +// it can be modified before server startup but not afterwards. +func (api *APIBuilder) Properties() context.Map { + if api.properties == nil { + api.properties = make(context.Map) + } + + return api.properties +} + // GetRoutes returns the routes information, // some of them can be changed at runtime some others not. // @@ -1096,6 +1120,8 @@ func (api *APIBuilder) DoneGlobal(handlers ...context.Handler) { // RemoveHandler deletes a handler from begin and done handlers // based on its name or the handler pc function. +// Note that UseGlobal and DoneGlobal handlers cannot be removed +// through this method as they were registered to the routes already. // // As an exception, if one of the arguments is a pointer to an int, // then this is used to set the total amount of removed handlers. diff --git a/core/router/party.go b/core/router/party.go index 016c6d7e..06fe1f66 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -39,6 +39,10 @@ type Party interface { // Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path Macros() *macro.Macros + // Properties returns the original Party's properties map, + // it can be modified before server startup but not afterwards. + Properties() context.Map + // SetRoutesNoLog disables (true) the verbose logging for the next registered // routes under this Party and its children. // diff --git a/core/router/route.go b/core/router/route.go index 1208bf5d..6df3190f 100644 --- a/core/router/route.go +++ b/core/router/route.go @@ -19,6 +19,8 @@ import ( // If any of the following fields are changed then the // caller should Refresh the router. type Route struct { + // The Party which this Route was created and registered on. + Party Party Title string `json:"title"` // custom name to replace the method on debug logging. Name string `json:"name"` // "userRoute" Description string `json:"description"` // "lists a user" @@ -86,7 +88,7 @@ type Route struct { // handlers and the macro container which all routes should share. // It parses the path based on the "macros", // handlers are being changed to validate the macros at serve time, if needed. -func NewRoute(statusErrorCode int, method, subdomain, unparsedPath string, +func NewRoute(p Party, statusErrorCode int, method, subdomain, unparsedPath string, handlers context.Handlers, macros macro.Macros) (*Route, error) { tmpl, err := macro.Parse(unparsedPath, macros) if err != nil { @@ -110,6 +112,7 @@ func NewRoute(statusErrorCode int, method, subdomain, unparsedPath string, formattedPath := formatPath(path) route := &Route{ + Party: p, StatusCode: statusErrorCode, Name: defaultName, Method: method, @@ -583,6 +586,8 @@ type routeReadOnlyWrapper struct { *Route } +var _ context.RouteReadOnly = routeReadOnlyWrapper{} + func (rd routeReadOnlyWrapper) StatusErrorCode() int { return rd.Route.StatusCode } @@ -619,6 +624,17 @@ func (rd routeReadOnlyWrapper) MainHandlerIndex() int { return rd.Route.MainHandlerIndex } +func (rd routeReadOnlyWrapper) Property(key string) (interface{}, bool) { + properties := rd.Route.Party.Properties() + if properties != nil { + if property, ok := properties[key]; ok { + return property, true + } + } + + return nil, false +} + func (rd routeReadOnlyWrapper) GetLastMod() time.Time { return rd.Route.LastMod } diff --git a/hero/reflect.go b/hero/reflect.go index 64c53eba..4619c91b 100644 --- a/hero/reflect.go +++ b/hero/reflect.go @@ -18,7 +18,7 @@ func valueOf(v interface{}) reflect.Value { // indirectType returns the value of a pointer-type "typ". // If "typ" is a pointer, array, chan, map or slice it returns its Elem, -// otherwise returns the typ as it's. +// otherwise returns the "typ" as it is. func indirectType(typ reflect.Type) reflect.Type { switch typ.Kind() { case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: diff --git a/middleware/accesslog/accesslog.go b/middleware/accesslog/accesslog.go index 2adb23d3..99b89415 100644 --- a/middleware/accesslog/accesslog.go +++ b/middleware/accesslog/accesslog.go @@ -327,7 +327,7 @@ func File(path string) *AccessLog { return New(bufio.NewReadWriter(bufio.NewReader(f), bufio.NewWriter(f))) } -// FileUnbuffered same as File but it does not buffers the data, +// 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) diff --git a/sessions/sessiondb/boltdb/database.go b/sessions/sessiondb/boltdb/database.go index a2b2335c..11b667e9 100644 --- a/sessions/sessiondb/boltdb/database.go +++ b/sessions/sessiondb/boltdb/database.go @@ -339,14 +339,14 @@ func (db *Database) Visit(sid string, cb func(key string, value interface{})) er } // Len returns the length of the session's entries (keys). -func (db *Database) Len(sid string) (n int64) { +func (db *Database) Len(sid string) (n int) { err := db.Service.View(func(tx *bolt.Tx) error { b := db.getBucketForSession(tx, sid) if b == nil { return nil } - n = int64(b.Stats().KeyN) + n = int(int64(b.Stats().KeyN)) return nil }) diff --git a/versioning/deprecation.go b/versioning/deprecation.go index 7e28d421..a67a9f1e 100644 --- a/versioning/deprecation.go +++ b/versioning/deprecation.go @@ -6,10 +6,17 @@ import ( "github.com/kataras/iris/v12/context" ) +// The response header keys when a resource is deprecated by the server. +const ( + APIWarnHeader = "X-Api-Warn" + APIDeprecationDateHeader = "X-Api-Deprecation-Date" + APIDeprecationInfoHeader = "X-Api-Deprecation-Info" +) + // DeprecationOptions describes the deprecation headers key-values. -// - "X-API-Warn": options.WarnMessage -// - "X-API-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate)) -// - "X-API-Deprecation-Info": options.DeprecationInfo +// - "X-Api-Warn": options.WarnMessage +// - "X-Api-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate)) +// - "X-Api-Deprecation-Info": options.DeprecationInfo type DeprecationOptions struct { WarnMessage string DeprecationDate time.Time @@ -37,14 +44,14 @@ func WriteDeprecated(ctx *context.Context, options DeprecationOptions) { options.WarnMessage = DefaultDeprecationOptions.WarnMessage } - ctx.Header("X-API-Warn", options.WarnMessage) + ctx.Header(APIWarnHeader, options.WarnMessage) if !options.DeprecationDate.IsZero() { - ctx.Header("X-API-Deprecation-Date", context.FormatTime(ctx, options.DeprecationDate)) + ctx.Header(APIDeprecationDateHeader, context.FormatTime(ctx, options.DeprecationDate)) } if options.DeprecationInfo != "" { - ctx.Header("X-API-Deprecation-Info", options.DeprecationInfo) + ctx.Header(APIDeprecationInfoHeader, options.DeprecationInfo) } } diff --git a/versioning/group.go b/versioning/group.go index d74d96c1..2b67bf2e 100644 --- a/versioning/group.go +++ b/versioning/group.go @@ -5,6 +5,11 @@ import ( "github.com/kataras/iris/v12/core/router" ) +// Property to be defined inside the registered +// Party on NewGroup, useful for a party to know its (optional) version +// when the versioning feature is used. +const Property = "iris.party.version" + // 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. @@ -20,16 +25,24 @@ type Group struct { 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 ptr to 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. -// -// Example: _examples/routing/versioning +// Examples at: _examples/routing/versioning // Usage: -// api := versioning.NewGroup(Parent_Party, ">= 1, < 2") -// api.Get/Post/Put/Delete... +// app := iris.New() +// api := app.Party("/api") +// v1 := versioning.NewGroup(api, ">= 1, < 2") +// v1.Get/Post/Put/Delete... +// +// See the `GetVersion` function to learn how +// a version is extracted and matched over this. func NewGroup(r router.Party, version string) *Group { + r = r.Party("/") + r.Properties()[Property] = version + // Note that this feature alters the RouteRegisterRule to RouteOverlap // the RouteOverlap rule does not contain any performance downside // but it's good to know that if you registered other mode, this wanna change it. @@ -54,3 +67,21 @@ func (g *Group) Deprecated(options DeprecationOptions) *Group { }) return g } + +// FromQuery is a simple helper which tries to +// set the version constraint from a given URL Query Parameter. +// The X-Api-Version is still valid. +func FromQuery(urlQueryParameterName string, defaultVersion string) context.Handler { + return func(ctx *context.Context) { + version := ctx.URLParam(urlQueryParameterName) + if version == "" { + version = defaultVersion + } + + if version != "" { + SetVersion(ctx, version) + } + + ctx.Next() + } +} diff --git a/versioning/version.go b/versioning/version.go index 26be5541..94c4722d 100644 --- a/versioning/version.go +++ b/versioning/version.go @@ -8,28 +8,26 @@ import ( ) 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 @@ -107,7 +105,113 @@ 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") +// 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", // when no version was provided by the client. +// "beta": "4.0.0", +// "stage": "5.0.0-alpha" +// })) +// +// v1 := NewGroup(api, ">= 1, < 2") +// v1.Get/Post... +// +// v4 := NewGroup(api, ">= 4, < 5") +// 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() + } +} + +// 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 overriden or copied to the previous map one. +func SetVersionAliases(ctx *context.Context, aliases AliasMap, override bool) { + key := ctx.Application().ConfigurationReadOnly().GetVersionAliasesContextKey() + if key == "" { + return + } + + v := ctx.Values().Get(key) + if v == nil || override { + ctx.Values().Set(key, aliases) + return + } + + if existing, ok := v.(AliasMap); ok { + for k, v := range aliases { + existing[k] = v + } + } +} diff --git a/versioning/version_test.go b/versioning/version_test.go index eab10ea9..c5d2272b 100644 --- a/versioning/version_test.go +++ b/versioning/version_test.go @@ -46,3 +46,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", + "stage": "2", + })) + + writeVesion := func(ctx iris.Context) { + ctx.WriteString(versioning.GetVersion(ctx)) + } + + // A group without registration order. + v3 := versioning.NewGroup(api, ">= 3, < 4") + v3.Get("/", writeVesion) + + v1 := versioning.NewGroup(api, ">= 1, < 2") + v1.Get("/", writeVesion) + + v2 := versioning.NewGroup(api, ">= 2, < 3") + 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").Expect(). + Status(iris.StatusNotImplemented).Body().Equal("version not found") + // Test "stage" alias. + e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "stage").Expect(). + Status(iris.StatusOK).Body().Equal("2.0.0") + // Test version 2. + e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "2").Expect(). + Status(iris.StatusOK).Body().Equal("2.0.0") + // Test version 3 (registered first). + e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "3.1").Expect(). + Status(iris.StatusOK).Body().Equal("3.1.0") +} diff --git a/versioning/versioning.go b/versioning/versioning.go index 4041f304..04ef293c 100644 --- a/versioning/versioning.go +++ b/versioning/versioning.go @@ -14,6 +14,10 @@ func If(v string, is string) bool { } func check(v string, is string) (string, bool) { + if v == "" { + return "", false + } + ver, err := version.NewVersion(v) if err != nil { return "", false @@ -31,16 +35,30 @@ func check(v string, is string) (string, bool) { // 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 +// If matched then it sets the "X-Api-Version" response header and // stores the matched version into Context (see `GetVersion` too). +// +// See the `Aliases` function to register version constraint +// aliases for a versioning Party, extremely useful when a Group is used. func Match(ctx *context.Context, expectedVersion string) bool { - versionString, matched := check(GetVersion(ctx), expectedVersion) + gotVersion := GetVersion(ctx) + + alias, aliasFound := GetVersionAlias(ctx, gotVersion) + if aliasFound { + SetVersion(ctx, alias) // set the version so next routes have it already. + gotVersion = alias + } + + versionString, matched := check(gotVersion, expectedVersion) if !matched { return false } - SetVersion(ctx, versionString) - ctx.Header("X-API-Version", versionString) + if !aliasFound { // don't lose any time to set if already set. + SetVersion(ctx, versionString) + } + + ctx.Header(APIVersionResponseHeader, versionString) return true } @@ -77,6 +95,7 @@ func NewMatcher(versions Map) context.Handler { return func(ctx *context.Context) { versionString := GetVersion(ctx) + if versionString == "" || versionString == NotFound { notFoundHandler(ctx) return @@ -90,7 +109,7 @@ func NewMatcher(versions Map) context.Handler { for _, ch := range constraintsHandlers { if ch.constraints.Check(ver) { - ctx.Header("X-API-Version", ver.String()) + ctx.Header(APIVersionResponseHeader, ver.String()) ch.handler(ctx) return } From eb64006fcbc601598706ecfcd358fba00a6a54ec Mon Sep 17 00:00:00 2001 From: yanghuiwen Date: Wed, 6 Jan 2021 12:13:30 +0800 Subject: [PATCH 10/12] remove unnecessary code --- _examples/mvc/error-handler-preflight/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/_examples/mvc/error-handler-preflight/main.go b/_examples/mvc/error-handler-preflight/main.go index 1ffd8fb6..e25ab5e2 100644 --- a/_examples/mvc/error-handler-preflight/main.go +++ b/_examples/mvc/error-handler-preflight/main.go @@ -30,8 +30,8 @@ type response struct { } func (r *response) Preflight(ctx iris.Context) error { - if (*r).ID > 0 { - (*r).Timestamp = time.Now().Unix() + if r.ID > 0 { + r.Timestamp = time.Now().Unix() } if code := r.Code; code > 0 { @@ -46,7 +46,7 @@ func (r *response) Preflight(ctx iris.Context) error { Code: code, /* use any r.Data as the template data OR the whole "response" as its data. */ - Data: *r, + Data: r, /* automatically pick the template per error (just for the sake of the example) */ Name: fmt.Sprintf("%d", code), }.Dispatch(ctx) @@ -54,7 +54,7 @@ func (r *response) Preflight(ctx iris.Context) error { return iris.ErrStopExecution } - ctx.StatusCode((*r).Code) + ctx.StatusCode(r.Code) } return nil From 240fdb6dc3e877e2c442f1b730c21d577bd911f9 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 7 Jan 2021 04:14:41 +0200 Subject: [PATCH 11/12] API versioning improvements Replace the go-version package with a regex-free alternative semver the result: versioned apis have almost zero performance cost now thanks @motogo for your kind donation :heart: - please check your github notifications --- .gitignore | 1 + HISTORY.md | 7 +- NOTICE | 6 +- README.md | 1 + _examples/auth/jwt/basic/main.go | 9 +- _examples/mvc/versioned-controller/main.go | 8 +- .../mvc/versioned-controller/main_test.go | 8 +- _examples/routing/versioning/main.go | 153 ++++++++-------- _examples/routing/versioning/v1/index.html | 1 + _examples/routing/versioning/v2/index.html | 1 + _examples/routing/versioning/v3/index.html | 1 + configuration.go | 2 +- core/router/api_builder.go | 4 +- go.mod | 2 +- mvc/versioning.go | 8 +- versioning/group.go | 108 ++++++++--- versioning/group_test.go | 60 ++++++ versioning/version.go | 111 ++++++++++- versioning/version_test.go | 45 +++-- versioning/versioning.go | 173 ------------------ versioning/versioning_test.go | 135 -------------- 21 files changed, 381 insertions(+), 463 deletions(-) create mode 100644 _examples/routing/versioning/v1/index.html create mode 100644 _examples/routing/versioning/v2/index.html create mode 100644 _examples/routing/versioning/v3/index.html create mode 100644 versioning/group_test.go delete mode 100644 versioning/versioning.go delete mode 100644 versioning/versioning_test.go diff --git a/.gitignore b/.gitignore index 37739136..8ddb0de7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ coverage.out package-lock.json go.sum +access.log node_modules issue-*/ internalcode-*/ diff --git a/HISTORY.md b/HISTORY.md index dd6f5a25..3a8fc9c4 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,7 +28,7 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements -- New `versioning.Aliases` middleware. Example Code: +- New `versioning.Aliases` middleware and up to 80% faster version resolve. Example Code: ```go app := iris.New() @@ -40,10 +40,10 @@ api.Use(Aliases(map[string]string{ "stage": "5.0.0-alpha" })) -v1 := NewGroup(api, ">= 1, < 2") +v1 := NewGroup(api, ">=1.0.0 <2.0.0") v1.Get/Post... -v4 := NewGroup(api, ">= 4, < 5") +v4 := NewGroup(api, ">=4.0.0 <5.0.0") v4.Get/Post... stage := NewGroup(api, "5.0.0-alpha") @@ -723,6 +723,7 @@ Response: ## Breaking Changes +- Strict versions format on `versioning.NewGroup` is required. E.g. `"1"` is not valid anymore, you have to specify `"1.0.0"`. Example: `NewGroup(api, ">=1.0.0 <2.0.0")`. The [routing/versioning](_examples/routing/versioning) examples have been updated. - Now that `RegisterView` can be used to register different view engines per-Party, there is no need to support registering multiple engines under the same Party. The `app.RegisterView` now upserts the given Engine instead of append. You can now render templates **without file extension**, e.g. `index` instead of `index.ace`, both forms are valid now. - The `Context.ContentType` does not accept filenames to resolve the mime type anymore (caused issues with vendor-specific(vnd) MIME types). - The `Configuration.RemoteAddrPrivateSubnets.IPRange.Start and End` are now type of `string` instead of `net.IP`. The `WithRemoteAddrPrivateSubnet` option remains as it is, already accepts `string`s. diff --git a/NOTICE b/NOTICE index f5f25113..aaf04975 100644 --- a/NOTICE +++ b/NOTICE @@ -44,9 +44,9 @@ Revision ID: 5fc50a00491616d5cd0cbce3abd8b699838e25ca easyjson 8ab5ff9cd8e4e43 https://github.com/mailru/easyjson 2e8b79f6c47d324 a31dd803cf - go-version 2b13044f5cdd383 https://github.com/hashicorp/go-version - 3370d41ce57d8bf - 3cec5e62b8 + semver 4487282d78122a2 https://github.com/blang/semver + 45e413d7515e7c5 + 16b70c33fd golog f7561df84e64ab9 https://github.com/kataras/golog 212f021923ce4ff db5df5594d diff --git a/README.md b/README.md index 39c5cae0..f13cbefe 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ With your help, we can improve Open Source web development for everyone! > Donations from **China** are now accepted!

+ Horst Ender Pavithran MULYAWAN SENTOSA KIT UNITED diff --git a/_examples/auth/jwt/basic/main.go b/_examples/auth/jwt/basic/main.go index f37ce681..ddcb9a58 100644 --- a/_examples/auth/jwt/basic/main.go +++ b/_examples/auth/jwt/basic/main.go @@ -4,16 +4,10 @@ import ( "time" "github.com/kataras/iris/v12" - "github.com/kataras/jwt" + "github.com/kataras/iris/v12/middleware/jwt" ) /* -Learn how to use any JWT 3rd-party package with Iris. -In this example we use the kataras/jwt one. - -Install with: - go get -u github.com/kataras/jwt - Documentation: https://github.com/kataras/jwt#table-of-contents */ @@ -71,6 +65,7 @@ func protected(ctx iris.Context) { // Just an example on how you can retrieve all the standard claims (set by jwt.MaxAge, "exp"). standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims + expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) timeLeft := standardClaims.Timeleft() diff --git a/_examples/mvc/versioned-controller/main.go b/_examples/mvc/versioned-controller/main.go index 9c8bb950..56cedb47 100644 --- a/_examples/mvc/versioned-controller/main.go +++ b/_examples/mvc/versioned-controller/main.go @@ -28,10 +28,10 @@ func newApp() *iris.Application { { m := mvc.New(dataRouter) - m.Handle(new(v1Controller), mvc.Version("1"), mvc.Deprecated(opts)) // 1 or 1.0, 1.0.0 ... - m.Handle(new(v2Controller), mvc.Version("2.3")) // 2.3 or 2.3.0 - m.Handle(new(v3Controller), mvc.Version(">=3, <4")) // 3, 3.x, 3.x.x ... - m.Handle(new(noVersionController)) // or if missing it will respond with 501 version not found. + m.Handle(new(v1Controller), mvc.Version("1.0.0"), mvc.Deprecated(opts)) + m.Handle(new(v2Controller), mvc.Version("2.3.0")) + m.Handle(new(v3Controller), mvc.Version(">=3.0.0 <4.0.0")) + m.Handle(new(noVersionController)) // or if missing it will respond with 501 version not found. } return app diff --git a/_examples/mvc/versioned-controller/main_test.go b/_examples/mvc/versioned-controller/main_test.go index 309a14f1..33cedd42 100644 --- a/_examples/mvc/versioned-controller/main_test.go +++ b/_examples/mvc/versioned-controller/main_test.go @@ -12,21 +12,21 @@ func TestVersionedController(t *testing.T) { app := newApp() e := httptest.New(t, app) - e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect(). + e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect(). Status(iris.StatusOK).Body().Equal("data (v1.x)") e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "2.3.0").Expect(). Status(iris.StatusOK).Body().Equal("data (v2.x)") - e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "3.1").Expect(). + e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "3.1.0").Expect(). Status(iris.StatusOK).Body().Equal("data (v3.x)") // Test invalid version or no version at all. - e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "4").Expect(). + e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "4.0.0").Expect(). Status(iris.StatusOK).Body().Equal("data") e.GET("/data").Expect(). Status(iris.StatusOK).Body().Equal("data") // Test Deprecated (v1) - ex := e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect() + ex := e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect() ex.Status(iris.StatusOK).Body().Equal("data (v1.x)") ex.Header("X-API-Warn").Equal(opts.WarnMessage) expectedDateStr := opts.DeprecationDate.Format(app.ConfigurationReadOnly().GetTimeFormat()) diff --git a/_examples/routing/versioning/main.go b/_examples/routing/versioning/main.go index 1e621f33..221fcf5f 100644 --- a/_examples/routing/versioning/main.go +++ b/_examples/routing/versioning/main.go @@ -8,84 +8,93 @@ import ( func main() { app := iris.New() - examplePerRoute(app) - examplePerParty(app) + app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { + ctx.WriteString(`Root not found handler. + This will be applied everywhere except the /api/* requests.`) + }) + + api := app.Party("/api") + // Optional, set version aliases (literal strings). + // We use `UseRouter` instead of `Use` + // to handle HTTP errors per version, but it's up to you. + api.UseRouter(versioning.Aliases(versioning.AliasMap{ + // If no version provided by the client, default it to the "1.0.0". + versioning.Empty: "1.0.0", + // If a "latest" version is provided by the client, + // set the version to be compared to "3.0.0". + "latest": "3.0.0", + })) + + /* + A version is extracted through the versioning.GetVersion function, + request headers: + - Accept-Version: 1.0.0 + - Accept: application/json; version=1.0.0 + You can customize it by setting a version based on the request context: + api.Use(func(ctx *context.Context) { + if version := ctx.URLParam("version"); version != "" { + SetVersion(ctx, version) + } + + ctx.Next() + }) + OR: api.Use(versioning.FromQuery("version", "")) + */ + + // |----------------| + // | The fun begins | + // |----------------| + + // Create a new Group, which is a compatible Party, + // based on version constraints. + v1 := versioning.NewGroup(api, ">=1.0.0 <2.0.0") + + // Optionally, set custom view engine and path + // for templates based on the version. + v1.RegisterView(iris.HTML("./v1", ".html")) + + // Optionally, set custom error handler(s) based on the version. + // Keep in mind that if you do this, you will + // have to register error handlers + // for the rest of the parties as well. + v1.OnErrorCode(iris.StatusNotFound, testError("v1")) + + // Register resources based on the version. + v1.Get("/", testHandler("v1")) + v1.Get("/render", testView) + + // Do the same for version 2 and version 3, + // for the sake of the example. + v2 := versioning.NewGroup(api, ">=2.0.0 <3.0.0") + v2.RegisterView(iris.HTML("./v2", ".html")) + v2.OnErrorCode(iris.StatusNotFound, testError("v2")) + v2.Get("/", testHandler("v2")) + v2.Get("/render", testView) + + v3 := versioning.NewGroup(api, ">=3.0.0 <4.0.0") + v3.RegisterView(iris.HTML("./v3", ".html")) + v3.OnErrorCode(iris.StatusNotFound, testError("v3")) + v3.Get("/", testHandler("v3")) + v3.Get("/render", testView) - // Read the README.md before any action. app.Listen(":8080") } -// How to test: -// Open Postman -// GET: localhost:8080/api/cats -// Headers[1] = Accept-Version: "1" and repeat with -// Headers[1] = Accept-Version: "2.5" -// or even "Accept": "application/json; version=2.5" -func examplePerRoute(app *iris.Application) { - app.Get("/api/cats", versioning.NewMatcher(versioning.Map{ - "1": catsVersionExactly1Handler, - ">= 2, < 3": catsV2Handler, - versioning.NotFound: versioning.NotFoundHandler, - })) +func testHandler(v string) iris.Handler { + return func(ctx iris.Context) { + ctx.JSON(iris.Map{ + "version": v, + "message": "Hello, world!", + }) + } } -// How to test: -// Open Postman -// GET: localhost:8080/api/users -// Headers[1] = Accept-Version: "1.9.9" and repeat with -// Headers[1] = Accept-Version: "2.5" -// -// POST: localhost:8080/api/users/new -// Headers[1] = Accept-Version: "1.8.3" -// -// POST: localhost:8080/api/users -// Headers[1] = Accept-Version: "2" -func examplePerParty(app *iris.Application) { - usersAPI := app.Party("/api/users") - // You can customize the way a version is extracting - // via middleware, for example: - // version url parameter, and, if it's missing we default it to "1". - // usersAPI.Use(func(ctx iris.Context) { - // versioning.SetVersion(ctx, ctx.URLParamDefault("version", "1")) - // ctx.Next() - // }) - // OR: - usersAPI.Use(versioning.FromQuery("version", "1")) - - // version 1. - usersAPIV1 := versioning.NewGroup(usersAPI, ">= 1, < 2") - usersAPIV1.Get("/", func(ctx iris.Context) { - ctx.Writef("v1 resource: /api/users handler") - }) - usersAPIV1.Post("/new", func(ctx iris.Context) { - ctx.Writef("v1 resource: /api/users/new post handler") - }) - - // version 2. - usersAPIV2 := versioning.NewGroup(usersAPI, ">= 2, < 3") - usersAPIV2.Get("/", func(ctx iris.Context) { - ctx.Writef("v2 resource: /api/users handler") - }) - usersAPIV2.Post("/", func(ctx iris.Context) { - ctx.Writef("v2 resource: /api/users post handler") - }) - - // version 3, pass it as a common iris.Party. - usersAPIV3 := versioning.NewGroup(usersAPI, ">= 3, < 4") - registerAPIV3(usersAPIV3) +func testError(v string) iris.Handler { + return func(ctx iris.Context) { + ctx.Writef("not found: %s", v) + } } -func catsVersionExactly1Handler(ctx iris.Context) { - ctx.Writef("v1 exactly resource: /api/cats handler") -} - -func catsV2Handler(ctx iris.Context) { - ctx.Writef("v2 resource: /api/cats handler") -} - -func registerAPIV3(p iris.Party) { - p.Get("/", func(ctx iris.Context) { - ctx.Writef("v3 resource: /api/users handler") - }) - // [...] +func testView(ctx iris.Context) { + ctx.View("index.html") } diff --git a/_examples/routing/versioning/v1/index.html b/_examples/routing/versioning/v1/index.html new file mode 100644 index 00000000..efe99490 --- /dev/null +++ b/_examples/routing/versioning/v1/index.html @@ -0,0 +1 @@ +

This is the directory for version 1 templates

\ No newline at end of file diff --git a/_examples/routing/versioning/v2/index.html b/_examples/routing/versioning/v2/index.html new file mode 100644 index 00000000..802d7081 --- /dev/null +++ b/_examples/routing/versioning/v2/index.html @@ -0,0 +1 @@ +

This is the directory for version 2 templates

\ No newline at end of file diff --git a/_examples/routing/versioning/v3/index.html b/_examples/routing/versioning/v3/index.html new file mode 100644 index 00000000..b1e3fdc7 --- /dev/null +++ b/_examples/routing/versioning/v3/index.html @@ -0,0 +1 @@ +

This is the directory for version 3 templates

\ No newline at end of file diff --git a/configuration.go b/configuration.go index 7d76de91..7e96aac9 100644 --- a/configuration.go +++ b/configuration.go @@ -764,7 +764,7 @@ type Configuration struct { // Defaults to "iris.locale.language.input". LanguageInputContextKey string `ini:"language_input_context_key" json:"languageInputContextKey,omitempty" yaml:"LanguageInputContextKey" toml:"LanguageInputContextKey"` // VersionContextKey is the context key which an API Version can be modified - // via a middleware through `SetVersion` method, e.g. `versioning.SetVersion(ctx, "1.0, 1.1")`. + // via a middleware through `SetVersion` method, e.g. `versioning.SetVersion(ctx, ">=1.0.0 <2.0.0")`. // Defaults to "iris.api.version". VersionContextKey string `ini:"version_context_key" json:"versionContextKey" yaml:"VersionContextKey" toml:"VersionContextKey"` // VersionAliasesContextKey is the context key which the versioning feature diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 9b4ccac6..4da5024d 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -164,7 +164,7 @@ func overlapRoute(r *Route, next *Route) { // Version was not found: // we need to be able to send the status on the last not found version // but reset the status code if a next available matched version was found. - // see: versioning.Handler. + // see the versioning package. if !errors.Is(ctx.GetErr(), context.ErrNotFound) { ctx.StatusCode(prevStatusCode) } @@ -720,7 +720,7 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P copy(allowMethods, api.allowMethods) // make a copy of the parent properties. - var properties map[string]interface{} + var properties context.Map for k, v := range api.properties { properties[k] = v } diff --git a/go.mod b/go.mod index 6d07aa51..9b1ffea7 100644 --- a/go.mod +++ b/go.mod @@ -8,13 +8,13 @@ require ( github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 github.com/andybalholm/brotli v1.0.1 github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible + github.com/blang/semver/v4 v4.0.0 github.com/dgraph-io/badger/v2 v2.2007.2 github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 github.com/fatih/structs v1.1.0 github.com/flosch/pongo2/v4 v4.0.1 github.com/go-redis/redis/v8 v8.4.0 github.com/google/uuid v1.1.2 - github.com/hashicorp/go-version v1.2.1 github.com/iris-contrib/httpexpect/v2 v2.0.5 github.com/iris-contrib/jade v1.1.4 github.com/iris-contrib/schema v0.0.6 diff --git a/mvc/versioning.go b/mvc/versioning.go index c16c2889..fcaf3078 100644 --- a/mvc/versioning.go +++ b/mvc/versioning.go @@ -8,14 +8,14 @@ import ( // Version returns a valid `Option` that can be passed to the `Application.Handle` method. // It requires a specific "version" constraint for a Controller, -// e.g. ">1, <=2" or just "1". +// e.g. ">1.0.0 <=2.0.0". // // // Usage: // m := mvc.New(dataRouter) -// m.Handle(new(v1Controller), mvc.Version("1"), mvc.Deprecated(mvc.DeprecationOptions{})) -// m.Handle(new(v2Controller), mvc.Version("2.3")) -// m.Handle(new(v3Controller), mvc.Version(">=3, <4")) +// m.Handle(new(v1Controller), mvc.Version("1.0.0"), mvc.Deprecated(mvc.DeprecationOptions{})) +// m.Handle(new(v2Controller), mvc.Version("2.3.0")) +// m.Handle(new(v3Controller), mvc.Version(">=3.0.0 <4.0.0")) // m.Handle(new(noVersionController)) // // See the `versioning` package's documentation for more information on diff --git a/versioning/group.go b/versioning/group.go index 2b67bf2e..10390133 100644 --- a/versioning/group.go +++ b/versioning/group.go @@ -1,57 +1,107 @@ package versioning import ( + "strings" + "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/router" -) -// Property to be defined inside the registered -// Party on NewGroup, useful for a party to know its (optional) version -// when the versioning feature is used. -const Property = "iris.party.version" + "github.com/blang/semver/v4" +) // API is a type alias of router.Party. // This is required in order for a Group instance // to implement the Party interface without field conflict. type API = router.Party -// Group is a group of version-based routes. -// One version per one or more routes. +// Group represents a group of resources that should +// be handled based on a version requested by the client. +// See `NewGroup` for more. type Group struct { API - // Information not currently in-use. - version string + validate semver.Range deprecation DeprecationOptions } -// NewGroup returns a ptr to Group based on the given "version" constraint. +// NewGroup returns a version Group based on the given "version" constraint. // Group completes the Party interface. // The returned Group wraps a cloned Party of the given "r" Party therefore, // any changes to its parent won't affect this one (e.g. register global middlewares afterwards). // +// A version is extracted through the versioning.GetVersion function: +// Accept-Version: 1.0.0 +// Accept: application/json; version=1.0.0 +// You can customize it by setting a version based on the request context: +// api.Use(func(ctx *context.Context) { +// if version := ctx.URLParam("version"); version != "" { +// SetVersion(ctx, version) +// } +// +// ctx.Next() +// }) +// OR: +// api.Use(versioning.FromQuery("version", "")) +// // Examples at: _examples/routing/versioning // Usage: // app := iris.New() // api := app.Party("/api") -// v1 := versioning.NewGroup(api, ">= 1, < 2") +// v1 := versioning.NewGroup(api, ">=1.0.0 <2.0.0") // v1.Get/Post/Put/Delete... // -// See the `GetVersion` function to learn how -// a version is extracted and matched over this. -func NewGroup(r router.Party, version string) *Group { +// Valid ranges are: +// - "<1.0.0" +// - "<=1.0.0" +// - ">1.0.0" +// - ">=1.0.0" +// - "1.0.0", "=1.0.0", "==1.0.0" +// - "!1.0.0", "!=1.0.0" +// +// A Range can consist of multiple ranges separated by space: +// Ranges can be linked by logical AND: +// - ">1.0.0 <2.0.0" would match between both ranges, so "1.1.1" and "1.8.7" +// but not "1.0.0" or "2.0.0" +// - ">1.0.0 <3.0.0 !2.0.3-beta.2" would match every version between 1.0.0 and 3.0.0 +// except 2.0.3-beta.2 +// +// Ranges can also be linked by logical OR: +// - "<2.0.0 || >=3.0.0" would match "1.x.x" and "3.x.x" but not "2.x.x" +// +// AND has a higher precedence than OR. It's not possible to use brackets. +// +// Ranges can be combined by both AND and OR +// +// - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`, +// but not `4.2.1`, `2.1.1` +func NewGroup(r API, version string) *Group { + version = strings.ReplaceAll(version, ",", " ") + version = strings.TrimSpace(version) + + verRange, err := semver.ParseRange(version) + if err != nil { + r.Logger().Errorf("versioning: %s: %s", r.GetRelPath(), strings.ToLower(err.Error())) + return &Group{API: r} + } + + // Clone this one. r = r.Party("/") - r.Properties()[Property] = version // Note that this feature alters the RouteRegisterRule to RouteOverlap // the RouteOverlap rule does not contain any performance downside // but it's good to know that if you registered other mode, this wanna change it. r.SetRegisterRule(router.RouteOverlap) - r.UseOnce(Handler(version)) // this is required in order to not populate this middleware to the next group. + + handler := makeHandler(verRange) + // This is required in order to not populate this middleware to the next group. + r.UseOnce(handler) + // This is required for versioned custom error handlers, + // of course if the parent registered one then this will do nothing. + r.UseError(handler) return &Group{ - API: r, - version: version, + API: r, + validate: verRange, } } @@ -68,18 +118,18 @@ func (g *Group) Deprecated(options DeprecationOptions) *Group { return g } -// FromQuery is a simple helper which tries to -// set the version constraint from a given URL Query Parameter. -// The X-Api-Version is still valid. -func FromQuery(urlQueryParameterName string, defaultVersion string) context.Handler { +func makeHandler(validate semver.Range) context.Handler { return func(ctx *context.Context) { - version := ctx.URLParam(urlQueryParameterName) - if version == "" { - version = defaultVersion - } - - if version != "" { - SetVersion(ctx, version) + if !matchVersionRange(ctx, validate) { + // The overlapped handler has an exception + // of a type of context.NotFound (which versioning.ErrNotFound wraps) + // to clear the status code + // and the error to ignore this + // when available match version exists (see `NewGroup`). + if h := NotFoundHandler; h != nil { + h(ctx) + return + } } ctx.Next() diff --git a/versioning/group_test.go b/versioning/group_test.go new file mode 100644 index 00000000..ee8726c2 --- /dev/null +++ b/versioning/group_test.go @@ -0,0 +1,60 @@ +package versioning_test + +import ( + "testing" + + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/httptest" + "github.com/kataras/iris/v12/versioning" +) + +func notFoundHandler(ctx iris.Context) { + ctx.NotFound() +} + +const ( + v10Response = "v1.0 handler" + v2Response = "v2.x handler" +) + +func sendHandler(contents string) iris.Handler { + return func(ctx iris.Context) { + ctx.WriteString(contents) + } +} + +func TestNewGroup(t *testing.T) { + app := iris.New() + + userAPI := app.Party("/api/user") + // [... static serving, middlewares and etc goes here]. + + userAPIV10 := versioning.NewGroup(userAPI, "1.0.0").Deprecated(versioning.DefaultDeprecationOptions) + + userAPIV10.Get("/", sendHandler(v10Response)) + userAPIV2 := versioning.NewGroup(userAPI, ">= 2.0.0 < 3.0.0") + + userAPIV2.Get("/", sendHandler(v2Response)) + userAPIV2.Post("/", sendHandler(v2Response)) + userAPIV2.Put("/other", sendHandler(v2Response)) + + e := httptest.New(t, app) + + ex := e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect() + ex.Status(iris.StatusOK).Body().Equal(v10Response) + ex.Header("X-API-Warn").Equal(versioning.DefaultDeprecationOptions.WarnMessage) + + e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0.0").Expect(). + Status(iris.StatusOK).Body().Equal(v2Response) + e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1.0").Expect(). + Status(iris.StatusOK).Body().Equal(v2Response) + e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect(). + Status(iris.StatusOK).Body().Equal(v2Response) + e.POST("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0.0").Expect(). + Status(iris.StatusOK).Body().Equal(v2Response) + e.PUT("/api/user/other").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.0").Expect(). + Status(iris.StatusOK).Body().Equal(v2Response) + + e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect(). + Status(iris.StatusNotImplemented).Body().Equal("version not found") +} diff --git a/versioning/version.go b/versioning/version.go index 94c4722d..a8084e4e 100644 --- a/versioning/version.go +++ b/versioning/version.go @@ -5,6 +5,8 @@ import ( "strings" "github.com/kataras/iris/v12/context" + + "github.com/blang/semver/v4" ) const ( @@ -52,11 +54,89 @@ var NotFoundHandler = func(ctx *context.Context) { ctx.StopWithPlainError(501, ErrNotFound) } +// FromQuery is a simple helper which tries to +// set the version constraint from a given URL Query Parameter. +// The X-Api-Version is still valid. +func FromQuery(urlQueryParameterName string, defaultVersion string) context.Handler { + return func(ctx *context.Context) { + version := ctx.URLParam(urlQueryParameterName) + if version == "" { + version = defaultVersion + } + + if version != "" { + SetVersion(ctx, version) + } + + ctx.Next() + } +} + +// If reports whether the "got" matches the "expected" one. +// the "expected" can be a constraint like ">=1.0.0 <2.0.0". +// This function is just a helper, better use the Group instead. +func If(got string, expected string) bool { + v, err := semver.Make(got) + if err != nil { + return false + } + + validate, err := semver.ParseRange(expected) + if err != nil { + return false + } + + return validate(v) +} + +// Match reports whether the request matches the expected version. +// This function is just a helper, better use the Group instead. +func Match(ctx *context.Context, expectedVersion string) bool { + validate, err := semver.ParseRange(expectedVersion) + if err != nil { + return false + } + + return matchVersionRange(ctx, validate) +} + +func matchVersionRange(ctx *context.Context, validate semver.Range) bool { + gotVersion := GetVersion(ctx) + + alias, aliasFound := GetVersionAlias(ctx, gotVersion) + if aliasFound { + SetVersion(ctx, alias) // set the version so next routes have it already. + gotVersion = alias + } + + if gotVersion == "" { + return false + } + + v, err := semver.Make(gotVersion) + if err != nil { + return false + } + + if !validate(v) { + return false + } + + versionString := v.String() + + if !aliasFound { // don't lose any time to set if already set. + SetVersion(ctx, versionString) + } + + ctx.Header(APIVersionResponseHeader, versionString) + return true +} + // GetVersion returns the current request version. // // By default the `GetVersion` will try to read from: -// - "Accept" header, i.e Accept: "application/json; version=1.0" -// - "Accept-Version" header, i.e Accept-Version: "1.0" +// - "Accept" header, i.e Accept: "application/json; version=1.0.0" +// - "Accept-Version" header, i.e Accept-Version: "1.0.0" // // However, the end developer can also set a custom version for a handler via a middleware by using the context's store key // for versions (see `Key` for further details on that). @@ -108,7 +188,7 @@ func GetVersion(ctx *context.Context) string { // Example of how you can change the default behavior to extract a requested version (which is by headers) // from a "version" url parameter instead: // func(ctx iris.Context) { // &version=1 -// version := ctx.URLParamDefault("version", "1") +// version := ctx.URLParamDefault("version", "1.0.0") // versioning.SetVersion(ctx, version) // ctx.Next() // } @@ -129,15 +209,15 @@ type AliasMap = map[string]string // // api := app.Party("/api") // api.Use(Aliases(map[string]string{ -// versioning.Empty: "1", // when no version was provided by the client. +// versioning.Empty: "1.0.0", // when no version was provided by the client. // "beta": "4.0.0", // "stage": "5.0.0-alpha" // })) // -// v1 := NewGroup(api, ">= 1, < 2") +// v1 := NewGroup(api, ">=1.0.0 < 2.0.0") // v1.Get/Post... // -// v4 := NewGroup(api, ">= 4, < 5") +// v4 := NewGroup(api, ">=4.0.0 < 5.0.0") // v4.Get/Post... // // stage := NewGroup(api, "5.0.0-alpha") @@ -154,6 +234,23 @@ func Aliases(aliases AliasMap) context.Handler { } } +// Handler returns a handler which is only fired +// when the "version" is matched with the requested one. +// It is not meant to be used by end-developers +// (exported for version controller feature). +// Use `NewGroup` instead. +func Handler(version string) context.Handler { + validate, err := semver.ParseRange(version) + if err != nil { + return func(ctx *context.Context) { + ctx.StopWithError(500, err) + return + } + } + + return makeHandler(validate) +} + // GetVersionAlias returns the version alias of the given "gotVersion" // or empty. It Reports whether the alias was found. // See `SetVersionAliases`, `Aliases` and `Match` for more. @@ -196,7 +293,7 @@ func GetVersionAlias(ctx *context.Context, gotVersion string) (string, bool) { // // The last "override" input argument indicates whether any // existing aliases, registered by previous handlers in the chain, -// should be overriden or copied to the previous map one. +// should be overridden or copied to the previous map one. func SetVersionAliases(ctx *context.Context, aliases AliasMap, override bool) { key := ctx.Application().ConfigurationReadOnly().GetVersionAliasesContextKey() if key == "" { diff --git a/versioning/version_test.go b/versioning/version_test.go index c5d2272b..f403fae2 100644 --- a/versioning/version_test.go +++ b/versioning/version_test.go @@ -8,6 +8,15 @@ import ( "github.com/kataras/iris/v12/versioning" ) +func TestIf(t *testing.T) { + if expected, got := true, versioning.If("1.0.0", ">=1.0.0"); expected != got { + t.Fatalf("expected %s to be %s", "1.0.0", ">= 1.0.0") + } + if expected, got := true, versioning.If("1.2.3", "> 1.2.0"); expected != got { + t.Fatalf("expected %s to be %s", "1.2.3", "> 1.2.0") + } +} + func TestGetVersion(t *testing.T) { app := iris.New() @@ -23,16 +32,16 @@ func TestGetVersion(t *testing.T) { e := httptest.New(t, app) - e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect(). - Status(iris.StatusOK).Body().Equal("1.0") - e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1").Expect(). - Status(iris.StatusOK).Body().Equal("2.1") - e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1 ;other=dsa").Expect(). - Status(iris.StatusOK).Body().Equal("2.1") - e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=2.1").Expect(). - Status(iris.StatusOK).Body().Equal("2.1") - e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=1").Expect(). - Status(iris.StatusOK).Body().Equal("1") + e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect(). + Status(iris.StatusOK).Body().Equal("1.0.0") + e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1.0").Expect(). + Status(iris.StatusOK).Body().Equal("2.1.0") + e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1.0 ;other=dsa").Expect(). + Status(iris.StatusOK).Body().Equal("2.1.0") + e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=2.1.0").Expect(). + Status(iris.StatusOK).Body().Equal("2.1.0") + e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=1.0.0").Expect(). + Status(iris.StatusOK).Body().Equal("1.0.0") // unknown versions. e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "").Expect(). @@ -52,8 +61,8 @@ func TestVersionAliases(t *testing.T) { api := app.Party("/api") api.Use(versioning.Aliases(map[string]string{ - versioning.Empty: "1", - "stage": "2", + versioning.Empty: "1.0.0", + "stage": "2.0.0", })) writeVesion := func(ctx iris.Context) { @@ -61,13 +70,13 @@ func TestVersionAliases(t *testing.T) { } // A group without registration order. - v3 := versioning.NewGroup(api, ">= 3, < 4") + v3 := versioning.NewGroup(api, ">= 3.0.0 < 4.0.0") v3.Get("/", writeVesion) - v1 := versioning.NewGroup(api, ">= 1, < 2") + v1 := versioning.NewGroup(api, ">= 1.0.0 < 2.0.0") v1.Get("/", writeVesion) - v2 := versioning.NewGroup(api, ">= 2, < 3") + v2 := versioning.NewGroup(api, ">= 2.0.0 < 3.0.0") v2.Get("/", writeVesion) api.Get("/manual", func(ctx iris.Context) { @@ -84,15 +93,15 @@ func TestVersionAliases(t *testing.T) { e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "").Expect(). Status(iris.StatusOK).Body().Equal("1.0.0") // Test NotFound error, aliases are not responsible for that. - e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "4").Expect(). + e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "4.0.0").Expect(). Status(iris.StatusNotImplemented).Body().Equal("version not found") // Test "stage" alias. e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "stage").Expect(). Status(iris.StatusOK).Body().Equal("2.0.0") // Test version 2. - e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "2").Expect(). + e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "2.0.0").Expect(). Status(iris.StatusOK).Body().Equal("2.0.0") // Test version 3 (registered first). - e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "3.1").Expect(). + e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "3.1.0").Expect(). Status(iris.StatusOK).Body().Equal("3.1.0") } diff --git a/versioning/versioning.go b/versioning/versioning.go deleted file mode 100644 index 04ef293c..00000000 --- a/versioning/versioning.go +++ /dev/null @@ -1,173 +0,0 @@ -package versioning - -import ( - "github.com/kataras/iris/v12/context" - - "github.com/hashicorp/go-version" -) - -// If reports whether the "version" is matching to the "is". -// the "is" can be a constraint like ">= 1, < 3". -func If(v string, is string) bool { - _, ok := check(v, is) - return ok -} - -func check(v string, is string) (string, bool) { - if v == "" { - return "", false - } - - ver, err := version.NewVersion(v) - if err != nil { - return "", false - } - - constraints, err := version.NewConstraint(is) - if err != nil { - return "", false - } - - // return the extracted version from request, even if not matched. - return ver.String(), constraints.Check(ver) -} - -// Match acts exactly the same as `If` does but instead it accepts -// a Context, so it can be called by a handler to determinate the requested version. -// -// If matched then it sets the "X-Api-Version" response header and -// stores the matched version into Context (see `GetVersion` too). -// -// See the `Aliases` function to register version constraint -// aliases for a versioning Party, extremely useful when a Group is used. -func Match(ctx *context.Context, expectedVersion string) bool { - gotVersion := GetVersion(ctx) - - alias, aliasFound := GetVersionAlias(ctx, gotVersion) - if aliasFound { - SetVersion(ctx, alias) // set the version so next routes have it already. - gotVersion = alias - } - - versionString, matched := check(gotVersion, expectedVersion) - if !matched { - return false - } - - if !aliasFound { // don't lose any time to set if already set. - SetVersion(ctx, versionString) - } - - ctx.Header(APIVersionResponseHeader, versionString) - return true -} - -// Handler returns a handler which stop the execution -// when the given "version" does not match with the requested one. -func Handler(version string) context.Handler { - return func(ctx *context.Context) { - if !Match(ctx, version) { - // The overlapped handler has an exception - // of a type of context.NotFound (which versioning.ErrNotFound wraps) - // to clear the status code - // and the error to ignore this - // when available match version exists (see `NewGroup`). - NotFoundHandler(ctx) - return - } - - ctx.Next() - } -} - -// Map is a map of versions targets to a handlers, -// a handler per version or constraint, the key can be something like ">1, <=2" or just "1". -type Map map[string]context.Handler - -// NewMatcher creates a single handler which decides what handler -// should be executed based on the requested version. -// -// Use the `NewGroup` if you want to add many routes under a specific version. -// -// See `Map` and `NewGroup` too. -func NewMatcher(versions Map) context.Handler { - constraintsHandlers, notFoundHandler := buildConstraints(versions) - - return func(ctx *context.Context) { - versionString := GetVersion(ctx) - - if versionString == "" || versionString == NotFound { - notFoundHandler(ctx) - return - } - - ver, err := version.NewVersion(versionString) - if err != nil { - notFoundHandler(ctx) - return - } - - for _, ch := range constraintsHandlers { - if ch.constraints.Check(ver) { - ctx.Header(APIVersionResponseHeader, ver.String()) - ch.handler(ctx) - return - } - } - - // pass the not matched version so the not found handler can have knowedge about it. - // SetVersion(ctx, versionString) - // or let a manual cal of GetVersion(ctx) do that instead. - notFoundHandler(ctx) - } -} - -type constraintsHandler struct { - constraints version.Constraints - handler context.Handler -} - -func buildConstraints(versionsHandler Map) (constraintsHandlers []*constraintsHandler, notfoundHandler context.Handler) { - for v, h := range versionsHandler { - if v == NotFound { - notfoundHandler = h - continue - } - - constraints, err := version.NewConstraint(v) - if err != nil { - panic(err) - } - - constraintsHandlers = append(constraintsHandlers, &constraintsHandler{ - constraints: constraints, - handler: h, - }) - } - - if notfoundHandler == nil { - notfoundHandler = NotFoundHandler - } - - // no sort, the end-dev should declare - // all version constraint, i.e < 4.0 may be catch 1.0 if not something like - // >= 3.0, < 4.0. - // I can make it ordered but I do NOT like the final API of it: - /* - app.Get("/api/user", NewMatcher( // accepts an array, ordered, see last elem. - V("1.0", vHandler("v1 here")), - V("2.0", vHandler("v2 here")), - V("< 4.0", vHandler("v3.x here")), - )) - instead we have: - - app.Get("/api/user", NewMatcher(Map{ // accepts a map, unordered, see last elem. - "1.0": Deprecated(vHandler("v1 here")), - "2.0": vHandler("v2 here"), - ">= 3.0, < 4.0": vHandler("v3.x here"), - VersionUnknown: customHandlerForNotMatchingVersion, - })) - */ - - return -} diff --git a/versioning/versioning_test.go b/versioning/versioning_test.go deleted file mode 100644 index 536b39bc..00000000 --- a/versioning/versioning_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package versioning_test - -import ( - "testing" - - "github.com/kataras/iris/v12" - "github.com/kataras/iris/v12/httptest" - "github.com/kataras/iris/v12/versioning" -) - -func notFoundHandler(ctx iris.Context) { - ctx.NotFound() -} - -const ( - v10Response = "v1.0 handler" - v2Response = "v2.x handler" -) - -func sendHandler(contents string) iris.Handler { - return func(ctx iris.Context) { - ctx.WriteString(contents) - } -} - -func TestIf(t *testing.T) { - if expected, got := true, versioning.If("1.0", ">=1"); expected != got { - t.Fatalf("expected %s to be %s", "1.0", ">= 1") - } - if expected, got := true, versioning.If("1.2.3", "> 1.2"); expected != got { - t.Fatalf("expected %s to be %s", "1.2.3", "> 1.2") - } -} - -func TestNewMatcher(t *testing.T) { - app := iris.New() - - userAPI := app.Party("/api/user") - userAPI.Get("/", versioning.NewMatcher(versioning.Map{ - "1.0": sendHandler(v10Response), - ">= 2, < 3": sendHandler(v2Response), - versioning.NotFound: notFoundHandler, - })) - - // middleware as usual. - myMiddleware := func(ctx iris.Context) { - ctx.Header("X-Custom", "something") - ctx.Next() - } - myVersions := versioning.Map{ - "1.0": sendHandler(v10Response), - } - - userAPI.Get("/with_middleware", myMiddleware, versioning.NewMatcher(myVersions)) - - e := httptest.New(t, app) - - e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect(). - Status(iris.StatusOK).Body().Equal(v10Response) - e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0").Expect(). - Status(iris.StatusOK).Body().Equal(v2Response) - e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1").Expect(). - Status(iris.StatusOK).Body().Equal(v2Response) - e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect(). - Status(iris.StatusOK).Body().Equal(v2Response) - - // middleware as usual. - ex := e.GET("/api/user/with_middleware").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect() - ex.Status(iris.StatusOK).Body().Equal(v10Response) - ex.Header("X-Custom").Equal("something") - - e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect(). - Status(iris.StatusNotFound).Body().Equal("Not Found") -} - -func TestNewGroup(t *testing.T) { - app := iris.New() - - userAPI := app.Party("/api/user") - // [... static serving, middlewares and etc goes here]. - - userAPIV10 := versioning.NewGroup(userAPI, "1.0").Deprecated(versioning.DefaultDeprecationOptions) - // V10middlewareResponse := "m1" - // userAPIV10.Use(func(ctx iris.Context) { - // println("exec userAPIV10.Use - midl1") - // sendHandler(V10middlewareResponse)(ctx) - // ctx.Next() - // }) - // userAPIV10.Use(func(ctx iris.Context) { - // println("exec userAPIV10.Use - midl2") - // sendHandler(V10middlewareResponse + "midl2")(ctx) - // ctx.Next() - // }) - // userAPIV10.Use(func(ctx iris.Context) { - // println("exec userAPIV10.Use - midl3") - // ctx.Next() - // }) - - userAPIV10.Get("/", sendHandler(v10Response)) - userAPIV2 := versioning.NewGroup(userAPI, ">= 2, < 3") - // V2middlewareResponse := "m2" - // userAPIV2.Use(func(ctx iris.Context) { - // println("exec userAPIV2.Use - midl1") - // sendHandler(V2middlewareResponse)(ctx) - // ctx.Next() - // }) - // userAPIV2.Use(func(ctx iris.Context) { - // println("exec userAPIV2.Use - midl2") - // ctx.Next() - // }) - - userAPIV2.Get("/", sendHandler(v2Response)) - userAPIV2.Post("/", sendHandler(v2Response)) - userAPIV2.Put("/other", sendHandler(v2Response)) - - e := httptest.New(t, app) - - ex := e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect() - ex.Status(iris.StatusOK).Body().Equal(v10Response) - ex.Header("X-API-Warn").Equal(versioning.DefaultDeprecationOptions.WarnMessage) - - e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0").Expect(). - Status(iris.StatusOK).Body().Equal(v2Response) - e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1").Expect(). - Status(iris.StatusOK).Body().Equal(v2Response) - e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect(). - Status(iris.StatusOK).Body().Equal(v2Response) - e.POST("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2").Expect(). - Status(iris.StatusOK).Body().Equal(v2Response) - e.PUT("/api/user/other").WithHeader(versioning.AcceptVersionHeaderKey, "2.9").Expect(). - Status(iris.StatusOK).Body().Equal(v2Response) - - e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect(). - Status(iris.StatusNotImplemented).Body().Equal("version not found") -} From 541fa75caf543bcdef65b8dae8f2d493de806c48 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 7 Jan 2021 05:36:56 +0200 Subject: [PATCH 12/12] minor (see previous commit) --- HISTORY.md | 2 +- _examples/routing/versioning/main.go | 4 +++- versioning/group_test.go | 4 ---- versioning/version.go | 1 - 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 3a8fc9c4..307e4b65 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -723,7 +723,7 @@ Response: ## Breaking Changes -- Strict versions format on `versioning.NewGroup` is required. E.g. `"1"` is not valid anymore, you have to specify `"1.0.0"`. Example: `NewGroup(api, ">=1.0.0 <2.0.0")`. The [routing/versioning](_examples/routing/versioning) examples have been updated. +- The `versioning.NewMatcher` has been removed entirely in favor of `NewGroup`. Strict versions format on `versioning.NewGroup` is required. E.g. `"1"` is not valid anymore, you have to specify `"1.0.0"`. Example: `NewGroup(api, ">=1.0.0 <2.0.0")`. The [routing/versioning](_examples/routing/versioning) examples have been updated. - Now that `RegisterView` can be used to register different view engines per-Party, there is no need to support registering multiple engines under the same Party. The `app.RegisterView` now upserts the given Engine instead of append. You can now render templates **without file extension**, e.g. `index` instead of `index.ace`, both forms are valid now. - The `Context.ContentType` does not accept filenames to resolve the mime type anymore (caused issues with vendor-specific(vnd) MIME types). - The `Configuration.RemoteAddrPrivateSubnets.IPRange.Start and End` are now type of `string` instead of `net.IP`. The `WithRemoteAddrPrivateSubnet` option remains as it is, already accepts `string`s. diff --git a/_examples/routing/versioning/main.go b/_examples/routing/versioning/main.go index 221fcf5f..3e18aa7e 100644 --- a/_examples/routing/versioning/main.go +++ b/_examples/routing/versioning/main.go @@ -33,7 +33,7 @@ func main() { 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) + versioning.SetVersion(ctx, version) } ctx.Next() @@ -48,6 +48,8 @@ func main() { // 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. diff --git a/versioning/group_test.go b/versioning/group_test.go index ee8726c2..8512bc16 100644 --- a/versioning/group_test.go +++ b/versioning/group_test.go @@ -8,10 +8,6 @@ import ( "github.com/kataras/iris/v12/versioning" ) -func notFoundHandler(ctx iris.Context) { - ctx.NotFound() -} - const ( v10Response = "v1.0 handler" v2Response = "v2.x handler" diff --git a/versioning/version.go b/versioning/version.go index a8084e4e..cf2adc87 100644 --- a/versioning/version.go +++ b/versioning/version.go @@ -244,7 +244,6 @@ func Handler(version string) context.Handler { if err != nil { return func(ctx *context.Context) { ctx.StopWithError(500, err) - return } }