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