From b409f7807e548bcd7b67eccd9015c18b14399c80 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 6 Jan 2021 01:52:39 +0200 Subject: [PATCH] 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 }