mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 18:51:03 +01:00
240fdb6dc3
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
138 lines
4.0 KiB
Go
138 lines
4.0 KiB
Go
package versioning
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/kataras/iris/v12/context"
|
|
"github.com/kataras/iris/v12/core/router"
|
|
|
|
"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 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
|
|
|
|
validate semver.Range
|
|
deprecation DeprecationOptions
|
|
}
|
|
|
|
// 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.0.0 <2.0.0")
|
|
// v1.Get/Post/Put/Delete...
|
|
//
|
|
// 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("/")
|
|
|
|
// 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)
|
|
|
|
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,
|
|
validate: verRange,
|
|
}
|
|
}
|
|
|
|
// Deprecated marks this group and all its versioned routes
|
|
// as deprecated versions of that endpoint.
|
|
func (g *Group) Deprecated(options DeprecationOptions) *Group {
|
|
// store it for future use, e.g. collect all deprecated APIs and notify the developer.
|
|
g.deprecation = options
|
|
|
|
g.API.UseOnce(func(ctx *context.Context) {
|
|
WriteDeprecated(ctx, options)
|
|
ctx.Next()
|
|
})
|
|
return g
|
|
}
|
|
|
|
func makeHandler(validate semver.Range) context.Handler {
|
|
return func(ctx *context.Context) {
|
|
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()
|
|
}
|
|
}
|