mirror of
https://github.com/kataras/iris.git
synced 2026-01-09 21:15:56 +00:00
API versioning improvements
Replace the go-version package with a regex-free alternative semver
the result: versioned apis have almost zero performance cost now
thanks @motogo for your kind donation ❤️ - please check your github notifications
This commit is contained in:
@@ -1,57 +1,107 @@
|
||||
package versioning
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris/v12/context"
|
||||
"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"
|
||||
"github.com/blang/semver/v4"
|
||||
)
|
||||
|
||||
// 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.
|
||||
type API = router.Party
|
||||
|
||||
// Group is a group of version-based routes.
|
||||
// One version per one or more routes.
|
||||
// Group represents a group of resources that should
|
||||
// be handled based on a version requested by the client.
|
||||
// See `NewGroup` for more.
|
||||
type Group struct {
|
||||
API
|
||||
|
||||
// Information not currently in-use.
|
||||
version string
|
||||
validate semver.Range
|
||||
deprecation DeprecationOptions
|
||||
}
|
||||
|
||||
// NewGroup returns a ptr to Group based on the given "version" constraint.
|
||||
// NewGroup returns a version 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).
|
||||
//
|
||||
// A version is extracted through the versioning.GetVersion function:
|
||||
// Accept-Version: 1.0.0
|
||||
// Accept: application/json; version=1.0.0
|
||||
// You can customize it by setting a version based on the request context:
|
||||
// api.Use(func(ctx *context.Context) {
|
||||
// if version := ctx.URLParam("version"); version != "" {
|
||||
// SetVersion(ctx, version)
|
||||
// }
|
||||
//
|
||||
// ctx.Next()
|
||||
// })
|
||||
// OR:
|
||||
// api.Use(versioning.FromQuery("version", ""))
|
||||
//
|
||||
// Examples at: _examples/routing/versioning
|
||||
// Usage:
|
||||
// app := iris.New()
|
||||
// api := app.Party("/api")
|
||||
// v1 := versioning.NewGroup(api, ">= 1, < 2")
|
||||
// v1 := versioning.NewGroup(api, ">=1.0.0 <2.0.0")
|
||||
// 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 {
|
||||
// Valid ranges are:
|
||||
// - "<1.0.0"
|
||||
// - "<=1.0.0"
|
||||
// - ">1.0.0"
|
||||
// - ">=1.0.0"
|
||||
// - "1.0.0", "=1.0.0", "==1.0.0"
|
||||
// - "!1.0.0", "!=1.0.0"
|
||||
//
|
||||
// A Range can consist of multiple ranges separated by space:
|
||||
// Ranges can be linked by logical AND:
|
||||
// - ">1.0.0 <2.0.0" would match between both ranges, so "1.1.1" and "1.8.7"
|
||||
// but not "1.0.0" or "2.0.0"
|
||||
// - ">1.0.0 <3.0.0 !2.0.3-beta.2" would match every version between 1.0.0 and 3.0.0
|
||||
// except 2.0.3-beta.2
|
||||
//
|
||||
// Ranges can also be linked by logical OR:
|
||||
// - "<2.0.0 || >=3.0.0" would match "1.x.x" and "3.x.x" but not "2.x.x"
|
||||
//
|
||||
// AND has a higher precedence than OR. It's not possible to use brackets.
|
||||
//
|
||||
// Ranges can be combined by both AND and OR
|
||||
//
|
||||
// - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`,
|
||||
// but not `4.2.1`, `2.1.1`
|
||||
func NewGroup(r API, version string) *Group {
|
||||
version = strings.ReplaceAll(version, ",", " ")
|
||||
version = strings.TrimSpace(version)
|
||||
|
||||
verRange, err := semver.ParseRange(version)
|
||||
if err != nil {
|
||||
r.Logger().Errorf("versioning: %s: %s", r.GetRelPath(), strings.ToLower(err.Error()))
|
||||
return &Group{API: r}
|
||||
}
|
||||
|
||||
// Clone this one.
|
||||
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.
|
||||
r.SetRegisterRule(router.RouteOverlap)
|
||||
r.UseOnce(Handler(version)) // this is required in order to not populate this middleware to the next group.
|
||||
|
||||
handler := makeHandler(verRange)
|
||||
// This is required in order to not populate this middleware to the next group.
|
||||
r.UseOnce(handler)
|
||||
// This is required for versioned custom error handlers,
|
||||
// of course if the parent registered one then this will do nothing.
|
||||
r.UseError(handler)
|
||||
|
||||
return &Group{
|
||||
API: r,
|
||||
version: version,
|
||||
API: r,
|
||||
validate: verRange,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,18 +118,18 @@ 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 {
|
||||
func makeHandler(validate semver.Range) context.Handler {
|
||||
return func(ctx *context.Context) {
|
||||
version := ctx.URLParam(urlQueryParameterName)
|
||||
if version == "" {
|
||||
version = defaultVersion
|
||||
}
|
||||
|
||||
if version != "" {
|
||||
SetVersion(ctx, version)
|
||||
if !matchVersionRange(ctx, validate) {
|
||||
// The overlapped handler has an exception
|
||||
// of a type of context.NotFound (which versioning.ErrNotFound wraps)
|
||||
// to clear the status code
|
||||
// and the error to ignore this
|
||||
// when available match version exists (see `NewGroup`).
|
||||
if h := NotFoundHandler; h != nil {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Next()
|
||||
|
||||
Reference in New Issue
Block a user