versioning API: initialize support for grouping

Former-commit-id: 36cf8cd79801e8556f3c7b560f3bd759d9770d67
This commit is contained in:
Gerasimos (Makis) Maropoulos 2018-11-10 23:29:24 +02:00
parent b22a18da6b
commit 7608873e70
6 changed files with 314 additions and 28 deletions

View File

@ -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}.

View File

@ -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
View 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
}

View File

@ -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 {

View File

@ -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"),

View File

@ -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")
}