diff --git a/_examples/routing/dynamic-path/main.go b/_examples/routing/dynamic-path/main.go index d94b0c18..6e9fd0bb 100644 --- a/_examples/routing/dynamic-path/main.go +++ b/_examples/routing/dynamic-path/main.go @@ -28,30 +28,65 @@ func main() { // otherwise it pre-compiles the regexp and adds the necessary middleware(s). // // Standard macro types for parameters: - // +------------------------+ - // | {param:string} | - // +------------------------+ + // +------------------------+ + // | {param:string} | + // +------------------------+ // string type - // anything + // anything (single path segmnent) // - // +-------------------------------+ - // | {param:int} or {param:int} | - // +-------------------------------+ + // +-------------------------------+ + // | {param:int} | + // +-------------------------------+ // 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 // -9223372036854775808 to 9223372036854775807 // // +------------------------+ + // | {param:uint} | + // +------------------------+ + // uint type + // 0 to 18446744073709551615 (x64) or 0 to 4294967295 (x32) + // + // +------------------------+ // | {param:uint8} | // +------------------------+ // uint8 type // 0 to 255 // + // +------------------------+ + // | {param:uint16} | + // +------------------------+ + // uint16 type + // 0 to 65535 + // + // +------------------------+ + // | {param:uint32} | + // +------------------------+ + // uint32 type + // 0 to 4294967295 // // +------------------------+ // | {param:uint64} | @@ -66,15 +101,15 @@ func main() { // only "1" or "t" or "T" or "TRUE" or "true" or "True" // or "0" or "f" or "F" or "FALSE" or "false" or "False" // - // +------------------------+ - // | {param:alphabetical} | - // +------------------------+ + // +------------------------+ + // | {param:alphabetical} | + // +------------------------+ // alphabetical/letter type // letters only (upper or lowercase) // - // +------------------------+ - // | {param:file} | - // +------------------------+ + // +------------------------+ + // | {param:file} | + // +------------------------+ // file type // letters (upper or lowercase) // numbers (0-9) @@ -83,12 +118,12 @@ func main() { // point (.) // no spaces ! or other character // - // +------------------------+ - // | {param:path} | - // +------------------------+ + // +------------------------+ + // | {param:path} | + // +------------------------+ // path type - // anything, should be the last part, more than one path segment, - // i.e: /path1/path2/path3 , ctx.Params().Get("param") == "/path1/path2/path3" + // anything, should be the last part, can be more than one path segment, + // 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 // {param} == {param:string}. diff --git a/versioning/deprecation.go b/versioning/deprecation.go index afccc5f9..3d8b5363 100644 --- a/versioning/deprecation.go +++ b/versioning/deprecation.go @@ -12,6 +12,10 @@ type DeprecationOptions struct { DeprecationInfo string } +func (opts DeprecationOptions) ShouldHandle() bool { + return opts.WarnMessage != "" || !opts.DeprecationDate.IsZero() || opts.DeprecationInfo != "" +} + var DefaultDeprecationOptions = DeprecationOptions{ WarnMessage: "WARNING! You are using a deprecated version of this API.", } diff --git a/versioning/group.go b/versioning/group.go new file mode 100644 index 00000000..2d52c15c --- /dev/null +++ b/versioning/group.go @@ -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 +} diff --git a/versioning/version.go b/versioning/version.go index 1536ea2d..3de1f865 100644 --- a/versioning/version.go +++ b/versioning/version.go @@ -27,8 +27,9 @@ var NotFoundHandler = func(ctx context.Context) { This is the appropriate response when the server does not recognize the request method and is not capable of supporting it for any resource. */ - ctx.WriteString("version not found") + ctx.StatusCode(501) + ctx.WriteString("version not found") } func GetVersion(ctx context.Context) string { diff --git a/versioning/versioning.go b/versioning/versioning.go index 8aaec792..90d976c0 100644 --- a/versioning/versioning.go +++ b/versioning/versioning.go @@ -8,7 +8,7 @@ import ( type Map map[string]context.Handler -func Handler(versions Map) context.Handler { +func NewMatcher(versions Map) context.Handler { constraintsHandlers, notFoundHandler := buildConstraints(versions) 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) } } @@ -68,14 +71,14 @@ func buildConstraints(versionsHandler Map) (constraintsHandlers []*constraintsHa // >= 3.0, < 4.0. // 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("2.0", vHandler("v2 here")), V("< 4.0", vHandler("v3.x here")), )) 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")), "2.0": vHandler("v2 here"), ">= 3.0, < 4.0": vHandler("v3.x here"), diff --git a/versioning/versioning_test.go b/versioning/versioning_test.go index 40dc41a3..06a205d2 100644 --- a/versioning/versioning_test.go +++ b/versioning/versioning_test.go @@ -23,11 +23,11 @@ func sendHandler(contents string) iris.Handler { } } -func TestHandler(t *testing.T) { +func TestNewMatcher(t *testing.T) { app := iris.New() userAPI := app.Party("/api/user") - userAPI.Get("/", versioning.Handler(versioning.Map{ + userAPI.Get("/", versioning.NewMatcher(versioning.Map{ "1.0": sendHandler(v10Response), ">= 2, < 3": sendHandler(v2Response), versioning.NotFound: notFoundHandler, @@ -47,3 +47,71 @@ func TestHandler(t *testing.T) { e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect(). 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") +}