mirror of
https://github.com/kataras/iris.git
synced 2026-01-18 09:25:59 +00:00
New feature: versioning.Aliases
Thanks @mulyawansentosa and @remopavithran for your donates ❤️
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user