mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 02:31:04 +01:00
new feature: versioned controllers
Former-commit-id: c797e23c78b1e74bbe9ba56673f3a98f17f5e2f7
This commit is contained in:
parent
3f98b39632
commit
311b560717
|
@ -371,13 +371,17 @@ Other Improvements:
|
|||
|
||||
![DBUG routes](https://iris-go.com/images/v12.2.0-dbug2.png?v=0)
|
||||
|
||||
- New [rollbar example](https://github.com/kataras/iris/tree/master/_examples/logging/rollbar/main.go).
|
||||
- Versioned Controllers feature through the new `mvc.Version` option. See [_examples/mvc/versioned-controller](https://github.com/kataras/iris/blob/master/_examples/mvc/versioned-controller/main.go).
|
||||
|
||||
- Fix [#1539](https://github.com/kataras/iris/issues/1539).
|
||||
|
||||
- New [rollbar example](https://github.com/kataras/iris/blob/master/_examples/logging/rollbar/main.go).
|
||||
|
||||
- New builtin [requestid](https://github.com/kataras/iris/tree/master/middleware/requestid) middleware.
|
||||
|
||||
- New builtin [JWT](https://github.com/kataras/iris/tree/master/middleware/jwt) middleware based on [square/go-jose](https://github.com/square/go-jose) featured with optional encryption to set claims with sensitive data when necessary.
|
||||
|
||||
- New `iris.RouteOverlap` route registration rule. `Party.SetRegisterRule(iris.RouteOverlap)` to allow overlapping across multiple routes for the same request subdomain, method, path. See [1536#issuecomment-643719922](https://github.com/kataras/iris/issues/1536#issuecomment-643719922).
|
||||
- New `iris.RouteOverlap` route registration rule. `Party.SetRegisterRule(iris.RouteOverlap)` to allow overlapping across multiple routes for the same request subdomain, method, path. See [1536#issuecomment-643719922](https://github.com/kataras/iris/issues/1536#issuecomment-643719922). This allows two or more **MVC Controllers** to listen on the same path based on one or more registered dependencies (see [_examples/mvc/authenticated-controller](https://github.com/kataras/iris/tree/master/_examples/mvc/authenticated-controller)).
|
||||
|
||||
- `Context.ReadForm` now can return an `iris.ErrEmptyForm` instead of `nil` when the new `Configuration.FireEmptyFormError` is true (when `iris.WithEmptyFormError` is set) on missing form body to read from.
|
||||
|
||||
|
|
|
@ -190,6 +190,7 @@
|
|||
* [Regexp](mvc/regexp/main.go)
|
||||
* [Session Controller](mvc/session-controller/main.go)
|
||||
* [Authenticated Controller](mvc/authenticated-controller/main.go)
|
||||
* [Versioned Controller](mvc/versioned-controller/main.go)
|
||||
* [Websocket Controller](mvc/websocket)
|
||||
* [Register Middleware](mvc/middleware)
|
||||
* [gRPC](mvc/grpc-compatible)
|
||||
|
|
52
_examples/mvc/versioned-controller/main.go
Normal file
52
_examples/mvc/versioned-controller/main.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/mvc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := newApp()
|
||||
|
||||
// See main_test.go for request examples.
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func newApp() *iris.Application {
|
||||
app := iris.New()
|
||||
|
||||
dataRouter := app.Party("/data")
|
||||
{
|
||||
m := mvc.New(dataRouter)
|
||||
m.Handle(new(v1Controller), mvc.Version("1")) // 1 or 1.0, 1.0.0 ...
|
||||
m.Handle(new(v2Controller), mvc.Version("2.3")) // 2.3 or 2.3.0
|
||||
m.Handle(new(v3Controller), mvc.Version(">=3, <4")) // 3, 3.x, 3.x.x ...
|
||||
m.Handle(new(noVersionController))
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
type v1Controller struct{}
|
||||
|
||||
func (c *v1Controller) Get() string {
|
||||
return "data (v1.x)"
|
||||
}
|
||||
|
||||
type v2Controller struct{}
|
||||
|
||||
func (c *v2Controller) Get() string {
|
||||
return "data (v2.x)"
|
||||
}
|
||||
|
||||
type v3Controller struct{}
|
||||
|
||||
func (c *v3Controller) Get() string {
|
||||
return "data (v3.x)"
|
||||
}
|
||||
|
||||
type noVersionController struct{}
|
||||
|
||||
func (c *noVersionController) Get() string {
|
||||
return "data"
|
||||
}
|
26
_examples/mvc/versioned-controller/main_test.go
Normal file
26
_examples/mvc/versioned-controller/main_test.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/httptest"
|
||||
"github.com/kataras/iris/v12/versioning"
|
||||
)
|
||||
|
||||
func TestVersionedController(t *testing.T) {
|
||||
app := newApp()
|
||||
|
||||
e := httptest.New(t, app)
|
||||
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("data (v1.x)")
|
||||
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "2.3.0").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("data (v2.x)")
|
||||
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "3.1").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("data (v3.x)")
|
||||
// Test invalid version or no version at all.
|
||||
e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "4").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("data")
|
||||
e.GET("/data").Expect().
|
||||
Status(iris.StatusOK).Body().Equal("data")
|
||||
}
|
|
@ -14,14 +14,14 @@ func TestSessionsEncodeDecode(t *testing.T) {
|
|||
es := e.GET("/set").Expect()
|
||||
es.Status(iris.StatusOK)
|
||||
es.Cookies().NotEmpty()
|
||||
es.Body().Equal("All ok session set to: iris")
|
||||
es.Body().Equal("All ok session set to: iris [isNew=true]")
|
||||
|
||||
e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: iris")
|
||||
// delete and re-get
|
||||
e.GET("/delete").Expect().Status(iris.StatusOK)
|
||||
e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: ")
|
||||
// set, clear and re-get
|
||||
e.GET("/set").Expect().Body().Equal("All ok session set to: iris")
|
||||
e.GET("/set").Expect().Body().Equal("All ok session set to: iris [isNew=false]")
|
||||
e.GET("/clear").Expect().Status(iris.StatusOK)
|
||||
e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal("The name on the /set was: ")
|
||||
}
|
||||
|
|
|
@ -283,3 +283,24 @@ func NewConditionalHandler(filter Filter, handlers ...Handler) Handler {
|
|||
ctx.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// JoinHandlers returns a copy of "h1" and "h2" Handlers slice joined as one slice of Handlers.
|
||||
func JoinHandlers(h1 Handlers, h2 Handlers) Handlers {
|
||||
if len(h1) == 0 {
|
||||
return h2
|
||||
}
|
||||
|
||||
if len(h2) == 0 {
|
||||
return h1
|
||||
}
|
||||
|
||||
nowLen := len(h1)
|
||||
totalLen := nowLen + len(h2)
|
||||
// create a new slice of Handlers in order to merge the "h1" and "h2"
|
||||
newHandlers := make(Handlers, totalLen)
|
||||
// copy the already Handlers to the just created
|
||||
copy(newHandlers, h1)
|
||||
// start from there we finish, and store the new Handlers too
|
||||
copy(newHandlers[nowLen:], h2)
|
||||
return newHandlers
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ func (repo *repository) register(route *Route, rule RouteRegisterRule) (*Route,
|
|||
|
||||
var defaultOverlapFilter = func(ctx context.Context) bool {
|
||||
if ctx.IsStopped() {
|
||||
// It's stopped and the response can be overriden by a new handler.
|
||||
// It's stopped and the response can be overridden by a new handler.
|
||||
rs, ok := ctx.ResponseWriter().(context.ResponseWriterReseter)
|
||||
return ok && rs.Reset()
|
||||
}
|
||||
|
@ -509,8 +509,8 @@ func (api *APIBuilder) createRoutes(errorCode int, methods []string, relativePat
|
|||
)
|
||||
|
||||
if errorCode == 0 {
|
||||
beginHandlers = joinHandlers(api.middleware, beginHandlers)
|
||||
doneHandlers = joinHandlers(api.doneHandlers, doneHandlers)
|
||||
beginHandlers = context.JoinHandlers(api.middleware, beginHandlers)
|
||||
doneHandlers = context.JoinHandlers(api.doneHandlers, doneHandlers)
|
||||
}
|
||||
|
||||
mainHandlers := context.Handlers(handlers)
|
||||
|
@ -528,9 +528,9 @@ func (api *APIBuilder) createRoutes(errorCode int, methods []string, relativePat
|
|||
|
||||
// global begin handlers -> middleware that are registered before route registration
|
||||
// -> handlers that are passed to this Handle function.
|
||||
routeHandlers := joinHandlers(beginHandlers, mainHandlers)
|
||||
routeHandlers := context.JoinHandlers(beginHandlers, mainHandlers)
|
||||
// -> done handlers
|
||||
routeHandlers = joinHandlers(routeHandlers, doneHandlers)
|
||||
routeHandlers = context.JoinHandlers(routeHandlers, doneHandlers)
|
||||
|
||||
// here we separate the subdomain and relative path
|
||||
subdomain, path := splitSubdomainAndPath(fullpath)
|
||||
|
@ -618,7 +618,7 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
|
|||
|
||||
fullpath := parentPath + relativePath
|
||||
// append the parent's + child's handlers
|
||||
middleware := joinHandlers(api.middleware, handlers)
|
||||
middleware := context.JoinHandlers(api.middleware, handlers)
|
||||
|
||||
// the allow methods per party and its children.
|
||||
allowMethods := make([]string, len(api.allowMethods))
|
||||
|
@ -1060,19 +1060,6 @@ func (api *APIBuilder) Layout(tmplLayoutFile string) Party {
|
|||
return api
|
||||
}
|
||||
|
||||
// joinHandlers uses to create a copy of all Handlers and return them in order to use inside the node
|
||||
func joinHandlers(h1 context.Handlers, h2 context.Handlers) context.Handlers {
|
||||
nowLen := len(h1)
|
||||
totalLen := nowLen + len(h2)
|
||||
// create a new slice of Handlers in order to merge the "h1" and "h2"
|
||||
newHandlers := make(context.Handlers, totalLen)
|
||||
// copy the already Handlers to the just created
|
||||
copy(newHandlers, h1)
|
||||
// start from there we finish, and store the new Handlers too
|
||||
copy(newHandlers[nowLen:], h2)
|
||||
return newHandlers
|
||||
}
|
||||
|
||||
// https://golang.org/doc/go1.9#callersframes
|
||||
func getCaller() (string, int) {
|
||||
var pcs [32]uintptr
|
||||
|
|
|
@ -271,6 +271,7 @@ func dispatchFuncResult(ctx context.Context, values []reflect.Value, handler Res
|
|||
contentType = value
|
||||
} else {
|
||||
// otherwise is content
|
||||
contentType = context.ContentTextHeaderValue
|
||||
content = []byte(value)
|
||||
}
|
||||
|
||||
|
|
|
@ -78,6 +78,12 @@ type ControllerActivator struct {
|
|||
// End-devs can change some properties of the *Route on the `BeforeActivator` by using the
|
||||
// `GetRoute/GetRoutes(functionName)`.
|
||||
routes map[string][]*router.Route
|
||||
// BeginHandlers is a slice of middleware for this controller.
|
||||
// These handlers will be prependend to each one of
|
||||
// the route that this controller will register(Handle/HandleMany/struct methods)
|
||||
// to the targeted Party.
|
||||
// Look the `Use` method too.
|
||||
BeginHandlers context.Handlers
|
||||
|
||||
// true if this controller listens and serves to websocket events.
|
||||
servesWebsocket bool
|
||||
|
@ -199,6 +205,15 @@ func (c *ControllerActivator) GetRoutes(methodName string) []*router.Route {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Use registers a middleware for this Controller.
|
||||
// It appends one or more handlers to the `BeginHandlers`.
|
||||
// It's like the `Party.Use` but specifically
|
||||
// for the routes that this controller will register to the targeted `Party`.
|
||||
func (c *ControllerActivator) Use(handlers ...context.Handler) *ControllerActivator {
|
||||
c.BeginHandlers = append(c.BeginHandlers, handlers...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Singleton returns new if all incoming clients' requests
|
||||
// have the same controller instance.
|
||||
// This is done automatically by iris to reduce the creation
|
||||
|
@ -343,6 +358,7 @@ func (c *ControllerActivator) handleMany(method, path, funcName string, override
|
|||
}
|
||||
|
||||
handler := c.handlerOf(path, funcName)
|
||||
middleware = context.JoinHandlers(c.BeginHandlers, middleware)
|
||||
|
||||
// register the handler now.
|
||||
routes := c.app.Router.HandleMany(method, path, append(middleware, handler)...)
|
||||
|
|
|
@ -703,6 +703,31 @@ func TestControllerOverlapping(t *testing.T) {
|
|||
m.Handle(new(publicController))
|
||||
|
||||
e := httptest.New(t, app)
|
||||
e.GET("/").WithQuery("name", "kataras").Expect().Status(httptest.StatusOK).JSON().Equal(iris.Map{"id": 1})
|
||||
e.GET("/").Expect().Status(httptest.StatusOK).JSON().Equal(iris.Map{"data": "things"})
|
||||
e.GET("/").WithQuery("name", "kataras").Expect().Status(httptest.StatusOK).
|
||||
JSON().Equal(iris.Map{"id": 1})
|
||||
e.GET("/").Expect().Status(httptest.StatusOK).
|
||||
JSON().Equal(iris.Map{"data": "things"})
|
||||
}
|
||||
|
||||
type testControllerMethodHandlerBindStruct struct{}
|
||||
|
||||
type queryData struct {
|
||||
Name string `json:"name" url:"name"`
|
||||
}
|
||||
|
||||
func (*testControllerMethodHandlerBindStruct) Any(data queryData) queryData {
|
||||
return data
|
||||
}
|
||||
|
||||
func TestControllerMethodHandlerBindStruct(t *testing.T) {
|
||||
app := iris.New()
|
||||
|
||||
New(app).Handle(new(testControllerMethodHandlerBindStruct))
|
||||
|
||||
data := iris.Map{"name": "kataras"}
|
||||
|
||||
e := httptest.New(t, app)
|
||||
e.GET("/").WithQueryObject(data).Expect().Status(httptest.StatusOK).JSON().Equal(data)
|
||||
e.PATCH("/").WithJSON(data).Expect().Status(httptest.StatusOK).JSON().Equal(data)
|
||||
// more tests inside the hero package itself.
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ type GRPC struct {
|
|||
Strict bool
|
||||
}
|
||||
|
||||
var _ Option = GRPC{}
|
||||
|
||||
// Apply parses the controller's methods and registers gRPC handlers to the application.
|
||||
func (g GRPC) Apply(c *ControllerActivator) {
|
||||
defer c.Activated()
|
||||
|
|
26
mvc/mvc.go
26
mvc/mvc.go
|
@ -146,13 +146,25 @@ func (app *Application) Register(dependencies ...interface{}) *Application {
|
|||
return app
|
||||
}
|
||||
|
||||
// Option is an interface which does contain a single `Apply` method that accepts
|
||||
// a `ControllerActivator`. It can be passed on `Application.Handle` method to
|
||||
// mdoify the behavior right after the `BeforeActivation` state.
|
||||
//
|
||||
// See `GRPC` package-level structure too.
|
||||
type Option interface {
|
||||
Apply(*ControllerActivator)
|
||||
type (
|
||||
// Option is an interface which does contain a single `Apply` method that accepts
|
||||
// a `ControllerActivator`. It can be passed on `Application.Handle` method to
|
||||
// mdoify the behavior right after the `BeforeActivation` state.
|
||||
//
|
||||
// See `GRPC` package-level structure
|
||||
// and `Version` package-level function too.
|
||||
Option interface {
|
||||
Apply(*ControllerActivator)
|
||||
}
|
||||
|
||||
// OptionFunc is the functional type of `Option`.
|
||||
// Read `Option` docs.
|
||||
OptionFunc func(*ControllerActivator)
|
||||
)
|
||||
|
||||
// Apply completes the `Option` interface.
|
||||
func (opt OptionFunc) Apply(c *ControllerActivator) {
|
||||
opt(c)
|
||||
}
|
||||
|
||||
// Handle serves a controller for the current mvc application's Router.
|
||||
|
|
38
mvc/versioning.go
Normal file
38
mvc/versioning.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package mvc
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/v12/context"
|
||||
"github.com/kataras/iris/v12/core/router"
|
||||
"github.com/kataras/iris/v12/versioning"
|
||||
)
|
||||
|
||||
// Version returns a valid `Option` that can be passed to the `Application.Handle` method.
|
||||
// It requires a specific "version" constraint for a Controller,
|
||||
// e.g. ">1, <=2" or just "1".
|
||||
//
|
||||
//
|
||||
// Usage:
|
||||
// m := mvc.New(dataRouter)
|
||||
// m.Handle(new(v1Controller), mvc.Version("1"))
|
||||
// m.Handle(new(v2Controller), mvc.Version("2.3"))
|
||||
// m.Handle(new(v3Controller), mvc.Version(">=3, <4"))
|
||||
// m.Handle(new(noVersionController))
|
||||
//
|
||||
// See the `versioning` package's documentation for more information on
|
||||
// how the version is extracted from incoming requests.
|
||||
//
|
||||
// Note that this Option will set the route register rule to `RouteOverlap`.
|
||||
func Version(version string) OptionFunc {
|
||||
return func(c *ControllerActivator) {
|
||||
c.Router().SetRegisterRule(router.RouteOverlap) // required for this feature.
|
||||
|
||||
c.Use(func(ctx context.Context) {
|
||||
if !versioning.Match(ctx, version) {
|
||||
ctx.StopExecution()
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Next()
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user