mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 02:31:04 +01:00
New feature: versioning.Aliases
Thanks @mulyawansentosa and @remopavithran for your donates ❤️
This commit is contained in:
parent
7aa2d1f9d5
commit
b409f7807e
23
HISTORY.md
23
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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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: ")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
@ -1210,6 +1224,7 @@ func DefaultConfiguration() Configuration {
|
|||
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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
if !aliasFound { // don't lose any time to set if already set.
|
||||
SetVersion(ctx, versionString)
|
||||
ctx.Header("X-API-Version", 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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user