New feature: versioning.Aliases

Thanks @mulyawansentosa and @remopavithran for your donates ❤️
This commit is contained in:
Gerasimos (Makis) Maropoulos 2021-01-06 01:52:39 +02:00
parent 7aa2d1f9d5
commit b409f7807e
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
28 changed files with 396 additions and 85 deletions

View File

@ -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.

View File

@ -36,6 +36,8 @@ With your help, we can improve Open Source web development for everyone!
> Donations from **China** are now accepted!
<p>
<a href="https://github.com/remopavithran"><img src="https://avatars1.githubusercontent.com/u/50388068?v=4" alt ="Pavithran" title="remopavithran" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
<a href="https://github.com/mulyawansentosa"><img src="https://avatars1.githubusercontent.com/u/29946673?v=4" alt ="MULYAWAN SENTOSA" title="mulyawansentosa" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
<a href="https://github.com/TianJIANG"><img src="https://avatars1.githubusercontent.com/u/158459?v=4" alt ="KIT UNITED" title="TianJIANG" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
<a href="https://github.com/rhernandez-itemsoft"><img src="https://avatars1.githubusercontent.com/u/4327356?v=4" alt ="Ricardo Hernandez Lopez" title="rhernandez-itemsoft" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>
<a href="https://github.com/ChinChuanKuo"><img src="https://avatars1.githubusercontent.com/u/11756978?v=4" alt ="ChinChuanKuo" title="ChinChuanKuo" with="75" style="width:75px;max-width:75px;height:75px" height="75" /></a>

View File

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

View File

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

View File

@ -82,11 +82,11 @@ func registerGamesRoutes(app *iris.Application) {
}
func registerSubdomains(app *iris.Application) {
mysubdomain := app.Party("mysubdomain.")
mysubdomain := app.Subdomain("mysubdomain")
// http://mysubdomain.myhost.com
mysubdomain.Get("/", h)
willdcardSubdomain := app.Party("*.")
willdcardSubdomain := app.WildcardSubdomain()
willdcardSubdomain.Get("/", h)
willdcardSubdomain.Party("/party").Get("/", h)
}

View File

@ -116,7 +116,7 @@ func main() {
adminRoutes.Get("/settings", info)
// Wildcard/dynamic subdomain
dynamicSubdomainRoutes := app.Party("*.")
dynamicSubdomainRoutes := app.WildcardSubdomain()
// GET: http://any_thing_here.localhost:8080
dynamicSubdomainRoutes.Get("/", info)

View File

@ -33,7 +33,7 @@ func main() {
}*/
// no order, you can register subdomains at the end also.
dynamicSubdomains := app.Party("*.")
dynamicSubdomains := app.WildcardSubdomain()
{
dynamicSubdomains.Get("/", dynamicSubdomainHandler)

View File

@ -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")

View File

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

View File

@ -16,12 +16,12 @@ func TestSessionsEncodeDecode(t *testing.T) {
es.Cookies().NotEmpty()
es.Body().Equal("All ok session set to: iris [isNew=true]")
e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: iris")
e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The username on the /set was: iris")
// delete and re-get
e.GET("/delete").Expect().Status(iris.StatusOK)
e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: ")
e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The username on the /set was: ")
// set, clear and re-get
e.GET("/set").Expect().Body().Equal("All ok session set to: iris [isNew=false]")
e.GET("/clear").Expect().Status(iris.StatusOK)
e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: ")
e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The username on the /set was: ")
}

View File

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

View File

@ -27,7 +27,7 @@ func main() {
// wildcard subdomain, will catch username1.... username2.... username3... username4.... username5...
// that our below links are providing via page.html's first argument which is the subdomain.
subdomain := app.Party("*.")
subdomain := app.WildcardSubdomain()
mypathRoute := subdomain.Get("/mypath", emptyHandler)
mypathRoute.Name = "my-page1"

View File

@ -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",

View File

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

View File

@ -81,15 +81,15 @@ to the end-developer's custom implementations.
// SimpleUser is a simple implementation of the User interface.
type SimpleUser struct {
Authorization string `json:"authorization,omitempty"`
AuthorizedAt time.Time `json:"authorized_at,omitempty"`
ID string `json:"id,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Email string `json:"email,omitempty"`
Roles []string `json:"roles,omitempty"`
Token json.RawMessage `json:"token,omitempty"`
Fields Map `json:"fields,omitempty"`
Authorization string `json:"authorization,omitempty" db:"authorization"`
AuthorizedAt time.Time `json:"authorized_at,omitempty" db:"authorized_at"`
ID string `json:"id,omitempty" db:"id"`
Username string `json:"username,omitempty" db:"username"`
Password string `json:"password,omitempty" db:"password"`
Email string `json:"email,omitempty" db:"email"`
Roles []string `json:"roles,omitempty" db:"roles"`
Token json.RawMessage `json:"token,omitempty" db:"token"`
Fields Map `json:"fields,omitempty" db:"fields"`
}
var _ User = (*SimpleUser)(nil)

View File

@ -239,6 +239,10 @@ var ignoreMainHandlerNames = [...]string{
"iris.reCAPTCHA",
"iris.profiling",
"iris.recover",
"iris.accesslog",
"iris.grpc",
"iris.requestid",
"iris.rewrite",
}
// ingoreMainHandlerName reports whether a main handler of "name" should

View File

@ -62,6 +62,10 @@ type RouteReadOnly interface {
// MainHandlerIndex returns the first registered handler's index for the route.
MainHandlerIndex() int
// Property returns a specific property based on its "key"
// of this route's Party owner.
Property(key string) (interface{}, bool)
// Sitemap properties: https://www.sitemaps.org/protocol.html
// GetLastMod returns the date of last modification of the file served by this route.

View File

@ -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.

View File

@ -39,6 +39,10 @@ type Party interface {
// Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path
Macros() *macro.Macros
// Properties returns the original Party's properties map,
// it can be modified before server startup but not afterwards.
Properties() context.Map
// SetRoutesNoLog disables (true) the verbose logging for the next registered
// routes under this Party and its children.
//

View File

@ -19,6 +19,8 @@ import (
// If any of the following fields are changed then the
// caller should Refresh the router.
type Route struct {
// The Party which this Route was created and registered on.
Party Party
Title string `json:"title"` // custom name to replace the method on debug logging.
Name string `json:"name"` // "userRoute"
Description string `json:"description"` // "lists a user"
@ -86,7 +88,7 @@ type Route struct {
// handlers and the macro container which all routes should share.
// It parses the path based on the "macros",
// handlers are being changed to validate the macros at serve time, if needed.
func NewRoute(statusErrorCode int, method, subdomain, unparsedPath string,
func NewRoute(p Party, statusErrorCode int, method, subdomain, unparsedPath string,
handlers context.Handlers, macros macro.Macros) (*Route, error) {
tmpl, err := macro.Parse(unparsedPath, macros)
if err != nil {
@ -110,6 +112,7 @@ func NewRoute(statusErrorCode int, method, subdomain, unparsedPath string,
formattedPath := formatPath(path)
route := &Route{
Party: p,
StatusCode: statusErrorCode,
Name: defaultName,
Method: method,
@ -583,6 +586,8 @@ type routeReadOnlyWrapper struct {
*Route
}
var _ context.RouteReadOnly = routeReadOnlyWrapper{}
func (rd routeReadOnlyWrapper) StatusErrorCode() int {
return rd.Route.StatusCode
}
@ -619,6 +624,17 @@ func (rd routeReadOnlyWrapper) MainHandlerIndex() int {
return rd.Route.MainHandlerIndex
}
func (rd routeReadOnlyWrapper) Property(key string) (interface{}, bool) {
properties := rd.Route.Party.Properties()
if properties != nil {
if property, ok := properties[key]; ok {
return property, true
}
}
return nil, false
}
func (rd routeReadOnlyWrapper) GetLastMod() time.Time {
return rd.Route.LastMod
}

View File

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

View File

@ -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)

View File

@ -339,14 +339,14 @@ func (db *Database) Visit(sid string, cb func(key string, value interface{})) er
}
// Len returns the length of the session's entries (keys).
func (db *Database) Len(sid string) (n int64) {
func (db *Database) Len(sid string) (n int) {
err := db.Service.View(func(tx *bolt.Tx) error {
b := db.getBucketForSession(tx, sid)
if b == nil {
return nil
}
n = int64(b.Stats().KeyN)
n = int(int64(b.Stats().KeyN))
return nil
})

View File

@ -6,10 +6,17 @@ import (
"github.com/kataras/iris/v12/context"
)
// The response header keys when a resource is deprecated by the server.
const (
APIWarnHeader = "X-Api-Warn"
APIDeprecationDateHeader = "X-Api-Deprecation-Date"
APIDeprecationInfoHeader = "X-Api-Deprecation-Info"
)
// DeprecationOptions describes the deprecation headers key-values.
// - "X-API-Warn": options.WarnMessage
// - "X-API-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate))
// - "X-API-Deprecation-Info": options.DeprecationInfo
// - "X-Api-Warn": options.WarnMessage
// - "X-Api-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate))
// - "X-Api-Deprecation-Info": options.DeprecationInfo
type DeprecationOptions struct {
WarnMessage string
DeprecationDate time.Time
@ -37,14 +44,14 @@ func WriteDeprecated(ctx *context.Context, options DeprecationOptions) {
options.WarnMessage = DefaultDeprecationOptions.WarnMessage
}
ctx.Header("X-API-Warn", options.WarnMessage)
ctx.Header(APIWarnHeader, options.WarnMessage)
if !options.DeprecationDate.IsZero() {
ctx.Header("X-API-Deprecation-Date", context.FormatTime(ctx, options.DeprecationDate))
ctx.Header(APIDeprecationDateHeader, context.FormatTime(ctx, options.DeprecationDate))
}
if options.DeprecationInfo != "" {
ctx.Header("X-API-Deprecation-Info", options.DeprecationInfo)
ctx.Header(APIDeprecationInfoHeader, options.DeprecationInfo)
}
}

View File

@ -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()
}
}

View File

@ -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
}
}
}

View File

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

View File

@ -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
}