Browse Source

implement #1536 with (SetRegisterRule(iris.RouteOverlap))

Former-commit-id: 2b5523ff3e
tags/v0.0.1
Gerasimos (Makis) Maropoulos 1 year ago
parent
commit
ed5964716b
16 changed files with 210 additions and 24 deletions
  1. +1
    -1
      .github/ISSUE_TEMPLATE.md
  2. +1
    -1
      .github/PULL_REQUEST_TEMPLATE.md
  3. +2
    -2
      .github/workflows/go.yml
  4. +15
    -0
      _examples/mvc/authenticated-controller/main.go
  5. +0
    -1
      context/context.go
  6. +21
    -0
      context/gzip_response_writer.go
  7. +6
    -2
      context/response_recorder.go
  8. +45
    -0
      context/response_writer.go
  9. +7
    -12
      core/handlerconv/from_std.go
  10. +43
    -1
      core/router/api_builder.go
  11. +5
    -1
      core/router/party.go
  12. +7
    -0
      core/router/route.go
  13. +51
    -0
      core/router/route_register_rule_test.go
  14. +1
    -1
      doc.go
  15. +2
    -2
      go.mod
  16. +3
    -0
      iris.go

+ 1
- 1
.github/ISSUE_TEMPLATE.md View File

@@ -8,4 +8,4 @@ Love iris? Please consider supporting the project:
👉 https://iris-go.com/donate
Care to be part of a larger community? Fill our user experience form:
👉 https://goo.gl/forms/lnRbVgA6ICTkPyk02
👉 https://goo.gl/forms/lnRbVgA6ICTkPyk02

+ 1
- 1
.github/PULL_REQUEST_TEMPLATE.md View File

@@ -2,4 +2,4 @@
Read how you can [contribute to the project](https://github.com/kataras/iris/blob/master/CONTRIBUTING.md).
> Please attach an [issue](https://github.com/kataras/iris/issues) link which your PR solves otherwise your work may be rejected.
> Please attach an [issue](https://github.com/kataras/iris/issues) link which your PR solves otherwise your work may be rejected.

+ 2
- 2
.github/workflows/go.yml View File

@@ -17,10 +17,10 @@ jobs:
restore-keys: |
${{ runner.os }}-go-

- name: Set up Go 1.13
- name: Set up Go 1.14
uses: actions/setup-go@v1
with:
go-version: 1.13
go-version: 1.14
id: go

- name: Check out code into the Go module directory


+ 15
- 0
_examples/mvc/authenticated-controller/main.go View File

@@ -26,6 +26,11 @@ func main() {

userRouter := app.Party("/user")
{
// Use that in order to be able to register a route twice,
// last one will be executed if the previous route's handler(s) stopped and the response can be reset-ed.
// See core/router/route_register_rule_test.go#TestRegisterRuleOverlap.
userRouter.SetRegisterRule(iris.RouteOverlap)

// Initialize a new MVC application on top of the "userRouter".
userApp := mvc.New(userRouter)
// Register Dependencies.
@@ -34,6 +39,7 @@ func main() {
// Register Controllers.
userApp.Handle(new(MeController))
userApp.Handle(new(UserController))
userApp.Handle(new(UnauthenticatedUserController))
}

// Open a client, e.g. Postman and visit the below endpoints.
@@ -61,6 +67,15 @@ func authDependency(ctx iris.Context, session *sessions.Session) Authenticated {
return Authenticated(userID)
}

// UnauthenticatedUserController serves the "public" Unauthorized User API.
type UnauthenticatedUserController struct{}

// GetMe registers a route that will be executed when authentication is not passed
// (see UserController.GetMe) too.
func (c *UnauthenticatedUserController) GetMe() string {
return `custom action to redirect on authentication page`
}

// UserController serves the "public" User API.
type UserController struct {
Session *sessions.Session


+ 0
- 1
context/context.go View File

@@ -169,7 +169,6 @@ type Context interface {
SetHandlers(Handlers)
// Handlers keeps tracking of the current handlers.
Handlers() Handlers

// HandlerIndex sets the current index of the
// current context's handlers chain.
// If n < 0 or the current handlers length is 0 then it just returns the


+ 21
- 0
context/gzip_response_writer.go View File

@@ -192,6 +192,7 @@ func (w *GzipResponseWriter) Body() []byte {
}

// ResetBody resets the response body.
// Implements the `ResponseWriterBodyReseter`.
func (w *GzipResponseWriter) ResetBody() {
w.chunks = w.chunks[0:0]
}
@@ -202,6 +203,26 @@ func (w *GzipResponseWriter) Disable() {
w.disabled = true
}

// Reset disables the gzip content writer, clears headers, sets the status code to 200
// and clears the cached body.
//
// Implements the `ResponseWriterReseter`.
func (w *GzipResponseWriter) Reset() bool {
// disable gzip content writer.
w.Disable()
// clear headers.
h := w.ResponseWriter.Header()
for k := range h {
h[k] = nil
}
// restore status code.
w.WriteHeader(defaultStatusCode)
// reset body.
w.ResetBody()

return true
}

// FlushResponse validates the response headers in order to be compatible with the gzip written data
// and writes the data to the underline ResponseWriter.
func (w *GzipResponseWriter) FlushResponse() {


+ 6
- 2
context/response_recorder.go View File

@@ -139,11 +139,15 @@ func (w *ResponseRecorder) ClearHeaders() {
}
}

// Reset resets the response body, headers and the status code header.
func (w *ResponseRecorder) Reset() {
// Reset clears headers, sets the status code to 200
// and clears the cached body.
//
// Implements the `ResponseWriterReseter`.
func (w *ResponseRecorder) Reset() bool {
w.ClearHeaders()
w.WriteHeader(defaultStatusCode)
w.ResetBody()
return true
}

// FlushResponse the full body, headers and status code to the underline response writer


+ 45
- 0
context/response_writer.go View File

@@ -105,6 +105,32 @@ type ResponseWriter interface {
CloseNotifier() (http.CloseNotifier, bool)
}

// ResponseWriterBodyReseter can be implemented by
// response writers that supports response body overriding
// (e.g. recorder and compressed).
type ResponseWriterBodyReseter interface {
// ResetBody should reset the body and reports back if it could reset successfully.
ResetBody()
}

// ResponseWriterDisabler can be implemented
// by response writers that can be disabled and restored to their previous state
// (e.g. compressed).
type ResponseWriterDisabler interface {
// Disable should disable this type of response writer and fallback to the default one.
Disable()
}

// ResponseWriterReseter can be implemented
// by response writers that can clear the whole response
// so a new handler can write into this from the beginning.
// E.g. recorder, compressed (full) and common writer (status code and headers).
type ResponseWriterReseter interface {
// Reset should reset the whole response and reports
// whether it could reset successfully.
Reset() bool
}

// +------------------------------------------------------------+
// | Response Writer Implementation |
// +------------------------------------------------------------+
@@ -167,6 +193,25 @@ func (w *responseWriter) EndResponse() {
releaseResponseWriter(w)
}

// Reset clears headers, sets the status code to 200
// and clears the cached body.
//
// Implements the `ResponseWriterReseter`.
func (w *responseWriter) Reset() bool {
if w.written > 0 {
return false // if already written we can't reset this type of response writer.
}

h := w.Header()
for k := range h {
h[k] = nil
}

w.written = NoWritten
w.statusCode = defaultStatusCode
return true
}

// SetWritten sets manually a value for written, it can be
// NoWritten(-1) or StatusCodeWritten(0), > 0 means body length which is useless here.
func (w *responseWriter) SetWritten(n int) {


+ 7
- 12
core/handlerconv/from_std.go View File

@@ -18,21 +18,19 @@ func FromStd(handler interface{}) context.Handler {
case context.Handler:
{
//
// it's already a iris handler
// it's already an Iris Handler
//
return h
}

case http.Handler:
//
// handlerFunc.ServeHTTP(w,r)
//
{
//
// handlerFunc.ServeHTTP(w,r)
//
return func(ctx context.Context) {
h.ServeHTTP(ctx.ResponseWriter(), ctx.Request())
}
}

case func(http.ResponseWriter, *http.Request):
{
//
@@ -40,7 +38,6 @@ func FromStd(handler interface{}) context.Handler {
//
return FromStd(http.HandlerFunc(h))
}

case func(http.ResponseWriter, *http.Request, http.HandlerFunc):
{
//
@@ -48,7 +45,6 @@ func FromStd(handler interface{}) context.Handler {
//
return FromStdWithNext(h)
}

default:
{
//
@@ -60,9 +56,8 @@ func FromStd(handler interface{}) context.Handler {
- func(w http.ResponseWriter, r *http.Request)
- func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)
---------------------------------------------------------------------
It seems to be a %T points to: %v`, handler, handler))
It seems to be a %T points to: %v`, handler, handler))
}

}
}

@@ -70,10 +65,10 @@ func FromStd(handler interface{}) context.Handler {
// compatible context.Handler wrapper.
func FromStdWithNext(h func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)) context.Handler {
return func(ctx context.Context) {
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next := func(w http.ResponseWriter, r *http.Request) {
ctx.ResetRequest(r)
ctx.Next()
})
}

h(ctx.ResponseWriter(), ctx.Request(), next)
}


+ 43
- 1
core/router/api_builder.go View File

@@ -91,6 +91,9 @@ func (repo *repository) register(route *Route, rule RouteRegisterRule) (*Route,
return r, nil
} else if rule == RouteError {
return nil, fmt.Errorf("new route: %s conflicts with an already registered one: %s route", route.String(), r.String())
} else if rule == RouteOverlap {
overlapRoute(r, route)
return route, nil
} else {
// replace existing with the latest one, the default behavior.
repo.routes = append(repo.routes[:i], repo.routes[i+1:]...)
@@ -113,6 +116,38 @@ func (repo *repository) register(route *Route, rule RouteRegisterRule) (*Route,
return route, nil
}

var defaultOverlapFilter = func(ctx context.Context) bool {
if ctx.IsStopped() {
// It's stopped and the response can be overriden by a new handler.
rs, ok := ctx.ResponseWriter().(context.ResponseWriterReseter)
return ok && rs.Reset()
}

// It's not stopped, all OK no need to execute the alternative route.
return false
}

func overlapRoute(r *Route, next *Route) {
next.BuildHandlers()
nextHandlers := next.Handlers[0:]

decisionHandler := func(ctx context.Context) {
ctx.Next()

if !defaultOverlapFilter(ctx) {
return
}

ctx.SetCurrentRoute(next.ReadOnly)
ctx.HandlerIndex(0)
ctx.Do(nextHandlers)
}

// NOTE(@kataras): Any UseGlobal call will prepend to this, if they are
// in the same Party then it's expected, otherwise not.
r.beginHandlers = append(context.Handlers{decisionHandler}, r.beginHandlers...)
}

// APIBuilder the visible API for constructing the router
// and child routers.
type APIBuilder struct {
@@ -261,10 +296,17 @@ const (
// RouteError log when a route already exists, shown after the `Build` state,
// server never starts.
RouteError
// RouteOverlap will overlap the new route to the previous one.
// If the route stopped and its response can be reset then the new route will be execute.
RouteOverlap
)

// SetRegisterRule sets a `RouteRegisterRule` for this Party and its children.
// Available values are: RouteOverride (the default one), RouteSkip and RouteError.
// Available values are:
// * RouteOverride (the default one)
// * RouteSkip
// * RouteError
// * RouteOverlap.
func (api *APIBuilder) SetRegisterRule(rule RouteRegisterRule) Party {
api.routeRegisterRule = rule
return api


+ 5
- 1
core/router/party.go View File

@@ -116,7 +116,11 @@ type Party interface {
// Example: https://github.com/kataras/iris/tree/master/_examples/mvc/middleware/without-ctx-next
SetExecutionRules(executionRules ExecutionRules) Party
// SetRegisterRule sets a `RouteRegisterRule` for this Party and its children.
// Available values are: RouteOverride (the default one), RouteSkip and RouteError.
// Available values are:
// * RouteOverride (the default one)
// * RouteSkip
// * RouteError
// * RouteOverlap.
SetRegisterRule(rule RouteRegisterRule) Party

// Handle registers a route to the server's router.


+ 7
- 0
core/router/route.go View File

@@ -66,6 +66,9 @@ type Route struct {

// ReadOnly is the read-only structure of the Route.
ReadOnly context.RouteReadOnly

// OnBuild runs right before BuildHandlers.
OnBuild func(r *Route)
}

// NewRoute returns a new route based on its method,
@@ -186,6 +189,10 @@ func (r *Route) RestoreStatus() bool {
// at the `Application#Build` state. Do not call it manually, unless
// you were defined your own request mux handler.
func (r *Route) BuildHandlers() {
if r.OnBuild != nil {
r.OnBuild(r)
}

if len(r.beginHandlers) > 0 {
r.Handlers = append(r.beginHandlers, r.Handlers...)
r.beginHandlers = r.beginHandlers[0:0]


+ 51
- 0
core/router/route_register_rule_test.go View File

@@ -58,3 +58,54 @@ func testRegisterRule(e *httptest.Expect, expectedGetBody string) {
}
}
}

func TestRegisterRuleOverlap(t *testing.T) {
app := iris.New()
// TODO(@kataras) the overlapping does not work per-party yet,
// it just checks compares from the total app's routes (which is the best possible action to do
// because MVC applications can be separated into different parties too?).
usersRouter := app.Party("/users")
usersRouter.SetRegisterRule(iris.RouteOverlap)

// second handler will be executed, status will be reset-ed as well,
// stop without data written.
usersRouter.Get("/", func(ctx iris.Context) {
ctx.StopWithStatus(iris.StatusUnauthorized)
})
usersRouter.Get("/", func(ctx iris.Context) {
ctx.WriteString("data")
})

// first handler will be executed, no stop called.
usersRouter.Get("/p1", func(ctx iris.Context) {
ctx.StatusCode(iris.StatusUnauthorized)
})
usersRouter.Get("/p1", func(ctx iris.Context) {
ctx.WriteString("not written")
})

// first handler will be executed, stop but with data sent on default writer
// (body sent cannot be reset-ed here).
usersRouter.Get("/p2", func(ctx iris.Context) {
ctx.StopWithText(iris.StatusUnauthorized, "no access")
})
usersRouter.Get("/p2", func(ctx iris.Context) {
ctx.WriteString("not written")
})

// second will be executed, response can be reset-ed on recording.
usersRouter.Get("/p3", func(ctx iris.Context) {
ctx.Record()
ctx.StopWithText(iris.StatusUnauthorized, "no access")
})
usersRouter.Get("/p3", func(ctx iris.Context) {
ctx.WriteString("p3 data")
})

e := httptest.New(t, app)

e.GET("/users").Expect().Status(httptest.StatusOK).Body().Equal("data")
e.GET("/users/p1").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
e.GET("/users/p2").Expect().Status(httptest.StatusUnauthorized).Body().Equal("no access")
e.GET("/users/p3").Expect().Status(httptest.StatusOK).Body().Equal("p3 data")
}

+ 1
- 1
doc.go View File

@@ -38,7 +38,7 @@ Source code and other details for the project are available at GitHub:

Current Version

12.1.8
12.2.0

Installation



+ 2
- 2
go.mod View File

@@ -19,7 +19,7 @@ require (
github.com/iris-contrib/jade v1.1.4
github.com/iris-contrib/pongo2 v0.0.1
github.com/iris-contrib/schema v0.0.1
github.com/json-iterator/go v1.1.9
github.com/json-iterator/go v1.1.10
github.com/kataras/golog v0.0.18
github.com/kataras/neffos v0.0.16
github.com/kataras/pio v0.0.8
@@ -34,7 +34,7 @@ require (
github.com/vmihailenco/msgpack/v5 v5.0.0-alpha.2
go.etcd.io/bbolt v1.3.4
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7
golang.org/x/text v0.3.2
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1
gopkg.in/ini.v1 v1.57.0


+ 3
- 0
iris.go View File

@@ -617,6 +617,9 @@ const (
// RouteError log when a route already exists, shown after the `Build` state,
// server never starts.
RouteError = router.RouteError
// RouteOverlap will overlap the new route to the previous one.
// If the route stopped and its response can be reset then the new route will be execute.
RouteOverlap = router.RouteOverlap
)

// Contains the enum values of the `Context.GetReferrer()` method,


Loading…
Cancel
Save