mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 10:41:03 +01:00
versioning API: initialize support for grouping
Former-commit-id: 36cf8cd79801e8556f3c7b560f3bd759d9770d67
This commit is contained in:
parent
b22a18da6b
commit
7608873e70
|
@ -28,30 +28,65 @@ func main() {
|
||||||
// otherwise it pre-compiles the regexp and adds the necessary middleware(s).
|
// otherwise it pre-compiles the regexp and adds the necessary middleware(s).
|
||||||
//
|
//
|
||||||
// Standard macro types for parameters:
|
// Standard macro types for parameters:
|
||||||
// +------------------------+
|
// +------------------------+
|
||||||
// | {param:string} |
|
// | {param:string} |
|
||||||
// +------------------------+
|
// +------------------------+
|
||||||
// string type
|
// string type
|
||||||
// anything
|
// anything (single path segmnent)
|
||||||
//
|
//
|
||||||
// +-------------------------------+
|
// +-------------------------------+
|
||||||
// | {param:int} or {param:int} |
|
// | {param:int} |
|
||||||
// +-------------------------------+
|
// +-------------------------------+
|
||||||
// int type
|
// int type
|
||||||
// both positive and negative numbers, any number of digits (ctx.Params().GetInt will limit the digits based on the host arch)
|
// -9223372036854775808 to 9223372036854775807 (x64) or -2147483648 to 2147483647 (x32), depends on the host arch
|
||||||
//
|
//
|
||||||
// +-------------------------------+
|
// +------------------------+
|
||||||
// | {param:int64} or {param:long} |
|
// | {param:int8} |
|
||||||
// +-------------------------------+
|
// +------------------------+
|
||||||
|
// int8 type
|
||||||
|
// -128 to 127
|
||||||
|
//
|
||||||
|
// +------------------------+
|
||||||
|
// | {param:int16} |
|
||||||
|
// +------------------------+
|
||||||
|
// int16 type
|
||||||
|
// -32768 to 32767
|
||||||
|
//
|
||||||
|
// +------------------------+
|
||||||
|
// | {param:int32} |
|
||||||
|
// +------------------------+
|
||||||
|
// int32 type
|
||||||
|
// -2147483648 to 2147483647
|
||||||
|
//
|
||||||
|
// +------------------------+
|
||||||
|
// | {param:int64} |
|
||||||
|
// +------------------------+
|
||||||
// int64 type
|
// int64 type
|
||||||
// -9223372036854775808 to 9223372036854775807
|
// -9223372036854775808 to 9223372036854775807
|
||||||
//
|
//
|
||||||
// +------------------------+
|
// +------------------------+
|
||||||
|
// | {param:uint} |
|
||||||
|
// +------------------------+
|
||||||
|
// uint type
|
||||||
|
// 0 to 18446744073709551615 (x64) or 0 to 4294967295 (x32)
|
||||||
|
//
|
||||||
|
// +------------------------+
|
||||||
// | {param:uint8} |
|
// | {param:uint8} |
|
||||||
// +------------------------+
|
// +------------------------+
|
||||||
// uint8 type
|
// uint8 type
|
||||||
// 0 to 255
|
// 0 to 255
|
||||||
//
|
//
|
||||||
|
// +------------------------+
|
||||||
|
// | {param:uint16} |
|
||||||
|
// +------------------------+
|
||||||
|
// uint16 type
|
||||||
|
// 0 to 65535
|
||||||
|
//
|
||||||
|
// +------------------------+
|
||||||
|
// | {param:uint32} |
|
||||||
|
// +------------------------+
|
||||||
|
// uint32 type
|
||||||
|
// 0 to 4294967295
|
||||||
//
|
//
|
||||||
// +------------------------+
|
// +------------------------+
|
||||||
// | {param:uint64} |
|
// | {param:uint64} |
|
||||||
|
@ -66,15 +101,15 @@ func main() {
|
||||||
// only "1" or "t" or "T" or "TRUE" or "true" or "True"
|
// only "1" or "t" or "T" or "TRUE" or "true" or "True"
|
||||||
// or "0" or "f" or "F" or "FALSE" or "false" or "False"
|
// or "0" or "f" or "F" or "FALSE" or "false" or "False"
|
||||||
//
|
//
|
||||||
// +------------------------+
|
// +------------------------+
|
||||||
// | {param:alphabetical} |
|
// | {param:alphabetical} |
|
||||||
// +------------------------+
|
// +------------------------+
|
||||||
// alphabetical/letter type
|
// alphabetical/letter type
|
||||||
// letters only (upper or lowercase)
|
// letters only (upper or lowercase)
|
||||||
//
|
//
|
||||||
// +------------------------+
|
// +------------------------+
|
||||||
// | {param:file} |
|
// | {param:file} |
|
||||||
// +------------------------+
|
// +------------------------+
|
||||||
// file type
|
// file type
|
||||||
// letters (upper or lowercase)
|
// letters (upper or lowercase)
|
||||||
// numbers (0-9)
|
// numbers (0-9)
|
||||||
|
@ -83,12 +118,12 @@ func main() {
|
||||||
// point (.)
|
// point (.)
|
||||||
// no spaces ! or other character
|
// no spaces ! or other character
|
||||||
//
|
//
|
||||||
// +------------------------+
|
// +------------------------+
|
||||||
// | {param:path} |
|
// | {param:path} |
|
||||||
// +------------------------+
|
// +------------------------+
|
||||||
// path type
|
// path type
|
||||||
// anything, should be the last part, more than one path segment,
|
// anything, should be the last part, can be more than one path segment,
|
||||||
// i.e: /path1/path2/path3 , ctx.Params().Get("param") == "/path1/path2/path3"
|
// i.e: "/test/*param" and request: "/test/path1/path2/path3" , ctx.Params().Get("param") == "path1/path2/path3"
|
||||||
//
|
//
|
||||||
// if type is missing then parameter's type is defaulted to string, so
|
// if type is missing then parameter's type is defaulted to string, so
|
||||||
// {param} == {param:string}.
|
// {param} == {param:string}.
|
||||||
|
|
|
@ -12,6 +12,10 @@ type DeprecationOptions struct {
|
||||||
DeprecationInfo string
|
DeprecationInfo string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (opts DeprecationOptions) ShouldHandle() bool {
|
||||||
|
return opts.WarnMessage != "" || !opts.DeprecationDate.IsZero() || opts.DeprecationInfo != ""
|
||||||
|
}
|
||||||
|
|
||||||
var DefaultDeprecationOptions = DeprecationOptions{
|
var DefaultDeprecationOptions = DeprecationOptions{
|
||||||
WarnMessage: "WARNING! You are using a deprecated version of this API.",
|
WarnMessage: "WARNING! You are using a deprecated version of this API.",
|
||||||
}
|
}
|
||||||
|
|
175
versioning/group.go
Normal file
175
versioning/group.go
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
package versioning
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris/context"
|
||||||
|
"github.com/kataras/iris/core/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterGroups(r router.Party, groups ...*Group) []*router.Route {
|
||||||
|
return Concat(groups...).For(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
vroute struct {
|
||||||
|
method string
|
||||||
|
path string
|
||||||
|
versions Map
|
||||||
|
}
|
||||||
|
|
||||||
|
Group struct {
|
||||||
|
version string
|
||||||
|
routes []vroute
|
||||||
|
|
||||||
|
deprecation DeprecationOptions
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewGroup(version string) *Group {
|
||||||
|
return &Group{
|
||||||
|
version: version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated marks this group and all its versioned routes
|
||||||
|
// as deprecated versions of that endpoint.
|
||||||
|
// It can be called in the end just before `Concat` or `RegisterGroups`
|
||||||
|
// or first by `NewGroup(...).Deprecated(...)`. It returns itself.
|
||||||
|
func (g *Group) Deprecated(options DeprecationOptions) *Group {
|
||||||
|
// if `Deprecated` is called in the end.
|
||||||
|
for _, r := range g.routes {
|
||||||
|
r.versions[g.version] = Deprecated(r.versions[g.version], options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the options if called before registering any versioned routes.
|
||||||
|
g.deprecation = options
|
||||||
|
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle registers a versioned route to the group.
|
||||||
|
//
|
||||||
|
// See `Concat` and `RegisterGroups` for more.
|
||||||
|
func (g *Group) Handle(method string, registeredPath string, handler context.Handler) {
|
||||||
|
if g.deprecation.ShouldHandle() { // if `Deprecated` called first.
|
||||||
|
handler = Deprecated(handler, g.deprecation)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.routes = append(g.routes, vroute{
|
||||||
|
method: method,
|
||||||
|
path: registeredPath,
|
||||||
|
versions: Map{g.version: handler},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// None registers an "offline" versioned route
|
||||||
|
// see `context#ExecRoute(routeName)` and routing examples.
|
||||||
|
func (g *Group) None(path string, handler context.Handler) {
|
||||||
|
g.Handle(router.MethodNone, path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get registers a versioned route for the Get http method.
|
||||||
|
func (g *Group) Get(path string, handler context.Handler) {
|
||||||
|
g.Handle("GET", path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post registers a versioned route for the Post http method.
|
||||||
|
func (g *Group) Post(path string, handler context.Handler) {
|
||||||
|
g.Handle("POST", path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put registers a versioned route for the Put http method
|
||||||
|
func (g *Group) Put(path string, handler context.Handler) {
|
||||||
|
g.Handle("PUT", path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete registers a versioned route for the Delete http method.
|
||||||
|
func (g *Group) Delete(path string, handler context.Handler) {
|
||||||
|
g.Handle("DELETE", path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect registers a versioned route for the Connect http method.
|
||||||
|
func (g *Group) Connect(path string, handler context.Handler) {
|
||||||
|
g.Handle("CONNECT", path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Head registers a versioned route for the Head http method.
|
||||||
|
func (g *Group) Head(path string, handler context.Handler) {
|
||||||
|
g.Handle("HEAD", path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options registers a versioned route for the Options http method.
|
||||||
|
func (g *Group) Options(path string, handler context.Handler) {
|
||||||
|
g.Handle("OPTIONS", path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch registers a versioned route for the Patch http method.
|
||||||
|
func (g *Group) Patch(path string, handler context.Handler) {
|
||||||
|
g.Handle("PATCH", path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trace registers a versioned route for the Trace http method.
|
||||||
|
func (g *Group) Trace(path string, handler context.Handler) {
|
||||||
|
g.Handle("TRACE", path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any registers a versioned route for ALL of the http methods
|
||||||
|
// (Get,Post,Put,Head,Patch,Options,Connect,Delete).
|
||||||
|
func (g *Group) Any(registeredPath string, handler context.Handler) {
|
||||||
|
g.Get(registeredPath, handler)
|
||||||
|
g.Post(registeredPath, handler)
|
||||||
|
g.Put(registeredPath, handler)
|
||||||
|
g.Delete(registeredPath, handler)
|
||||||
|
g.Connect(registeredPath, handler)
|
||||||
|
g.Head(registeredPath, handler)
|
||||||
|
g.Options(registeredPath, handler)
|
||||||
|
g.Patch(registeredPath, handler)
|
||||||
|
g.Trace(registeredPath, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Groups struct {
|
||||||
|
routes []vroute
|
||||||
|
|
||||||
|
notFoundHandler context.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func Concat(groups ...*Group) *Groups {
|
||||||
|
var total []vroute
|
||||||
|
|
||||||
|
for _, g := range groups {
|
||||||
|
inner:
|
||||||
|
for _, r := range g.routes {
|
||||||
|
for i, tr := range total {
|
||||||
|
if tr.method == r.method && tr.path == r.path {
|
||||||
|
for k, v := range r.versions {
|
||||||
|
total[i].versions[k] = v
|
||||||
|
}
|
||||||
|
continue inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
total = append(total, g.routes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Groups{total, NotFoundHandler}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Groups) NotFound(handler context.Handler) *Groups {
|
||||||
|
g.notFoundHandler = handler
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Groups) For(r router.Party) (totalRoutesRegistered []*router.Route) {
|
||||||
|
for _, vr := range g.routes {
|
||||||
|
if g.notFoundHandler != nil {
|
||||||
|
vr.versions[NotFound] = g.notFoundHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Printf("Method: %s | Path: %s | Versions: %#+v\n", vr.method, vr.path, vr.versions)
|
||||||
|
route := r.Handle(vr.method, vr.path,
|
||||||
|
NewMatcher(vr.versions))
|
||||||
|
totalRoutesRegistered = append(totalRoutesRegistered, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -27,8 +27,9 @@ var NotFoundHandler = func(ctx context.Context) {
|
||||||
This is the appropriate response when the server does not
|
This is the appropriate response when the server does not
|
||||||
recognize the request method and is not capable of supporting it for any resource.
|
recognize the request method and is not capable of supporting it for any resource.
|
||||||
*/
|
*/
|
||||||
ctx.WriteString("version not found")
|
|
||||||
ctx.StatusCode(501)
|
ctx.StatusCode(501)
|
||||||
|
ctx.WriteString("version not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetVersion(ctx context.Context) string {
|
func GetVersion(ctx context.Context) string {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
type Map map[string]context.Handler
|
type Map map[string]context.Handler
|
||||||
|
|
||||||
func Handler(versions Map) context.Handler {
|
func NewMatcher(versions Map) context.Handler {
|
||||||
constraintsHandlers, notFoundHandler := buildConstraints(versions)
|
constraintsHandlers, notFoundHandler := buildConstraints(versions)
|
||||||
|
|
||||||
return func(ctx context.Context) {
|
return func(ctx context.Context) {
|
||||||
|
@ -32,6 +32,9 @@ func Handler(versions Map) context.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pass the not matched version so the not found handler can have knowedge about it.
|
||||||
|
// ctx.Values().Set(Key, versionString)
|
||||||
|
// or let a manual cal of GetVersion(ctx) do that instead.
|
||||||
notFoundHandler(ctx)
|
notFoundHandler(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,14 +71,14 @@ func buildConstraints(versionsHandler Map) (constraintsHandlers []*constraintsHa
|
||||||
// >= 3.0, < 4.0.
|
// >= 3.0, < 4.0.
|
||||||
// I can make it ordered but I do NOT like the final API of it:
|
// I can make it ordered but I do NOT like the final API of it:
|
||||||
/*
|
/*
|
||||||
app.Get("/api/user", Handler( // accepts an array, ordered, see last elem.
|
app.Get("/api/user", NewMatcher( // accepts an array, ordered, see last elem.
|
||||||
V("1.0", vHandler("v1 here")),
|
V("1.0", vHandler("v1 here")),
|
||||||
V("2.0", vHandler("v2 here")),
|
V("2.0", vHandler("v2 here")),
|
||||||
V("< 4.0", vHandler("v3.x here")),
|
V("< 4.0", vHandler("v3.x here")),
|
||||||
))
|
))
|
||||||
instead we have:
|
instead we have:
|
||||||
|
|
||||||
app.Get("/api/user", Handler(Map{ // accepts a map, unordered, see last elem.
|
app.Get("/api/user", NewMatcher(Map{ // accepts a map, unordered, see last elem.
|
||||||
"1.0": Deprecated(vHandler("v1 here")),
|
"1.0": Deprecated(vHandler("v1 here")),
|
||||||
"2.0": vHandler("v2 here"),
|
"2.0": vHandler("v2 here"),
|
||||||
">= 3.0, < 4.0": vHandler("v3.x here"),
|
">= 3.0, < 4.0": vHandler("v3.x here"),
|
||||||
|
|
|
@ -23,11 +23,11 @@ func sendHandler(contents string) iris.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler(t *testing.T) {
|
func TestNewMatcher(t *testing.T) {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
|
||||||
userAPI := app.Party("/api/user")
|
userAPI := app.Party("/api/user")
|
||||||
userAPI.Get("/", versioning.Handler(versioning.Map{
|
userAPI.Get("/", versioning.NewMatcher(versioning.Map{
|
||||||
"1.0": sendHandler(v10Response),
|
"1.0": sendHandler(v10Response),
|
||||||
">= 2, < 3": sendHandler(v2Response),
|
">= 2, < 3": sendHandler(v2Response),
|
||||||
versioning.NotFound: notFoundHandler,
|
versioning.NotFound: notFoundHandler,
|
||||||
|
@ -47,3 +47,71 @@ func TestHandler(t *testing.T) {
|
||||||
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect().
|
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect().
|
||||||
Status(iris.StatusNotFound).Body().Equal("Not Found")
|
Status(iris.StatusNotFound).Body().Equal("Not Found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewGroup(t *testing.T) {
|
||||||
|
app := iris.New()
|
||||||
|
// userAPI := app.Party("/api/user")
|
||||||
|
|
||||||
|
// userAPIV10 := versioning.NewGroup("1.0", userAPI)
|
||||||
|
// userAPIV10.Get("/", sendHandler(v10Response))
|
||||||
|
// userAPIV2 := versioning.NewGroup(">= 2, < 3", userAPI)
|
||||||
|
// userAPIV2.Get("/", sendHandler(v2Response))
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
// userAPI := app.Party("/api/user")
|
||||||
|
// userVAPI := versioning.NewGroup(userAPI)
|
||||||
|
// userAPIV10 := userVAPI.Version("1.0")
|
||||||
|
// userAPIV10.Get("/", sendHandler(v10Response))
|
||||||
|
|
||||||
|
// userAPIV10 := userVAPI.Version("2.0")
|
||||||
|
// userAPIV10.Get("/", sendHandler(v10Response))
|
||||||
|
// userVAPI.NotFound(...)
|
||||||
|
// userVAPI.Build()
|
||||||
|
|
||||||
|
// --
|
||||||
|
|
||||||
|
userAPI := app.Party("/api/user")
|
||||||
|
// [... static serving, middlewares and etc goes here].
|
||||||
|
|
||||||
|
userAPIV10 := versioning.NewGroup("1.0").Deprecated(versioning.DefaultDeprecationOptions)
|
||||||
|
userAPIV10.Get("/", sendHandler(v10Response))
|
||||||
|
|
||||||
|
userAPIV2 := versioning.NewGroup(">= 2, < 3")
|
||||||
|
userAPIV2.Get("/", sendHandler(v2Response))
|
||||||
|
userAPIV2.Post("/", sendHandler(v2Response))
|
||||||
|
userAPIV2.Put("/other", sendHandler(v2Response))
|
||||||
|
|
||||||
|
// versioning.Concat(userAPIV10, userAPIV2)
|
||||||
|
// NotFound(func(ctx iris.Context) {
|
||||||
|
// ctx.StatusCode(iris.StatusNotFound)
|
||||||
|
// ctx.Writef("unknown version %s", versioning.GetVersion(ctx))
|
||||||
|
// }).
|
||||||
|
// For(userAPI)
|
||||||
|
// This is legal too:
|
||||||
|
// For(app.PartyFunc("/api/user", func(r iris.Party) {
|
||||||
|
// // [... static serving, middlewares and etc goes here].
|
||||||
|
// }))
|
||||||
|
|
||||||
|
versioning.RegisterGroups(userAPI, userAPIV10, userAPIV2)
|
||||||
|
|
||||||
|
e := httptest.New(t, app)
|
||||||
|
|
||||||
|
ex := e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect()
|
||||||
|
ex.Status(iris.StatusOK).Body().Equal(v10Response)
|
||||||
|
ex.Header("X-API-Warn").Equal(versioning.DefaultDeprecationOptions.WarnMessage)
|
||||||
|
|
||||||
|
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0").Expect().
|
||||||
|
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||||
|
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1").Expect().
|
||||||
|
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||||
|
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect().
|
||||||
|
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||||
|
e.POST("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2").Expect().
|
||||||
|
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||||
|
e.PUT("/api/user/other").WithHeader(versioning.AcceptVersionHeaderKey, "2.9").Expect().
|
||||||
|
Status(iris.StatusOK).Body().Equal(v2Response)
|
||||||
|
|
||||||
|
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect().
|
||||||
|
Status(iris.StatusNotImplemented).Body().Equal("version not found")
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user