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!
+
+
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
}