mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 02:31:04 +01:00
❤️ awesome and unique features for end-developers are coming...
total refactor of the hero and mvc packages, see README#Next (it's not completed yet) Former-commit-id: b85ae99cbfe5965ba919c1e15cf4989e787982c0
This commit is contained in:
parent
027eb5d6da
commit
5fc24812bc
63
HISTORY.md
63
HISTORY.md
|
@ -21,6 +21,69 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene
|
||||||
|
|
||||||
**How to upgrade**: Open your command-line and execute this command: `go get github.com/kataras/iris/v12@latest`.
|
**How to upgrade**: Open your command-line and execute this command: `go get github.com/kataras/iris/v12@latest`.
|
||||||
|
|
||||||
|
# Next
|
||||||
|
|
||||||
|
This release introduces new features and some breaking changes inside the `mvc` and `hero` packages.
|
||||||
|
The codebase for dependency injection has been simplified a lot (fewer LOCs and easier to read and follow up).
|
||||||
|
|
||||||
|
Before this release the `iris.Context` was the only one dependency has been automatically binded to the controller's fields or handler's inputs, now the standard `"context"` package's `Context` is also automatically binded and all structs that are not mapping to a registered dependency are now automatically resolved to `payload` XML, YAML, Query, Form and JSON dependencies based on the request's `Content-Type` header (defaults to JSON if client didn't specified a content-type).
|
||||||
|
|
||||||
|
The new release contains a fresh new and awesome feature....**a function dependency can accept previous registered dependencies and update or return a new value of any type**.
|
||||||
|
|
||||||
|
The new implementation is **faster** on both design and serve-time.
|
||||||
|
|
||||||
|
The most common scenario from a route to handle is to:
|
||||||
|
- accept one or more path parameters and request data, a payload
|
||||||
|
- send back a response, a payload (JSON, XML,...)
|
||||||
|
|
||||||
|
The new Iris Dependency Injection feature is about **[33.2% faster](_benchmarks/_internal/README.md#dependency-injection)** than its predecessor on the above case. This drops down even more the performance cost between native handlers and dynamic handlers with dependencies. This reason itself brings us, with safety and performance-wise, to the new `Party.HandleFunc(method, relativePath string, handlersFn ...interface{}) *Route` and `Party.RegisterDependency` method.
|
||||||
|
|
||||||
|
Look how clean your codebase can be when using Iris':
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/kataras/iris/v12"
|
||||||
|
|
||||||
|
type (
|
||||||
|
testInput struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
testOutput struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func handler(id int, in testInput) testOutput {
|
||||||
|
return testOutput{
|
||||||
|
ID: id,
|
||||||
|
Name: in.Email,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := iris.New()
|
||||||
|
app.HandleFunc(iris.MethodPost, "/{id:int}", handler)
|
||||||
|
app.Listen(":5000", iris.WithOptimizations)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Your eyes don't lie you. You read well, no `ctx.ReadJSON(&v)` and `ctx.JSON(send)` neither `error` handling are presented. It is a huge relief but don't worry you can still control everything if you ever need, even errors from dependencies. Any error may occur from request-scoped dependencies or your own handler is dispatched through `Party.GetContainer().GetErrorHandler` which defaults to the `hero.DefaultErrorHandler` which sends a `400 Bad Request` response with the error's text as its body contents. If you want to handle `testInput` otherwise then just add a `Party.RegisterDependency(func(ctx iris.Context) testInput {...})` and you are ready to go.
|
||||||
|
|
||||||
|
New Context Methods:
|
||||||
|
|
||||||
|
- `context.Defer(Handler)` works like `Party.Done` but for the request life-cycle.
|
||||||
|
- `context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(context)`
|
||||||
|
- `context.Controller() reflect.Value` returns the current MVC Controller value (when fired from inside a controller's method).
|
||||||
|
|
||||||
|
Breaking Changes:
|
||||||
|
|
||||||
|
- `var mvc.AutoBinding` removed as the default behavior now resolves such dependencies automatically (see [[FEATURE REQUEST] MVC serving gRPC-compatible controller](https://github.com/kataras/iris/issues/1449))
|
||||||
|
- `mvc#Application.SortByNumMethods()` removed as the default behavior now binds the "thinnest" empty `interface{}` automatically (see [MVC: service injecting fails](https://github.com/kataras/iris/issues/1343))
|
||||||
|
- `mvc#BeforeActivation.Dependencies().Add` should be replaced with `mvc#BeforeActivation.Dependencies().Register` instead.
|
||||||
|
|
||||||
# Su, 16 February 2020 | v12.1.8
|
# Su, 16 February 2020 | v12.1.8
|
||||||
|
|
||||||
New Features:
|
New Features:
|
||||||
|
|
32
_benchmarks/_internal/README.md
Normal file
32
_benchmarks/_internal/README.md
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Benchmarks (internal)
|
||||||
|
|
||||||
|
Internal selected benchmarks between modified features across different versions.
|
||||||
|
|
||||||
|
* These benchmarks SHOULD run locally with a break of ~2 minutes between stress tests at the same machine, power plan and Turbo Boost set to ON
|
||||||
|
* The system's `GOPATH` environment variable SHOULD match the [vNext/go.mod replace directive](vNext/go.mod#L5) one
|
||||||
|
* A stress test may ran from a `name_test.go` file to measure _BUILD TIME_
|
||||||
|
* each version executes: `go test -run=NONE --bench=. -count=5 --benchmem > name_test.txt`
|
||||||
|
* the result will be presented through [benchstat](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat) tool
|
||||||
|
* _Or/and_ by firing [bombardier](https://github.com/codesenberg/bombardier/releases/tag/v1.2.4) _HTTP requests_ when it (the test) listens to an address
|
||||||
|
* Each benchmark SHOULD contain a brief explanation of what it does.
|
||||||
|
|
||||||
|
## Dependency Injection
|
||||||
|
|
||||||
|
Measures handler factory time.
|
||||||
|
|
||||||
|
| Name | Ops | Ns/op | B/op | Allocs/op |
|
||||||
|
|---------|:------|:--------|:--------|----|
|
||||||
|
| vNext | 181726 | 6631 | 1544 | 17 |
|
||||||
|
| v12.1.x | 96001 | 12604 | 976 | 26 |
|
||||||
|
|
||||||
|
It accepts a dynamic path parameter and a JSON request. It returns a JSON response. Fires 500000 requests with 125 concurrent connections.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# di.go
|
||||||
|
$ bombardier -c 125 -n 500000 --method="POST" --body-file=./request.json http://localhost:5000/42
|
||||||
|
```
|
||||||
|
|
||||||
|
| Name | Throughput | Reqs/sec | Latency | Time To Complete |
|
||||||
|
|---------|:-----------|:----------|:---------|----------------|
|
||||||
|
| vNext | 46.51MB/s | 160480.74 | 777.33us | 3s |
|
||||||
|
| v12.1.x | 32.43MB/s | 108839.19 | 1.14ms | 4s |
|
1
_benchmarks/_internal/request.json
Normal file
1
_benchmarks/_internal/request.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"email":"my_email"}
|
38
_benchmarks/_internal/v12.1.x/di.go
Normal file
38
_benchmarks/_internal/v12.1.x/di.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/iris/v12/hero"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
testInput struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
testOutput struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func handler(id int, in testInput) testOutput {
|
||||||
|
return testOutput{
|
||||||
|
ID: id,
|
||||||
|
Name: in.Email,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dependency(ctx iris.Context) (in testInput, err error) {
|
||||||
|
err = ctx.ReadJSON(&in)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := iris.New()
|
||||||
|
|
||||||
|
c := hero.New()
|
||||||
|
c.Register(dependency)
|
||||||
|
app.Post("/{id:int}", c.Handler(handler))
|
||||||
|
app.Listen(":5000", iris.WithOptimizations)
|
||||||
|
}
|
15
_benchmarks/_internal/v12.1.x/di_test.go
Normal file
15
_benchmarks/_internal/v12.1.x/di_test.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/hero"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkHero(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
c := hero.New()
|
||||||
|
c.Register(dependency)
|
||||||
|
_ = c.Handler(handler)
|
||||||
|
}
|
||||||
|
}
|
BIN
_benchmarks/_internal/v12.1.x/di_test.txt
Normal file
BIN
_benchmarks/_internal/v12.1.x/di_test.txt
Normal file
Binary file not shown.
8
_benchmarks/_internal/v12.1.x/go.mod
Normal file
8
_benchmarks/_internal/v12.1.x/go.mod
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
module myoldapp
|
||||||
|
|
||||||
|
go 1.14
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/kataras/iris/v12 v12.1.8
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||||
|
)
|
29
_benchmarks/_internal/vNext/di.go
Normal file
29
_benchmarks/_internal/vNext/di.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
testInput struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
testOutput struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func handler(id int, in testInput) testOutput {
|
||||||
|
return testOutput{
|
||||||
|
ID: id,
|
||||||
|
Name: in.Email,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := iris.New()
|
||||||
|
app.HandleFunc(iris.MethodPost, "/{id:int}", handler)
|
||||||
|
app.Listen(":5000", iris.WithOptimizations)
|
||||||
|
}
|
14
_benchmarks/_internal/vNext/di_test.go
Normal file
14
_benchmarks/_internal/vNext/di_test.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/hero"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkHero(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
c := hero.New()
|
||||||
|
_ = c.Handler(handler)
|
||||||
|
}
|
||||||
|
}
|
BIN
_benchmarks/_internal/vNext/di_test.txt
Normal file
BIN
_benchmarks/_internal/vNext/di_test.txt
Normal file
Binary file not shown.
7
_benchmarks/_internal/vNext/go.mod
Normal file
7
_benchmarks/_internal/vNext/go.mod
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module myapp
|
||||||
|
|
||||||
|
go 1.14
|
||||||
|
|
||||||
|
replace github.com/kataras/iris/v12 => C:/mygopath/src/github.com/kataras/iris
|
||||||
|
|
||||||
|
require github.com/kataras/iris/v12 v12.1.8
|
|
@ -12,7 +12,7 @@ func main() {
|
||||||
mvc.New(app.Party("/api/values/{id}")).
|
mvc.New(app.Party("/api/values/{id}")).
|
||||||
Handle(new(controllers.ValuesController))
|
Handle(new(controllers.ValuesController))
|
||||||
|
|
||||||
app.Run(iris.Addr(":5000"))
|
app.Listen(":5000")
|
||||||
}
|
}
|
||||||
|
|
||||||
// +2MB/s faster than the previous implementation, 0.4MB/s difference from the raw handlers.
|
// +2MB/s faster than the previous implementation, 0.4MB/s difference from the raw handlers.
|
||||||
|
|
|
@ -8,5 +8,5 @@ func main() {
|
||||||
ctx.WriteString("value")
|
ctx.WriteString("value")
|
||||||
})
|
})
|
||||||
|
|
||||||
app.Run(iris.Addr(":5000"))
|
app.Listen(":5000")
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// See https://github.com/kataras/iris/issues/1449
|
// See https://github.com/kataras/iris/issues/1449
|
||||||
// for more details but in-short you can convert Iris MVC to gRPC methods by
|
// Iris automatically binds the standard "context" context.Context to `iris.Context.Request().Context()`
|
||||||
// binding the `context.Context` from `iris.Context.Request().Context()` and gRPC input and output data.
|
// and any other structure that is not mapping to a registered dependency
|
||||||
|
// as a payload depends on the request, e.g XML, YAML, Query, Form, JSON.
|
||||||
|
//
|
||||||
|
// Useful to use gRPC services as Iris controllers fast and without wrappers.
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := newApp()
|
app := newApp()
|
||||||
|
@ -24,27 +27,7 @@ func main() {
|
||||||
func newApp() *iris.Application {
|
func newApp() *iris.Application {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
|
||||||
mvc.New(app).
|
mvc.New(app).Handle(&myController{})
|
||||||
// Request-scope binding for context.Context-type controller's method or field.
|
|
||||||
// (or import github.com/kataras/iris/v12/hero and hero.Register(...))
|
|
||||||
Register(func(ctx iris.Context) context.Context {
|
|
||||||
return ctx.Request().Context()
|
|
||||||
}).
|
|
||||||
// Bind loginRequest.
|
|
||||||
// Register(func(ctx iris.Context) loginRequest {
|
|
||||||
// var req loginRequest
|
|
||||||
// ctx.ReadJSON(&req)
|
|
||||||
// return req
|
|
||||||
// }).
|
|
||||||
// OR
|
|
||||||
// Bind any other structure or pointer to a structure from request's
|
|
||||||
// XML
|
|
||||||
// YAML
|
|
||||||
// Query
|
|
||||||
// Form
|
|
||||||
// JSON (default, if not client's "Content-Type" specified otherwise)
|
|
||||||
Register(mvc.AutoBinding).
|
|
||||||
Handle(&myController{})
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ func main() {
|
||||||
type myController struct{}
|
type myController struct{}
|
||||||
|
|
||||||
func (m *myController) BeforeActivation(b mvc.BeforeActivation) {
|
func (m *myController) BeforeActivation(b mvc.BeforeActivation) {
|
||||||
// b.Dependencies().Add/Remove
|
// b.Dependencies().Register
|
||||||
// b.Router().Use/UseGlobal/Done // and any standard API call you already know
|
// b.Router().Use/UseGlobal/Done // and any standard API call you already know
|
||||||
|
|
||||||
// 1-> Method
|
// 1-> Method
|
||||||
|
|
|
@ -11,16 +11,14 @@ import (
|
||||||
|
|
||||||
// VisitController handles the root route.
|
// VisitController handles the root route.
|
||||||
type VisitController struct {
|
type VisitController struct {
|
||||||
// the current request session,
|
// the current request session, automatically binded.
|
||||||
// its initialization happens by the dependency function that we've added to the `visitApp`.
|
|
||||||
Session *sessions.Session
|
Session *sessions.Session
|
||||||
|
|
||||||
// A time.time which is binded from the MVC,
|
// A time.time which is binded from the MVC application manually.
|
||||||
// order of binded fields doesn't matter.
|
|
||||||
StartTime time.Time
|
StartTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get handles
|
// Get handles index
|
||||||
// Method: GET
|
// Method: GET
|
||||||
// Path: http://localhost:8080
|
// Path: http://localhost:8080
|
||||||
func (c *VisitController) Get() string {
|
func (c *VisitController) Get() string {
|
||||||
|
@ -36,8 +34,9 @@ func (c *VisitController) Get() string {
|
||||||
func newApp() *iris.Application {
|
func newApp() *iris.Application {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
sess := sessions.New(sessions.Config{Cookie: "mysession_cookie_name"})
|
sess := sessions.New(sessions.Config{Cookie: "mysession_cookie_name"})
|
||||||
|
app.Use(sess.Handler())
|
||||||
|
|
||||||
visitApp := mvc.New(app.Party("/"))
|
visitApp := mvc.New(app)
|
||||||
// bind the current *session.Session, which is required, to the `VisitController.Session`
|
// bind the current *session.Session, which is required, to the `VisitController.Session`
|
||||||
// and the time.Now() to the `VisitController.StartTime`.
|
// and the time.Now() to the `VisitController.StartTime`.
|
||||||
visitApp.Register(
|
visitApp.Register(
|
||||||
|
@ -50,7 +49,7 @@ func newApp() *iris.Application {
|
||||||
// If dependencies are registered without field or function's input arguments as
|
// If dependencies are registered without field or function's input arguments as
|
||||||
// consumers then those dependencies are being ignored before the server ran,
|
// consumers then those dependencies are being ignored before the server ran,
|
||||||
// so you can bind many dependecies and use them in different controllers.
|
// so you can bind many dependecies and use them in different controllers.
|
||||||
sess.Start,
|
// sess.Start, // However after version 12.2 sessions are automatically binded.
|
||||||
time.Now(),
|
time.Now(),
|
||||||
)
|
)
|
||||||
visitApp.Handle(new(VisitController))
|
visitApp.Handle(new(VisitController))
|
||||||
|
|
|
@ -35,7 +35,7 @@ type formValue func(string) string
|
||||||
func (c *Controller) BeforeActivation(b mvc.BeforeActivation) {
|
func (c *Controller) BeforeActivation(b mvc.BeforeActivation) {
|
||||||
// bind the context's `FormValue` as well in order to be
|
// bind the context's `FormValue` as well in order to be
|
||||||
// acceptable on the controller or its methods' input arguments (NEW feature as well).
|
// acceptable on the controller or its methods' input arguments (NEW feature as well).
|
||||||
b.Dependencies().Add(func(ctx iris.Context) formValue { return ctx.FormValue })
|
b.Dependencies().Register(func(ctx iris.Context) formValue { return ctx.FormValue })
|
||||||
}
|
}
|
||||||
|
|
||||||
type page struct {
|
type page struct {
|
||||||
|
|
|
@ -25,10 +25,10 @@ type TodoController struct {
|
||||||
func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) {
|
func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) {
|
||||||
// this could be binded to a controller's function input argument
|
// this could be binded to a controller's function input argument
|
||||||
// if any, or struct field if any:
|
// if any, or struct field if any:
|
||||||
b.Dependencies().Add(func(ctx iris.Context) (items []todo.Item) {
|
b.Dependencies().Register(func(ctx iris.Context) (items []todo.Item) {
|
||||||
ctx.ReadJSON(&items)
|
ctx.ReadJSON(&items)
|
||||||
return
|
return
|
||||||
})
|
}) // Note: from Iris v12.2 these type of dependencies are automatically resolved.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get handles the GET: /todos route.
|
// Get handles the GET: /todos route.
|
||||||
|
|
|
@ -91,22 +91,26 @@ func (u UnmarshalerFunc) Unmarshal(data []byte, v interface{}) error {
|
||||||
type Context interface {
|
type Context interface {
|
||||||
// BeginRequest is executing once for each request
|
// BeginRequest is executing once for each request
|
||||||
// it should prepare the (new or acquired from pool) context's fields for the new request.
|
// it should prepare the (new or acquired from pool) context's fields for the new request.
|
||||||
|
// Do NOT call it manually. Framework calls it automatically.
|
||||||
//
|
//
|
||||||
// To follow the iris' flow, developer should:
|
// Resets
|
||||||
// 1. reset handlers to nil
|
// 1. handlers to nil.
|
||||||
// 2. reset values to empty
|
// 2. values to empty.
|
||||||
// 3. reset sessions to nil
|
// 3. the defer function.
|
||||||
// 4. reset response writer to the http.ResponseWriter
|
// 4. response writer to the http.ResponseWriter.
|
||||||
// 5. reset request to the *http.Request
|
// 5. request to the *http.Request.
|
||||||
// and any other optional steps, depends on dev's application type.
|
|
||||||
BeginRequest(http.ResponseWriter, *http.Request)
|
BeginRequest(http.ResponseWriter, *http.Request)
|
||||||
// EndRequest is executing once after a response to the request was sent and this context is useless or released.
|
// EndRequest is executing once after a response to the request was sent and this context is useless or released.
|
||||||
|
// Do NOT call it manually. Framework calls it automatically.
|
||||||
//
|
//
|
||||||
// To follow the iris' flow, developer should:
|
// 1. executes the Defer function (if any).
|
||||||
// 1. flush the response writer's result
|
// 2. flushes the response writer's result or fire any error handler.
|
||||||
// 2. release the response writer
|
// 3. releases the response writer.
|
||||||
// and any other optional steps, depends on dev's application type.
|
|
||||||
EndRequest()
|
EndRequest()
|
||||||
|
// Defer executes a handler on this Context right before the request ends.
|
||||||
|
// The `StopExecution` does not effect the execution of this defer handler.
|
||||||
|
// The "h" runs before `FireErrorCode` (when response status code is not successful).
|
||||||
|
Defer(Handler)
|
||||||
|
|
||||||
// ResponseWriter returns an http.ResponseWriter compatible response writer, as expected.
|
// ResponseWriter returns an http.ResponseWriter compatible response writer, as expected.
|
||||||
ResponseWriter() ResponseWriter
|
ResponseWriter() ResponseWriter
|
||||||
|
@ -998,6 +1002,9 @@ type Context interface {
|
||||||
// ReflectValue caches and returns a []reflect.Value{reflect.ValueOf(ctx)}.
|
// ReflectValue caches and returns a []reflect.Value{reflect.ValueOf(ctx)}.
|
||||||
// It's just a helper to maintain variable inside the context itself.
|
// It's just a helper to maintain variable inside the context itself.
|
||||||
ReflectValue() []reflect.Value
|
ReflectValue() []reflect.Value
|
||||||
|
// Controller returns a reflect Value of the custom Controller from which this handler executed.
|
||||||
|
// It will return a Kind() == reflect.Invalid if the handler was not executed from within a controller.
|
||||||
|
Controller() reflect.Value
|
||||||
|
|
||||||
// Application returns the iris app instance which belongs to this context.
|
// Application returns the iris app instance which belongs to this context.
|
||||||
// Worth to notice that this function returns an interface
|
// Worth to notice that this function returns an interface
|
||||||
|
@ -1065,6 +1072,7 @@ type context struct {
|
||||||
request *http.Request
|
request *http.Request
|
||||||
// the current route's name registered to this request path.
|
// the current route's name registered to this request path.
|
||||||
currentRouteName string
|
currentRouteName string
|
||||||
|
deferFunc Handler
|
||||||
|
|
||||||
// the local key-value storage
|
// the local key-value storage
|
||||||
params RequestParams // url named parameters.
|
params RequestParams // url named parameters.
|
||||||
|
@ -1089,20 +1097,21 @@ func NewContext(app Application) Context {
|
||||||
|
|
||||||
// BeginRequest is executing once for each request
|
// BeginRequest is executing once for each request
|
||||||
// it should prepare the (new or acquired from pool) context's fields for the new request.
|
// it should prepare the (new or acquired from pool) context's fields for the new request.
|
||||||
|
// Do NOT call it manually. Framework calls it automatically.
|
||||||
//
|
//
|
||||||
// To follow the iris' flow, developer should:
|
// Resets
|
||||||
// 1. reset handlers to nil
|
// 1. handlers to nil.
|
||||||
// 2. reset store to empty
|
// 2. values to empty.
|
||||||
// 3. reset sessions to nil
|
// 3. the defer function.
|
||||||
// 4. reset response writer to the http.ResponseWriter
|
// 4. response writer to the http.ResponseWriter.
|
||||||
// 5. reset request to the *http.Request
|
// 5. request to the *http.Request.
|
||||||
// and any other optional steps, depends on dev's application type.
|
|
||||||
func (ctx *context) BeginRequest(w http.ResponseWriter, r *http.Request) {
|
func (ctx *context) BeginRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx.handlers = nil // will be filled by router.Serve/HTTP
|
ctx.handlers = nil // will be filled by router.Serve/HTTP
|
||||||
ctx.values = ctx.values[0:0] // >> >> by context.Values().Set
|
ctx.values = ctx.values[0:0] // >> >> by context.Values().Set
|
||||||
ctx.params.Store = ctx.params.Store[0:0]
|
ctx.params.Store = ctx.params.Store[0:0]
|
||||||
ctx.request = r
|
ctx.request = r
|
||||||
ctx.currentHandlerIndex = 0
|
ctx.currentHandlerIndex = 0
|
||||||
|
ctx.deferFunc = nil
|
||||||
ctx.writer = AcquireResponseWriter()
|
ctx.writer = AcquireResponseWriter()
|
||||||
ctx.writer.BeginResponse(w)
|
ctx.writer.BeginResponse(w)
|
||||||
}
|
}
|
||||||
|
@ -1123,12 +1132,16 @@ var StatusCodeNotSuccessful = func(statusCode int) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndRequest is executing once after a response to the request was sent and this context is useless or released.
|
// EndRequest is executing once after a response to the request was sent and this context is useless or released.
|
||||||
|
// Do NOT call it manually. Framework calls it automatically.
|
||||||
//
|
//
|
||||||
// To follow the iris' flow, developer should:
|
// 1. executes the Defer function (if any).
|
||||||
// 1. flush the response writer's result
|
// 2. flushes the response writer's result or fire any error handler.
|
||||||
// 2. release the response writer
|
// 3. releases the response writer.
|
||||||
// and any other optional steps, depends on dev's application type.
|
|
||||||
func (ctx *context) EndRequest() {
|
func (ctx *context) EndRequest() {
|
||||||
|
if ctx.deferFunc != nil {
|
||||||
|
ctx.deferFunc(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
if StatusCodeNotSuccessful(ctx.GetStatusCode()) &&
|
if StatusCodeNotSuccessful(ctx.GetStatusCode()) &&
|
||||||
!ctx.Application().ConfigurationReadOnly().GetDisableAutoFireStatusCode() {
|
!ctx.Application().ConfigurationReadOnly().GetDisableAutoFireStatusCode() {
|
||||||
// author's note:
|
// author's note:
|
||||||
|
@ -1158,6 +1171,13 @@ func (ctx *context) EndRequest() {
|
||||||
ctx.writer.EndResponse()
|
ctx.writer.EndResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defer executes a handler on this Context right before the request ends.
|
||||||
|
// The `StopExecution` does not effect the execution of this defer handler.
|
||||||
|
// The "h" runs before `FireErrorCode` (when response status code is not successful).
|
||||||
|
func (ctx *context) Defer(h Handler) {
|
||||||
|
ctx.deferFunc = h
|
||||||
|
}
|
||||||
|
|
||||||
// ResponseWriter returns an http.ResponseWriter compatible response writer, as expected.
|
// ResponseWriter returns an http.ResponseWriter compatible response writer, as expected.
|
||||||
func (ctx *context) ResponseWriter() ResponseWriter {
|
func (ctx *context) ResponseWriter() ResponseWriter {
|
||||||
return ctx.writer
|
return ctx.writer
|
||||||
|
@ -4615,6 +4635,9 @@ func (ctx *context) RouteExists(method, path string) bool {
|
||||||
|
|
||||||
const (
|
const (
|
||||||
reflectValueContextKey = "_iris_context_reflect_value"
|
reflectValueContextKey = "_iris_context_reflect_value"
|
||||||
|
// ControllerContextKey returns the context key from which
|
||||||
|
// the `Context.Controller` method returns the store's value.
|
||||||
|
ControllerContextKey = "_iris_controller_reflect_value"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReflectValue caches and returns a []reflect.Value{reflect.ValueOf(ctx)}.
|
// ReflectValue caches and returns a []reflect.Value{reflect.ValueOf(ctx)}.
|
||||||
|
@ -4629,6 +4652,18 @@ func (ctx *context) ReflectValue() []reflect.Value {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var emptyValue reflect.Value
|
||||||
|
|
||||||
|
// Controller returns a reflect Value of the custom Controller from which this handler executed.
|
||||||
|
// It will return a Kind() == reflect.Invalid if the handler was not executed from within a controller.
|
||||||
|
func (ctx *context) Controller() reflect.Value {
|
||||||
|
if v := ctx.Values().Get(ControllerContextKey); v != nil {
|
||||||
|
return v.(reflect.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyValue
|
||||||
|
}
|
||||||
|
|
||||||
// Application returns the iris app instance which belongs to this context.
|
// Application returns the iris app instance which belongs to this context.
|
||||||
// Worth to notice that this function returns an interface
|
// Worth to notice that this function returns an interface
|
||||||
// of the Application, which contains methods that are safe
|
// of the Application, which contains methods that are safe
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/context"
|
"github.com/kataras/iris/v12/context"
|
||||||
"github.com/kataras/iris/v12/core/errgroup"
|
"github.com/kataras/iris/v12/core/errgroup"
|
||||||
|
"github.com/kataras/iris/v12/hero"
|
||||||
"github.com/kataras/iris/v12/macro"
|
"github.com/kataras/iris/v12/macro"
|
||||||
macroHandler "github.com/kataras/iris/v12/macro/handler"
|
macroHandler "github.com/kataras/iris/v12/macro/handler"
|
||||||
)
|
)
|
||||||
|
@ -139,7 +140,7 @@ type APIBuilder struct {
|
||||||
// global done handlers, order doesn't matter.
|
// global done handlers, order doesn't matter.
|
||||||
doneGlobalHandlers context.Handlers
|
doneGlobalHandlers context.Handlers
|
||||||
|
|
||||||
// the per-party
|
// the per-party relative path.
|
||||||
relativePath string
|
relativePath string
|
||||||
// allowMethods are filled with the `AllowMethods` func.
|
// allowMethods are filled with the `AllowMethods` func.
|
||||||
// They are used to create new routes
|
// They are used to create new routes
|
||||||
|
@ -151,6 +152,8 @@ type APIBuilder struct {
|
||||||
handlerExecutionRules ExecutionRules
|
handlerExecutionRules ExecutionRules
|
||||||
// the per-party (and its children) route registration rule, see `SetRegisterRule`.
|
// the per-party (and its children) route registration rule, see `SetRegisterRule`.
|
||||||
routeRegisterRule RouteRegisterRule
|
routeRegisterRule RouteRegisterRule
|
||||||
|
// the per-party (and its children gets a clone) DI container. See `HandleFunc`, `UseFunc`, `DoneFunc` too.
|
||||||
|
container *hero.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Party = (*APIBuilder)(nil)
|
var _ Party = (*APIBuilder)(nil)
|
||||||
|
@ -165,6 +168,7 @@ func NewAPIBuilder() *APIBuilder {
|
||||||
errors: errgroup.New("API Builder"),
|
errors: errgroup.New("API Builder"),
|
||||||
relativePath: "/",
|
relativePath: "/",
|
||||||
routes: new(repository),
|
routes: new(repository),
|
||||||
|
container: hero.New(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return api
|
return api
|
||||||
|
@ -245,109 +249,46 @@ func (api *APIBuilder) SetRegisterRule(rule RouteRegisterRule) Party {
|
||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRoutes returns a list of Party-based Routes.
|
// GetContainer returns the DI Container of this Party.
|
||||||
// It does NOT registers the route. Use `Handle, Get...` methods instead.
|
// Use it to manually convert functions or structs(controllers) to a Handler.
|
||||||
// This method can be used for third-parties Iris helpers packages and tools
|
//
|
||||||
// that want a more detailed view of Party-based Routes before take the decision to register them.
|
// See `RegisterDependency` and `HandleFunc` too.
|
||||||
func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handlers ...context.Handler) []*Route {
|
func (api *APIBuilder) GetContainer() *hero.Container {
|
||||||
if len(methods) == 0 || methods[0] == "ALL" || methods[0] == "ANY" { // then use like it was .Any
|
return api.container
|
||||||
return api.Any(relativePath, handlers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// no clean path yet because of subdomain indicator/separator which contains a dot.
|
|
||||||
// but remove the first slash if the relative has already ending with a slash
|
|
||||||
// it's not needed because later on we do normalize/clean the path, but better do it here too
|
|
||||||
// for any future updates.
|
|
||||||
if api.relativePath[len(api.relativePath)-1] == '/' {
|
|
||||||
if relativePath[0] == '/' {
|
|
||||||
relativePath = relativePath[1:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filename, line := getCaller()
|
|
||||||
|
|
||||||
fullpath := api.relativePath + relativePath // for now, keep the last "/" if any, "/xyz/"
|
|
||||||
if len(handlers) == 0 {
|
|
||||||
api.errors.Addf("missing handlers for route[%s:%d] %s: %s", filename, line, strings.Join(methods, ", "), fullpath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// note: this can not change the caller's handlers as they're but the entry values(handlers)
|
|
||||||
// of `middleware`, `doneHandlers` and `handlers` can.
|
|
||||||
// So if we just put `api.middleware` or `api.doneHandlers`
|
|
||||||
// then the next `Party` will have those updated handlers
|
|
||||||
// but dev may change the rules for that child Party, so we have to make clones of them here.
|
|
||||||
var (
|
|
||||||
beginHandlers = joinHandlers(api.middleware, context.Handlers{})
|
|
||||||
doneHandlers = joinHandlers(api.doneHandlers, context.Handlers{})
|
|
||||||
)
|
|
||||||
|
|
||||||
mainHandlers := context.Handlers(handlers)
|
|
||||||
// before join the middleware + handlers + done handlers and apply the execution rules.
|
|
||||||
|
|
||||||
possibleMainHandlerName := context.MainHandlerName(mainHandlers)
|
|
||||||
|
|
||||||
// TODO: for UseGlobal/DoneGlobal that doesn't work.
|
|
||||||
applyExecutionRules(api.handlerExecutionRules, &beginHandlers, &doneHandlers, &mainHandlers)
|
|
||||||
|
|
||||||
// global begin handlers -> middleware that are registered before route registration
|
|
||||||
// -> handlers that are passed to this Handle function.
|
|
||||||
routeHandlers := joinHandlers(beginHandlers, mainHandlers)
|
|
||||||
// -> done handlers
|
|
||||||
routeHandlers = joinHandlers(routeHandlers, doneHandlers)
|
|
||||||
|
|
||||||
// here we separate the subdomain and relative path
|
|
||||||
subdomain, path := splitSubdomainAndPath(fullpath)
|
|
||||||
|
|
||||||
// if allowMethods are empty, then simply register with the passed, main, method.
|
|
||||||
methods = append(api.allowMethods, methods...)
|
|
||||||
|
|
||||||
routes := make([]*Route, len(methods))
|
|
||||||
|
|
||||||
for i, m := range methods {
|
|
||||||
route, err := NewRoute(m, subdomain, path, possibleMainHandlerName, routeHandlers, *api.macros)
|
|
||||||
if err != nil { // template path parser errors:
|
|
||||||
api.errors.Addf("[%s:%d] %v -> %s:%s:%s", filename, line, err, m, subdomain, path)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
route.SourceFileName = filename
|
|
||||||
route.SourceLineNumber = line
|
|
||||||
|
|
||||||
// Add UseGlobal & DoneGlobal Handlers
|
|
||||||
route.Use(api.beginGlobalHandlers...)
|
|
||||||
route.Done(api.doneGlobalHandlers...)
|
|
||||||
|
|
||||||
routes[i] = route
|
|
||||||
}
|
|
||||||
|
|
||||||
return routes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://golang.org/doc/go1.9#callersframes
|
// RegisterDependency adds a dependency.
|
||||||
func getCaller() (string, int) {
|
// The value can be a single struct value or a function.
|
||||||
var pcs [32]uintptr
|
// Follow the rules:
|
||||||
n := runtime.Callers(1, pcs[:])
|
// * <T>{structValue}
|
||||||
frames := runtime.CallersFrames(pcs[:n])
|
// * func(accepts <T>) returns <D> or (<D>, error)
|
||||||
wd, _ := os.Getwd()
|
// * func(accepts iris.Context) returns <D> or (<D>, error)
|
||||||
for {
|
// * func(accepts1 iris.Context, accepts2 *hero.Input) returns <D> or (<D>, error)
|
||||||
frame, more := frames.Next()
|
//
|
||||||
file := frame.File
|
// A Dependency can accept a previous registered dependency and return a new one or the same updated.
|
||||||
|
// * func(accepts1 <D>, accepts2 <T>) returns <E> or (<E>, error) or error
|
||||||
|
// * func(acceptsPathParameter1 string, id uint64) returns <T> or (<T>, error)
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// - RegisterDependency(loggerService{prefix: "dev"})
|
||||||
|
// - RegisterDependency(func(ctx iris.Context) User {...})
|
||||||
|
// - RegisterDependency(func(User) OtherResponse {...})
|
||||||
|
func (api *APIBuilder) RegisterDependency(dependency interface{}) *hero.Dependency {
|
||||||
|
return api.container.Register(dependency)
|
||||||
|
}
|
||||||
|
|
||||||
if !strings.Contains(file, "/kataras/iris") || strings.Contains(file, "/kataras/iris/_examples") || strings.Contains(file, "iris-contrib/examples") {
|
// HandleFunc accepts one or more "handlersFn" functions which each one of them
|
||||||
if relFile, err := filepath.Rel(wd, file); err == nil {
|
// can accept any input arguments that match with the Party's registered Container's `Dependencies` and
|
||||||
file = "./" + relFile
|
// any output result; like custom structs <T>, string, []byte, int, error,
|
||||||
}
|
// a combination of the above, hero.Result(hero.View | hero.Response) and more.
|
||||||
|
func (api *APIBuilder) HandleFunc(method, relativePath string, handlersFn ...interface{}) *Route {
|
||||||
return file, frame.Line
|
handlers := make(context.Handlers, 0, len(handlersFn))
|
||||||
}
|
for _, h := range handlersFn {
|
||||||
|
handlers = append(handlers, api.container.Handler(h))
|
||||||
if !more {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "???", 0
|
return api.Handle(method, relativePath, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle registers a route to the server's api.
|
// Handle registers a route to the server's api.
|
||||||
|
@ -485,6 +426,85 @@ func (api *APIBuilder) HandleDir(requestPath, directory string, opts ...DirOptio
|
||||||
return getRoute
|
return getRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateRoutes returns a list of Party-based Routes.
|
||||||
|
// It does NOT registers the route. Use `Handle, Get...` methods instead.
|
||||||
|
// This method can be used for third-parties Iris helpers packages and tools
|
||||||
|
// that want a more detailed view of Party-based Routes before take the decision to register them.
|
||||||
|
func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handlers ...context.Handler) []*Route {
|
||||||
|
if len(methods) == 0 || methods[0] == "ALL" || methods[0] == "ANY" { // then use like it was .Any
|
||||||
|
return api.Any(relativePath, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// no clean path yet because of subdomain indicator/separator which contains a dot.
|
||||||
|
// but remove the first slash if the relative has already ending with a slash
|
||||||
|
// it's not needed because later on we do normalize/clean the path, but better do it here too
|
||||||
|
// for any future updates.
|
||||||
|
if api.relativePath[len(api.relativePath)-1] == '/' {
|
||||||
|
if relativePath[0] == '/' {
|
||||||
|
relativePath = relativePath[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filename, line := getCaller()
|
||||||
|
|
||||||
|
fullpath := api.relativePath + relativePath // for now, keep the last "/" if any, "/xyz/"
|
||||||
|
if len(handlers) == 0 {
|
||||||
|
api.errors.Addf("missing handlers for route[%s:%d] %s: %s", filename, line, strings.Join(methods, ", "), fullpath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// note: this can not change the caller's handlers as they're but the entry values(handlers)
|
||||||
|
// of `middleware`, `doneHandlers` and `handlers` can.
|
||||||
|
// So if we just put `api.middleware` or `api.doneHandlers`
|
||||||
|
// then the next `Party` will have those updated handlers
|
||||||
|
// but dev may change the rules for that child Party, so we have to make clones of them here.
|
||||||
|
var (
|
||||||
|
beginHandlers = joinHandlers(api.middleware, context.Handlers{})
|
||||||
|
doneHandlers = joinHandlers(api.doneHandlers, context.Handlers{})
|
||||||
|
)
|
||||||
|
|
||||||
|
mainHandlers := context.Handlers(handlers)
|
||||||
|
// before join the middleware + handlers + done handlers and apply the execution rules.
|
||||||
|
|
||||||
|
possibleMainHandlerName := context.MainHandlerName(mainHandlers)
|
||||||
|
|
||||||
|
// TODO: for UseGlobal/DoneGlobal that doesn't work.
|
||||||
|
applyExecutionRules(api.handlerExecutionRules, &beginHandlers, &doneHandlers, &mainHandlers)
|
||||||
|
|
||||||
|
// global begin handlers -> middleware that are registered before route registration
|
||||||
|
// -> handlers that are passed to this Handle function.
|
||||||
|
routeHandlers := joinHandlers(beginHandlers, mainHandlers)
|
||||||
|
// -> done handlers
|
||||||
|
routeHandlers = joinHandlers(routeHandlers, doneHandlers)
|
||||||
|
|
||||||
|
// here we separate the subdomain and relative path
|
||||||
|
subdomain, path := splitSubdomainAndPath(fullpath)
|
||||||
|
|
||||||
|
// if allowMethods are empty, then simply register with the passed, main, method.
|
||||||
|
methods = append(api.allowMethods, methods...)
|
||||||
|
|
||||||
|
routes := make([]*Route, len(methods))
|
||||||
|
|
||||||
|
for i, m := range methods {
|
||||||
|
route, err := NewRoute(m, subdomain, path, possibleMainHandlerName, routeHandlers, *api.macros)
|
||||||
|
if err != nil { // template path parser errors:
|
||||||
|
api.errors.Addf("[%s:%d] %v -> %s:%s:%s", filename, line, err, m, subdomain, path)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
route.SourceFileName = filename
|
||||||
|
route.SourceLineNumber = line
|
||||||
|
|
||||||
|
// Add UseGlobal & DoneGlobal Handlers
|
||||||
|
route.Use(api.beginGlobalHandlers...)
|
||||||
|
route.Done(api.doneGlobalHandlers...)
|
||||||
|
|
||||||
|
routes[i] = route
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes
|
||||||
|
}
|
||||||
|
|
||||||
// Party groups routes which may have the same prefix and share same handlers,
|
// Party groups routes which may have the same prefix and share same handlers,
|
||||||
// returns that new rich subrouter.
|
// returns that new rich subrouter.
|
||||||
//
|
//
|
||||||
|
@ -520,6 +540,12 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
|
||||||
allowMethods := make([]string, len(api.allowMethods))
|
allowMethods := make([]string, len(api.allowMethods))
|
||||||
copy(allowMethods, api.allowMethods)
|
copy(allowMethods, api.allowMethods)
|
||||||
|
|
||||||
|
// attach a new Container with correct dynamic path parameter start index for input arguments
|
||||||
|
// based on the fullpath.
|
||||||
|
childContainer := api.container.Clone()
|
||||||
|
fpath, _ := macro.Parse(fullpath, *api.macros)
|
||||||
|
childContainer.ParamStartIndex = len(fpath.Params)
|
||||||
|
|
||||||
return &APIBuilder{
|
return &APIBuilder{
|
||||||
// global/api builder
|
// global/api builder
|
||||||
macros: api.macros,
|
macros: api.macros,
|
||||||
|
@ -535,6 +561,7 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
|
||||||
allowMethods: allowMethods,
|
allowMethods: allowMethods,
|
||||||
handlerExecutionRules: api.handlerExecutionRules,
|
handlerExecutionRules: api.handlerExecutionRules,
|
||||||
routeRegisterRule: api.routeRegisterRule,
|
routeRegisterRule: api.routeRegisterRule,
|
||||||
|
container: childContainer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -730,6 +757,7 @@ func (api *APIBuilder) Reset() Party {
|
||||||
api.doneHandlers = api.doneHandlers[0:0]
|
api.doneHandlers = api.doneHandlers[0:0]
|
||||||
api.handlerExecutionRules = ExecutionRules{}
|
api.handlerExecutionRules = ExecutionRules{}
|
||||||
api.routeRegisterRule = RouteOverride
|
api.routeRegisterRule = RouteOverride
|
||||||
|
// keep container as it's.
|
||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -967,3 +995,29 @@ func joinHandlers(h1 context.Handlers, h2 context.Handlers) context.Handlers {
|
||||||
copy(newHandlers[nowLen:], h2)
|
copy(newHandlers[nowLen:], h2)
|
||||||
return newHandlers
|
return newHandlers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://golang.org/doc/go1.9#callersframes
|
||||||
|
func getCaller() (string, int) {
|
||||||
|
var pcs [32]uintptr
|
||||||
|
n := runtime.Callers(1, pcs[:])
|
||||||
|
frames := runtime.CallersFrames(pcs[:n])
|
||||||
|
wd, _ := os.Getwd()
|
||||||
|
for {
|
||||||
|
frame, more := frames.Next()
|
||||||
|
file := frame.File
|
||||||
|
|
||||||
|
if !strings.Contains(file, "/kataras/iris") || strings.Contains(file, "/kataras/iris/_examples") || strings.Contains(file, "iris-contrib/examples") {
|
||||||
|
if relFile, err := filepath.Rel(wd, file); err == nil {
|
||||||
|
file = "./" + relFile
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, frame.Line
|
||||||
|
}
|
||||||
|
|
||||||
|
if !more {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "???", 0
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package router
|
||||||
import (
|
import (
|
||||||
"github.com/kataras/iris/v12/context"
|
"github.com/kataras/iris/v12/context"
|
||||||
"github.com/kataras/iris/v12/core/errgroup"
|
"github.com/kataras/iris/v12/core/errgroup"
|
||||||
|
"github.com/kataras/iris/v12/hero"
|
||||||
"github.com/kataras/iris/v12/macro"
|
"github.com/kataras/iris/v12/macro"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -101,6 +102,36 @@ type Party interface {
|
||||||
// SetRegisterRule sets a `RouteRegisterRule` for this Party and its children.
|
// 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 and RouteError.
|
||||||
SetRegisterRule(rule RouteRegisterRule) Party
|
SetRegisterRule(rule RouteRegisterRule) Party
|
||||||
|
|
||||||
|
// GetContainer returns the DI Container of this Party.
|
||||||
|
// Use it to manually convert functions or structs(controllers) to a Handler.
|
||||||
|
//
|
||||||
|
// See `RegisterDependency` and `HandleFunc` too.
|
||||||
|
GetContainer() *hero.Container
|
||||||
|
// RegisterDependency adds a dependency.
|
||||||
|
// The value can be a single struct value or a function.
|
||||||
|
// Follow the rules:
|
||||||
|
// * <T>{structValue}
|
||||||
|
// * func(accepts <T>) returns <D> or (<D>, error)
|
||||||
|
// * func(accepts iris.Context) returns <D> or (<D>, error)
|
||||||
|
// * func(accepts1 iris.Context, accepts2 *hero.Input) returns <D> or (<D>, error)
|
||||||
|
//
|
||||||
|
// A Dependency can accept a previous registered dependency and return a new one or the same updated.
|
||||||
|
// * func(accepts1 <D>, accepts2 <T>) returns <E> or (<E>, error) or error
|
||||||
|
// * func(acceptsPathParameter1 string, id uint64) returns <T> or (<T>, error)
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// - RegisterDependency(loggerService{prefix: "dev"})
|
||||||
|
// - RegisterDependency(func(ctx iris.Context) User {...})
|
||||||
|
// - RegisterDependency(func(User) OtherResponse {...})
|
||||||
|
RegisterDependency(dependency interface{}) *hero.Dependency
|
||||||
|
// HandleFunc accepts one or more "handlersFn" functions which each one of them
|
||||||
|
// can accept any input arguments that match with the Party's registered Container's `Dependencies` and
|
||||||
|
// any output result; like custom structs <T>, string, []byte, int, error,
|
||||||
|
// a combination of the above, hero.Result(hero.View | hero.Response) and more.
|
||||||
|
HandleFunc(method, relativePath string, handlersFn ...interface{}) *Route
|
||||||
|
|
||||||
// Handle registers a route to the server's router.
|
// Handle registers a route to the server's router.
|
||||||
// if empty method is passed then handler(s) are being registered to all methods, same as .Any.
|
// if empty method is passed then handler(s) are being registered to all methods, same as .Any.
|
||||||
//
|
//
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -1,6 +1,6 @@
|
||||||
module github.com/kataras/iris/v12
|
module github.com/kataras/iris/v12
|
||||||
|
|
||||||
go 1.13
|
go 1.14
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v0.3.1
|
github.com/BurntSushi/toml v0.3.1
|
||||||
|
|
340
hero/binding.go
Normal file
340
hero/binding.go
Normal file
|
@ -0,0 +1,340 @@
|
||||||
|
package hero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Binding struct {
|
||||||
|
Dependency *Dependency
|
||||||
|
Input *Input
|
||||||
|
}
|
||||||
|
|
||||||
|
type Input struct {
|
||||||
|
Index int // for func inputs
|
||||||
|
StructFieldIndex []int // for struct fields in order to support embedded ones.
|
||||||
|
Type reflect.Type
|
||||||
|
|
||||||
|
selfValue reflect.Value // reflect.ValueOf(*Input) cache.
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInput(typ reflect.Type, index int, structFieldIndex []int) *Input {
|
||||||
|
in := &Input{
|
||||||
|
Index: index,
|
||||||
|
StructFieldIndex: structFieldIndex,
|
||||||
|
Type: typ,
|
||||||
|
}
|
||||||
|
|
||||||
|
in.selfValue = reflect.ValueOf(in)
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Binding) String() string {
|
||||||
|
index := fmt.Sprintf("%d", b.Input.Index)
|
||||||
|
if len(b.Input.StructFieldIndex) > 0 {
|
||||||
|
for j, i := range b.Input.StructFieldIndex {
|
||||||
|
if j == 0 {
|
||||||
|
index = fmt.Sprintf("%d", i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
index += fmt.Sprintf(".%d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("[%s:%s] maps to [%s]", index, b.Input.Type.String(), b.Dependency)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Binding) Equal(other *Binding) bool {
|
||||||
|
if b == nil {
|
||||||
|
return other == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if other == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// if b.String() != other.String() {
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
|
||||||
|
if expected, got := b.Dependency != nil, other.Dependency != nil; expected != got {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := fmt.Sprintf("%v", b.Dependency.OriginalValue), fmt.Sprintf("%v", other.Dependency.OriginalValue); expected != got {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := b.Dependency.DestType != nil, other.Dependency.DestType != nil; expected != got {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Dependency.DestType != nil {
|
||||||
|
if expected, got := b.Dependency.DestType.String(), other.Dependency.DestType.String(); expected != got {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := b.Input != nil, other.Input != nil; expected != got {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Input != nil {
|
||||||
|
if expected, got := b.Input.Index, other.Input.Index; expected != got {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := b.Input.Type.String(), other.Input.Type.String(); expected != got {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := b.Input.StructFieldIndex, other.Input.StructFieldIndex; !reflect.DeepEqual(expected, got) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchDependency(dep *Dependency, in reflect.Type) bool {
|
||||||
|
if dep.Explicit {
|
||||||
|
return dep.DestType == in
|
||||||
|
}
|
||||||
|
|
||||||
|
return dep.DestType == nil || equalTypes(dep.DestType, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBindingsFor(inputs []reflect.Type, deps []*Dependency, paramStartIndex int) (bindings []*Binding) {
|
||||||
|
bindedInput := make(map[int]struct{})
|
||||||
|
|
||||||
|
// lastParamIndex is used to bind parameters correctly when:
|
||||||
|
// otherDep, param1, param2 string and param1 string, otherDep, param2 string.
|
||||||
|
lastParamIndex := paramStartIndex
|
||||||
|
getParamIndex := func(index int) (paramIndex int) {
|
||||||
|
// if len(bindings) > 0 {
|
||||||
|
// // mostly, it means it's binding to a struct's method, which first value is always the ptr struct as its receiver.
|
||||||
|
// // so we decrement the parameter index otherwise first parameter would be declared as parameter index 1 instead of 0.
|
||||||
|
// paramIndex = len(bindings) + lastParamIndex - 1
|
||||||
|
// lastParamIndex = paramIndex + 1
|
||||||
|
// return paramIndex
|
||||||
|
// }
|
||||||
|
|
||||||
|
// lastParamIndex = index + 1
|
||||||
|
// return index
|
||||||
|
|
||||||
|
paramIndex = lastParamIndex
|
||||||
|
lastParamIndex = paramIndex + 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, in := range inputs { //order matters.
|
||||||
|
|
||||||
|
_, canBePathParameter := context.ParamResolvers[in]
|
||||||
|
canBePathParameter = canBePathParameter && paramStartIndex != -1 // if -1 then parameter resolver is disabled.
|
||||||
|
|
||||||
|
prevN := len(bindings) // to check if a new binding is attached; a dependency was matched (see below).
|
||||||
|
|
||||||
|
for j := len(deps) - 1; j >= 0; j-- {
|
||||||
|
d := deps[j]
|
||||||
|
// Note: we could use the same slice to return.
|
||||||
|
//
|
||||||
|
// Add all dynamic dependencies (caller-selecting) and the exact typed dependencies.
|
||||||
|
//
|
||||||
|
// A dependency can only be matched to 1 value, and 1 value has a single dependency
|
||||||
|
// (e.g. to avoid conflicting path parameters of the same type).
|
||||||
|
if _, alreadyBinded := bindedInput[j]; alreadyBinded {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
match := matchDependency(d, in)
|
||||||
|
if !match {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if canBePathParameter {
|
||||||
|
// wrap the existing dependency handler.
|
||||||
|
paramHandler := paramDependencyHandler(getParamIndex((i)))
|
||||||
|
prevHandler := d.Handle
|
||||||
|
d.Handle = func(ctx context.Context, input *Input) (reflect.Value, error) {
|
||||||
|
v, err := paramHandler(ctx, input)
|
||||||
|
if err != nil {
|
||||||
|
v, err = prevHandler(ctx, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
d.Static = false
|
||||||
|
d.OriginalValue = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings = append(bindings, &Binding{
|
||||||
|
Dependency: d,
|
||||||
|
Input: newInput(in, i, nil),
|
||||||
|
})
|
||||||
|
|
||||||
|
if !d.Explicit { // if explicit then it can be binded to more than one input
|
||||||
|
bindedInput[j] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if prevN == len(bindings) {
|
||||||
|
if canBePathParameter {
|
||||||
|
// no new dependency added for this input,
|
||||||
|
// let's check for path parameters.
|
||||||
|
bindings = append(bindings, paramBinding(i, getParamIndex(i), in))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// else add builtin bindings that may be registered by user too, but they didn't.
|
||||||
|
if indirectType(in).Kind() == reflect.Struct {
|
||||||
|
bindings = append(bindings, payloadBinding(i, in))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBindingsForFunc(fn reflect.Value, dependencies []*Dependency, paramStartIndex int) []*Binding {
|
||||||
|
fnTyp := fn.Type()
|
||||||
|
if !isFunc(fnTyp) {
|
||||||
|
panic("bindings: unresolved: not a func type")
|
||||||
|
}
|
||||||
|
|
||||||
|
n := fnTyp.NumIn()
|
||||||
|
inputs := make([]reflect.Type, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
inputs[i] = fnTyp.In(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings := getBindingsFor(inputs, dependencies, paramStartIndex)
|
||||||
|
if expected, got := n, len(bindings); expected > got {
|
||||||
|
panic(fmt.Sprintf("expected [%d] bindings (input parameters) but got [%d]", expected, got))
|
||||||
|
}
|
||||||
|
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBindingsForStruct(v reflect.Value, dependencies []*Dependency, paramStartIndex int, sorter Sorter) (bindings []*Binding) {
|
||||||
|
typ := indirectType(v.Type())
|
||||||
|
if typ.Kind() != reflect.Struct {
|
||||||
|
panic("bindings: unresolved: no struct type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get bindings from any struct's non zero values first, including unexported.
|
||||||
|
elem := reflect.Indirect(v)
|
||||||
|
nonZero := lookupNonZeroFieldValues(elem)
|
||||||
|
for _, f := range nonZero {
|
||||||
|
// fmt.Printf("Controller [%s] | NonZero | Field Index: %v | Field Type: %s\n", typ, f.Index, f.Type)
|
||||||
|
bindings = append(bindings, &Binding{
|
||||||
|
Dependency: NewDependency(elem.FieldByIndex(f.Index).Interface()),
|
||||||
|
Input: newInput(f.Type, f.Index[0], f.Index),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := lookupFields(elem, true, true, nil)
|
||||||
|
n := len(fields)
|
||||||
|
|
||||||
|
if n > 1 && sorter != nil {
|
||||||
|
sort.Slice(fields, func(i, j int) bool {
|
||||||
|
return sorter(fields[i].Type, fields[j].Type)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs := make([]reflect.Type, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
// fmt.Printf("Controller [%s] | Field Index: %v | Field Type: %s\n", typ, fields[i].Index, fields[i].Type)
|
||||||
|
inputs[i] = fields[i].Type
|
||||||
|
}
|
||||||
|
exportedBindings := getBindingsFor(inputs, dependencies, paramStartIndex)
|
||||||
|
|
||||||
|
// fmt.Printf("Controller [%s] Inputs length: %d vs Bindings length: %d\n", typ, n, len(exportedBindings))
|
||||||
|
if len(nonZero) >= len(exportedBindings) { // if all are fields are defined then just return.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get declared bindings from deps.
|
||||||
|
bindings = append(bindings, exportedBindings...)
|
||||||
|
for _, binding := range bindings {
|
||||||
|
if len(binding.Input.StructFieldIndex) == 0 {
|
||||||
|
// set correctly the input's field index.
|
||||||
|
structFieldIndex := fields[binding.Input.Index].Index
|
||||||
|
binding.Input.StructFieldIndex = structFieldIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Printf("Controller [%s] | Binding Index: %v | Binding Type: %s\n", typ, binding.Input.StructFieldIndex, binding.Input.Type)
|
||||||
|
|
||||||
|
// fmt.Printf("Controller [%s] Set [%s] to struct field index: %v\n", typ.String(), binding.Input.Type.String(), structFieldIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Builtin dynamic bindings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
func paramBinding(index, paramIndex int, typ reflect.Type) *Binding {
|
||||||
|
return &Binding{
|
||||||
|
Dependency: &Dependency{Handle: paramDependencyHandler(paramIndex), DestType: typ, Source: getSource()},
|
||||||
|
Input: newInput(typ, index, nil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func paramDependencyHandler(paramIndex int) DependencyHandler {
|
||||||
|
return func(ctx context.Context, input *Input) (reflect.Value, error) {
|
||||||
|
if ctx.Params().Len() <= paramIndex {
|
||||||
|
return emptyValue, ErrSeeOther
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflect.ValueOf(ctx.Params().Store[paramIndex].ValueRaw), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// registered if input parameters are more than matched dependencies.
|
||||||
|
// It binds an input to a request body based on the request content-type header (JSON, XML, YAML, Query, Form).
|
||||||
|
func payloadBinding(index int, typ reflect.Type) *Binding {
|
||||||
|
return &Binding{
|
||||||
|
Dependency: &Dependency{
|
||||||
|
Handle: func(ctx context.Context, input *Input) (newValue reflect.Value, err error) {
|
||||||
|
wasPtr := input.Type.Kind() == reflect.Ptr
|
||||||
|
|
||||||
|
newValue = reflect.New(indirectType(input.Type))
|
||||||
|
ptr := newValue.Interface()
|
||||||
|
|
||||||
|
switch ctx.GetContentTypeRequested() {
|
||||||
|
case context.ContentXMLHeaderValue:
|
||||||
|
err = ctx.ReadXML(ptr)
|
||||||
|
case context.ContentYAMLHeaderValue:
|
||||||
|
err = ctx.ReadYAML(ptr)
|
||||||
|
case context.ContentFormHeaderValue:
|
||||||
|
err = ctx.ReadQuery(ptr)
|
||||||
|
case context.ContentFormMultipartHeaderValue:
|
||||||
|
err = ctx.ReadForm(ptr)
|
||||||
|
default:
|
||||||
|
err = ctx.ReadJSON(ptr)
|
||||||
|
// json
|
||||||
|
}
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// return emptyValue, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
if !wasPtr {
|
||||||
|
newValue = newValue.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
},
|
||||||
|
Source: getSource(),
|
||||||
|
},
|
||||||
|
Input: newInput(typ, index, nil),
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
482
hero/binding_test.go
Normal file
482
hero/binding_test.go
Normal file
|
@ -0,0 +1,482 @@
|
||||||
|
package hero
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdContext "context"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
"github.com/kataras/iris/v12/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func contextBinding(index int) *Binding {
|
||||||
|
return &Binding{
|
||||||
|
Dependency: BuiltinDependencies[0],
|
||||||
|
Input: &Input{Type: BuiltinDependencies[0].DestType, Index: index},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetBindingsForFunc(t *testing.T) {
|
||||||
|
type (
|
||||||
|
testResponse struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
testRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
testRequest2 struct {
|
||||||
|
// normally a body can't have two requests but let's test it.
|
||||||
|
Age int `json:"age"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var testRequestTyp = reflect.TypeOf(testRequest{})
|
||||||
|
|
||||||
|
var deps = []*Dependency{
|
||||||
|
NewDependency(func(ctx context.Context) testRequest { return testRequest{Email: "should be ignored"} }),
|
||||||
|
NewDependency(42),
|
||||||
|
NewDependency(func(ctx context.Context) (v testRequest, err error) {
|
||||||
|
err = ctx.ReadJSON(&v)
|
||||||
|
return
|
||||||
|
}),
|
||||||
|
NewDependency("if two strings requested this should be the last one"),
|
||||||
|
NewDependency("should not be ignored when requested"),
|
||||||
|
|
||||||
|
// Dependencies like these should always be registered last.
|
||||||
|
NewDependency(func(ctx context.Context, input *Input) (newValue reflect.Value, err error) {
|
||||||
|
wasPtr := input.Type.Kind() == reflect.Ptr
|
||||||
|
|
||||||
|
newValue = reflect.New(indirectType(input.Type))
|
||||||
|
ptr := newValue.Interface()
|
||||||
|
err = ctx.ReadJSON(ptr)
|
||||||
|
|
||||||
|
if !wasPtr {
|
||||||
|
newValue = newValue.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
return newValue, err
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
var tests = []struct {
|
||||||
|
Func interface{}
|
||||||
|
Expected []*Binding
|
||||||
|
}{
|
||||||
|
{ // 0
|
||||||
|
Func: func(ctx context.Context) {
|
||||||
|
ctx.WriteString("t1")
|
||||||
|
},
|
||||||
|
Expected: []*Binding{contextBinding(0)},
|
||||||
|
},
|
||||||
|
{ // 1
|
||||||
|
Func: func(ctx context.Context) error {
|
||||||
|
return fmt.Errorf("err1")
|
||||||
|
},
|
||||||
|
Expected: []*Binding{contextBinding(0)},
|
||||||
|
},
|
||||||
|
{ // 2
|
||||||
|
Func: func(ctx context.Context) testResponse {
|
||||||
|
return testResponse{Name: "name"}
|
||||||
|
},
|
||||||
|
Expected: []*Binding{contextBinding(0)},
|
||||||
|
},
|
||||||
|
{ // 3
|
||||||
|
Func: func(in testRequest) (testResponse, error) {
|
||||||
|
return testResponse{Name: "email of " + in.Email}, nil
|
||||||
|
},
|
||||||
|
Expected: []*Binding{{Dependency: deps[2], Input: &Input{Index: 0, Type: testRequestTyp}}},
|
||||||
|
},
|
||||||
|
{ // 4
|
||||||
|
Func: func(in testRequest) (testResponse, error) {
|
||||||
|
return testResponse{Name: "not valid "}, fmt.Errorf("invalid")
|
||||||
|
},
|
||||||
|
Expected: []*Binding{{Dependency: deps[2], Input: &Input{Index: 0, Type: testRequestTyp}}},
|
||||||
|
},
|
||||||
|
{ // 5
|
||||||
|
Func: func(ctx context.Context, in testRequest) testResponse {
|
||||||
|
return testResponse{Name: "(with ctx) email of " + in.Email}
|
||||||
|
},
|
||||||
|
Expected: []*Binding{contextBinding(0), {Dependency: deps[2], Input: &Input{Index: 1, Type: testRequestTyp}}},
|
||||||
|
},
|
||||||
|
{ // 6
|
||||||
|
Func: func(in testRequest, ctx context.Context) testResponse { // reversed.
|
||||||
|
return testResponse{Name: "(with ctx) email of " + in.Email}
|
||||||
|
},
|
||||||
|
Expected: []*Binding{{Dependency: deps[2], Input: &Input{Index: 0, Type: testRequestTyp}}, contextBinding(1)},
|
||||||
|
},
|
||||||
|
{ // 7
|
||||||
|
Func: func(in testRequest, ctx context.Context, in2 string) testResponse { // reversed.
|
||||||
|
return testResponse{Name: "(with ctx) email of " + in.Email + "and in2: " + in2}
|
||||||
|
},
|
||||||
|
Expected: []*Binding{
|
||||||
|
{
|
||||||
|
Dependency: deps[2],
|
||||||
|
Input: &Input{Index: 0, Type: testRequestTyp},
|
||||||
|
},
|
||||||
|
contextBinding(1),
|
||||||
|
{
|
||||||
|
Dependency: deps[4],
|
||||||
|
Input: &Input{Index: 2, Type: reflect.TypeOf("")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // 8
|
||||||
|
Func: func(in testRequest, ctx context.Context, in2, in3 string) testResponse { // reversed.
|
||||||
|
return testResponse{Name: "(with ctx) email of " + in.Email + " | in2: " + in2 + " in3: " + in3}
|
||||||
|
},
|
||||||
|
Expected: []*Binding{
|
||||||
|
{
|
||||||
|
Dependency: deps[2],
|
||||||
|
Input: &Input{Index: 0, Type: testRequestTyp},
|
||||||
|
},
|
||||||
|
contextBinding(1),
|
||||||
|
{
|
||||||
|
Dependency: deps[len(deps)-3],
|
||||||
|
Input: &Input{Index: 2, Type: reflect.TypeOf("")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dependency: deps[len(deps)-2],
|
||||||
|
Input: &Input{Index: 3, Type: reflect.TypeOf("")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // 9
|
||||||
|
Func: func(ctx context.Context, in testRequest, in2 testRequest2) testResponse {
|
||||||
|
return testResponse{Name: fmt.Sprintf("(with ctx) email of %s and in2.Age %d", in.Email, in2.Age)}
|
||||||
|
},
|
||||||
|
Expected: []*Binding{
|
||||||
|
contextBinding(0),
|
||||||
|
{
|
||||||
|
Dependency: deps[2],
|
||||||
|
Input: &Input{Index: 1, Type: testRequestTyp},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dependency: deps[len(deps)-1],
|
||||||
|
Input: &Input{Index: 2, Type: reflect.TypeOf(testRequest2{})},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // 10
|
||||||
|
Func: func() testResponse {
|
||||||
|
return testResponse{Name: "empty in, one out"}
|
||||||
|
},
|
||||||
|
Expected: nil,
|
||||||
|
},
|
||||||
|
{ // 1
|
||||||
|
Func: func(userID string, age int) testResponse {
|
||||||
|
return testResponse{Name: "in from path parameters"}
|
||||||
|
},
|
||||||
|
Expected: []*Binding{
|
||||||
|
paramBinding(0, 0, reflect.TypeOf("")),
|
||||||
|
paramBinding(1, 1, reflect.TypeOf(0)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// test std context and session bindings.
|
||||||
|
{ // 12
|
||||||
|
Func: func(ctx stdContext.Context, s *sessions.Session, t time.Time) testResponse {
|
||||||
|
return testResponse{"from std context and session"}
|
||||||
|
},
|
||||||
|
Expected: []*Binding{
|
||||||
|
{
|
||||||
|
Dependency: NewDependency(BuiltinDependencies[1]),
|
||||||
|
Input: &Input{Index: 0, Type: stdContextTyp},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dependency: NewDependency(BuiltinDependencies[2]),
|
||||||
|
Input: &Input{Index: 1, Type: sessionTyp},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dependency: NewDependency(BuiltinDependencies[3]),
|
||||||
|
Input: &Input{Index: 2, Type: reflect.TypeOf(time.Time{})},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c := New()
|
||||||
|
for _, dependency := range deps {
|
||||||
|
c.Register(dependency)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
bindings := getBindingsForFunc(reflect.ValueOf(tt.Func), c.Dependencies, 0)
|
||||||
|
|
||||||
|
if expected, got := len(tt.Expected), len(bindings); expected != got {
|
||||||
|
t.Fatalf("[%d] expected bindings length to be: %d but got: %d", i, expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
for j, b := range bindings {
|
||||||
|
if b == nil {
|
||||||
|
t.Fatalf("[%d:%d] binding is nil!", i, j)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.Expected[j] == nil {
|
||||||
|
t.Fatalf("[%d:%d] expected dependency was not found!", i, j)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if expected := tt.Expected[j]; !expected.Equal(b) {
|
||||||
|
// t.Fatalf("[%d:%d] got unexpected binding:\n%s", i, j, spew.Sdump(expected, b))
|
||||||
|
// }
|
||||||
|
|
||||||
|
if expected := tt.Expected[j]; !expected.Equal(b) {
|
||||||
|
t.Fatalf("[%d:%d] expected binding:\n%s\nbut got:\n%s", i, j, expected, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
service interface {
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
serviceImpl struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
var serviceTyp = reflect.TypeOf((*service)(nil)).Elem()
|
||||||
|
|
||||||
|
func (s *serviceImpl) String() string {
|
||||||
|
return "service"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindingsForStruct(t *testing.T) {
|
||||||
|
type (
|
||||||
|
controller struct {
|
||||||
|
Name string
|
||||||
|
Service service
|
||||||
|
}
|
||||||
|
|
||||||
|
embedded1 struct {
|
||||||
|
Age int
|
||||||
|
}
|
||||||
|
|
||||||
|
embedded2 struct {
|
||||||
|
Now time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
Embedded3 struct {
|
||||||
|
Age int
|
||||||
|
}
|
||||||
|
|
||||||
|
Embedded4 struct {
|
||||||
|
Now time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
controllerEmbeddingExported struct {
|
||||||
|
Embedded3
|
||||||
|
Embedded4
|
||||||
|
}
|
||||||
|
|
||||||
|
controllerEmbeddingUnexported struct {
|
||||||
|
embedded1
|
||||||
|
embedded2
|
||||||
|
}
|
||||||
|
|
||||||
|
controller2 struct {
|
||||||
|
Emb1 embedded1
|
||||||
|
Emb2 embedded2
|
||||||
|
}
|
||||||
|
|
||||||
|
controller3 struct {
|
||||||
|
Emb1 embedded1
|
||||||
|
emb2 embedded2
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var deps = []*Dependency{
|
||||||
|
NewDependency("name"),
|
||||||
|
NewDependency(new(serviceImpl)),
|
||||||
|
}
|
||||||
|
|
||||||
|
var depsForAnonymousEmbedded = []*Dependency{
|
||||||
|
NewDependency(42),
|
||||||
|
NewDependency(time.Now()),
|
||||||
|
}
|
||||||
|
|
||||||
|
var depsForFieldsOfStruct = []*Dependency{
|
||||||
|
NewDependency(embedded1{Age: 42}),
|
||||||
|
NewDependency(embedded2{time.Now()}),
|
||||||
|
}
|
||||||
|
|
||||||
|
var depsInterfaces = []*Dependency{
|
||||||
|
NewDependency(func(ctx context.Context) interface{} {
|
||||||
|
return "name"
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
var autoBindings = []*Binding{
|
||||||
|
payloadBinding(0, reflect.TypeOf(embedded1{})),
|
||||||
|
payloadBinding(1, reflect.TypeOf(embedded2{})),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, b := range autoBindings {
|
||||||
|
b.Input.StructFieldIndex = []int{b.Input.Index}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tests = []struct {
|
||||||
|
Value interface{}
|
||||||
|
Registered []*Dependency
|
||||||
|
Expected []*Binding
|
||||||
|
}{
|
||||||
|
{ // 0.
|
||||||
|
Value: &controller{},
|
||||||
|
Registered: deps,
|
||||||
|
Expected: []*Binding{
|
||||||
|
{
|
||||||
|
Dependency: deps[0],
|
||||||
|
Input: &Input{Index: 0, StructFieldIndex: []int{0}, Type: reflect.TypeOf("")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dependency: deps[1],
|
||||||
|
Input: &Input{Index: 1, StructFieldIndex: []int{1}, Type: serviceTyp},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 1. test controller with pre-defined variables.
|
||||||
|
{
|
||||||
|
Value: &controller{Name: "name_struct", Service: new(serviceImpl)},
|
||||||
|
Expected: []*Binding{
|
||||||
|
{
|
||||||
|
Dependency: NewDependency("name_struct"),
|
||||||
|
Input: &Input{Index: 0, StructFieldIndex: []int{0}, Type: reflect.TypeOf("")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dependency: NewDependency(new(serviceImpl)),
|
||||||
|
Input: &Input{Index: 1, StructFieldIndex: []int{1}, Type: serviceTyp},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 2. test controller with pre-defined variables and other deps with the exact order and value
|
||||||
|
// (deps from non zero values should be registerded only, if not the Dependency:name_struct will fail for sure).
|
||||||
|
{
|
||||||
|
Value: &controller{Name: "name_struct", Service: new(serviceImpl)},
|
||||||
|
Registered: deps,
|
||||||
|
Expected: []*Binding{
|
||||||
|
{
|
||||||
|
Dependency: NewDependency("name_struct"),
|
||||||
|
Input: &Input{Index: 0, StructFieldIndex: []int{0}, Type: reflect.TypeOf("")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dependency: NewDependency(new(serviceImpl)),
|
||||||
|
Input: &Input{Index: 1, StructFieldIndex: []int{1}, Type: serviceTyp},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 3. test embedded structs with anonymous and exported.
|
||||||
|
{
|
||||||
|
Value: &controllerEmbeddingExported{},
|
||||||
|
Registered: depsForAnonymousEmbedded,
|
||||||
|
Expected: []*Binding{
|
||||||
|
{
|
||||||
|
Dependency: depsForAnonymousEmbedded[0],
|
||||||
|
Input: &Input{Index: 0, StructFieldIndex: []int{0, 0}, Type: reflect.TypeOf(0)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dependency: depsForAnonymousEmbedded[1],
|
||||||
|
Input: &Input{Index: 1, StructFieldIndex: []int{1, 0}, Type: reflect.TypeOf(time.Time{})},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 4. test for anonymous but not exported (should still be 2, unexported structs are binded).
|
||||||
|
{
|
||||||
|
Value: &controllerEmbeddingUnexported{},
|
||||||
|
Registered: depsForAnonymousEmbedded,
|
||||||
|
Expected: []*Binding{
|
||||||
|
{
|
||||||
|
Dependency: depsForAnonymousEmbedded[0],
|
||||||
|
Input: &Input{Index: 0, StructFieldIndex: []int{0, 0}, Type: reflect.TypeOf(0)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dependency: depsForAnonymousEmbedded[1],
|
||||||
|
Input: &Input{Index: 1, StructFieldIndex: []int{1, 0}, Type: reflect.TypeOf(time.Time{})},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 5. test for auto-bindings with zero registered.
|
||||||
|
{
|
||||||
|
Value: &controller2{},
|
||||||
|
Registered: nil,
|
||||||
|
Expected: autoBindings,
|
||||||
|
},
|
||||||
|
// 6. test for embedded with named fields which should NOT contain any registered deps
|
||||||
|
// except the two auto-bindings for structs,
|
||||||
|
{
|
||||||
|
Value: &controller2{},
|
||||||
|
Registered: depsForAnonymousEmbedded,
|
||||||
|
Expected: autoBindings,
|
||||||
|
}, // 7. and only embedded struct's fields are readen, otherwise we expect the struct to be a dependency.
|
||||||
|
{
|
||||||
|
Value: &controller2{},
|
||||||
|
Registered: depsForFieldsOfStruct,
|
||||||
|
Expected: []*Binding{
|
||||||
|
{
|
||||||
|
Dependency: depsForFieldsOfStruct[0],
|
||||||
|
Input: &Input{Index: 0, StructFieldIndex: []int{0}, Type: reflect.TypeOf(embedded1{})},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dependency: depsForFieldsOfStruct[1],
|
||||||
|
Input: &Input{Index: 1, StructFieldIndex: []int{1}, Type: reflect.TypeOf(embedded2{})},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 8. test one exported and other not exported.
|
||||||
|
{
|
||||||
|
Value: &controller3{},
|
||||||
|
Registered: []*Dependency{depsForFieldsOfStruct[0]},
|
||||||
|
Expected: []*Binding{
|
||||||
|
{
|
||||||
|
Dependency: depsForFieldsOfStruct[0],
|
||||||
|
Input: &Input{Index: 0, StructFieldIndex: []int{0}, Type: reflect.TypeOf(embedded1{})},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 9. test same as the above but by registering all dependencies.
|
||||||
|
{
|
||||||
|
Value: &controller3{},
|
||||||
|
Registered: depsForFieldsOfStruct,
|
||||||
|
Expected: []*Binding{
|
||||||
|
{
|
||||||
|
Dependency: depsForFieldsOfStruct[0],
|
||||||
|
Input: &Input{Index: 0, StructFieldIndex: []int{0}, Type: reflect.TypeOf(embedded1{})},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 10. test bind an interface{}.
|
||||||
|
{
|
||||||
|
Value: &controller{},
|
||||||
|
Registered: depsInterfaces,
|
||||||
|
Expected: []*Binding{
|
||||||
|
{
|
||||||
|
Dependency: depsInterfaces[0],
|
||||||
|
Input: &Input{Index: 0, StructFieldIndex: []int{0}, Type: reflect.TypeOf("")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
bindings := getBindingsForStruct(reflect.ValueOf(tt.Value), tt.Registered, 0, nil)
|
||||||
|
|
||||||
|
if expected, got := len(tt.Expected), len(bindings); expected != got {
|
||||||
|
t.Logf("[%d] expected bindings length to be: %d but got: %d:\n", i, expected, got)
|
||||||
|
for _, b := range bindings {
|
||||||
|
t.Logf("\t%s\n", b)
|
||||||
|
}
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for j, b := range bindings {
|
||||||
|
if tt.Expected[j] == nil {
|
||||||
|
t.Fatalf("[%d:%d] expected dependency was not found!", i, j)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected := tt.Expected[j]; !expected.Equal(b) {
|
||||||
|
t.Fatalf("[%d:%d] expected binding:\n%s\nbut got:\n%s", i, j, expected, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
166
hero/container.go
Normal file
166
hero/container.go
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
package hero
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdContext "context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
"github.com/kataras/iris/v12/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fatalf(format string, args ...interface{}) {
|
||||||
|
panic(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default is the default container value which can be used for dependencies share.
|
||||||
|
var Default = New()
|
||||||
|
|
||||||
|
// Container contains and delivers the Dependencies that will be binded
|
||||||
|
// to the controller(s) or handler(s) that can be created
|
||||||
|
// using the Container's `Handler` and `Struct` methods.
|
||||||
|
//
|
||||||
|
// This is not exported for being used by everyone, use it only when you want
|
||||||
|
// to share containers between multi mvc.go#Application
|
||||||
|
// or make custom hero handlers that can be used on the standard
|
||||||
|
// iris' APIBuilder.
|
||||||
|
//
|
||||||
|
// For a more high-level structure please take a look at the "mvc.go#Application".
|
||||||
|
type Container struct {
|
||||||
|
// Indicates the path parameter start index for inputs binding.
|
||||||
|
// Defaults to 0.
|
||||||
|
ParamStartIndex int
|
||||||
|
// Sorter specifies how the inputs should be sorted before binded.
|
||||||
|
// Defaults to sort by "thinnest" target empty interface.
|
||||||
|
Sorter Sorter
|
||||||
|
// The dependencies entries.
|
||||||
|
Dependencies []*Dependency
|
||||||
|
// GetErrorHandler should return a valid `ErrorHandler` to handle bindings AND handler dispatch errors.
|
||||||
|
// Defaults to a functon which returns the `DefaultErrorHandler`.
|
||||||
|
GetErrorHandler func(context.Context) ErrorHandler // cannot be nil.
|
||||||
|
}
|
||||||
|
|
||||||
|
var BuiltinDependencies = []*Dependency{
|
||||||
|
// iris context dependency.
|
||||||
|
NewDependency(func(ctx context.Context) context.Context { return ctx }),
|
||||||
|
// standard context dependency.
|
||||||
|
NewDependency(func(ctx context.Context) stdContext.Context {
|
||||||
|
return ctx.Request().Context()
|
||||||
|
}),
|
||||||
|
// iris session dependency.
|
||||||
|
NewDependency(func(ctx context.Context) *sessions.Session {
|
||||||
|
session := sessions.Get(ctx)
|
||||||
|
if session == nil {
|
||||||
|
panic("binding: session is nil - app.Use(sess.Handler()) to fix it")
|
||||||
|
}
|
||||||
|
|
||||||
|
return session
|
||||||
|
}),
|
||||||
|
// time.Time to time.Now dependency.
|
||||||
|
NewDependency(func(ctx context.Context) time.Time {
|
||||||
|
return time.Now()
|
||||||
|
}),
|
||||||
|
|
||||||
|
// payload and param bindings are dynamically allocated and declared at the end of the `binding` source file.
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Container, a container for dependencies and a factory
|
||||||
|
// for handlers and controllers, this is used internally by the `mvc#Application` structure.
|
||||||
|
// Please take a look at the structure's documentation for more information.
|
||||||
|
func New(dependencies ...interface{}) *Container {
|
||||||
|
deps := make([]*Dependency, len(BuiltinDependencies))
|
||||||
|
copy(deps, BuiltinDependencies)
|
||||||
|
|
||||||
|
c := &Container{
|
||||||
|
ParamStartIndex: 0,
|
||||||
|
Sorter: sortByNumMethods,
|
||||||
|
Dependencies: deps,
|
||||||
|
GetErrorHandler: func(context.Context) ErrorHandler {
|
||||||
|
return DefaultErrorHandler
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dependency := range dependencies {
|
||||||
|
c.Register(dependency)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns a new cloned container.
|
||||||
|
// It copies the ErrorHandler, Dependencies and all Options from "c" receiver.
|
||||||
|
func (c *Container) Clone() *Container {
|
||||||
|
cloned := New()
|
||||||
|
cloned.ParamStartIndex = c.ParamStartIndex
|
||||||
|
cloned.GetErrorHandler = c.GetErrorHandler
|
||||||
|
cloned.Sorter = c.Sorter
|
||||||
|
clonedDeps := make([]*Dependency, len(c.Dependencies))
|
||||||
|
copy(clonedDeps, c.Dependencies)
|
||||||
|
cloned.Dependencies = clonedDeps
|
||||||
|
return cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register adds a dependency.
|
||||||
|
// The value can be a single struct value-instance or a function
|
||||||
|
// which has one input and one output, that output type
|
||||||
|
// will be binded to the handler's input argument, if matching.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// - Register(loggerService{prefix: "dev"})
|
||||||
|
// - Register(func(ctx iris.Context) User {...})
|
||||||
|
// - Register(func(User) OtherResponse {...})
|
||||||
|
func Register(dependency interface{}) *Dependency {
|
||||||
|
return Default.Register(dependency)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register adds a dependency.
|
||||||
|
// The value can be a single struct value or a function.
|
||||||
|
// Follow the rules:
|
||||||
|
// * <T>{structValue}
|
||||||
|
// * func(accepts <T>) returns <D> or (<D>, error)
|
||||||
|
// * func(accepts iris.Context) returns <D> or (<D>, error)
|
||||||
|
// * func(accepts1 iris.Context, accepts2 *hero.Input) returns <D> or (<D>, error)
|
||||||
|
//
|
||||||
|
// A Dependency can accept a previous registered dependency and return a new one or the same updated.
|
||||||
|
// * func(accepts1 <D>, accepts2 <T>) returns <E> or (<E>, error) or error
|
||||||
|
// * func(acceptsPathParameter1 string, id uint64) returns <T> or (<T>, error)
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// - Register(loggerService{prefix: "dev"})
|
||||||
|
// - Register(func(ctx iris.Context) User {...})
|
||||||
|
// - Register(func(User) OtherResponse {...})
|
||||||
|
func (c *Container) Register(dependency interface{}) *Dependency {
|
||||||
|
d := NewDependency(dependency, c.Dependencies...)
|
||||||
|
if d.DestType == nil {
|
||||||
|
// prepend the dynamic dependency so it will be tried at the end
|
||||||
|
// (we don't care about performance here, design-time)
|
||||||
|
c.Dependencies = append([]*Dependency{d}, c.Dependencies...)
|
||||||
|
} else {
|
||||||
|
c.Dependencies = append(c.Dependencies, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler accepts a "handler" function which can accept any input arguments that match
|
||||||
|
// with the Container's `Dependencies` and any output result; like string, int (string,int),
|
||||||
|
// custom structs, Result(View | Response) and anything you can imagine.
|
||||||
|
// It returns a standard `iris/context.Handler` which can be used anywhere in an Iris Application,
|
||||||
|
// as middleware or as simple route handler or subdomain's handler.
|
||||||
|
func Handler(fn interface{}) context.Handler {
|
||||||
|
return Default.Handler(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler accepts a handler "fn" function which can accept any input arguments that match
|
||||||
|
// with the Container's `Dependencies` and any output result; like string, int (string,int),
|
||||||
|
// custom structs, Result(View | Response) and more.
|
||||||
|
// It returns a standard `iris/context.Handler` which can be used anywhere in an Iris Application,
|
||||||
|
// as middleware or as simple route handler or subdomain's handler.
|
||||||
|
func (c *Container) Handler(fn interface{}) context.Handler {
|
||||||
|
return makeHandler(fn, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) Struct(ptrValue interface{}) *Struct {
|
||||||
|
return makeStruct(ptrValue, c)
|
||||||
|
}
|
59
hero/container_test.go
Normal file
59
hero/container_test.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package hero_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
. "github.com/kataras/iris/v12/hero"
|
||||||
|
"github.com/kataras/iris/v12/httptest"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errTyp = reflect.TypeOf((*error)(nil)).Elem()
|
||||||
|
|
||||||
|
// isError returns true if "typ" is type of `error`.
|
||||||
|
func isError(typ reflect.Type) bool {
|
||||||
|
return typ.Implements(errTyp)
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
testInput struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
testOutput struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
fn = func(id int, in testInput) testOutput {
|
||||||
|
return testOutput{
|
||||||
|
ID: id,
|
||||||
|
Name: in.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedOutput = testOutput{
|
||||||
|
ID: 42,
|
||||||
|
Name: "makis",
|
||||||
|
}
|
||||||
|
|
||||||
|
input = testInput{
|
||||||
|
Name: "makis",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHeroHandler(t *testing.T) {
|
||||||
|
app := iris.New()
|
||||||
|
|
||||||
|
b := New()
|
||||||
|
postHandler := b.Handler(fn)
|
||||||
|
app.Post("/{id:int}", postHandler)
|
||||||
|
|
||||||
|
e := httptest.New(t, app)
|
||||||
|
path := fmt.Sprintf("/%d", expectedOutput.ID)
|
||||||
|
e.POST(path).WithJSON(input).Expect().Status(httptest.StatusOK).JSON().Equal(expectedOutput)
|
||||||
|
}
|
242
hero/dependency.go
Normal file
242
hero/dependency.go
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
package hero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
DependencyHandler func(ctx context.Context, input *Input) (reflect.Value, error)
|
||||||
|
|
||||||
|
Dependency struct {
|
||||||
|
OriginalValue interface{} // Used for debugging and for logging only.
|
||||||
|
Source Source
|
||||||
|
Handle DependencyHandler
|
||||||
|
// It's the exact type of return to bind, if declared to return <T>, otherwise nil.
|
||||||
|
DestType reflect.Type
|
||||||
|
Static bool
|
||||||
|
// If true then input and dependnecy DestType should be indedical,
|
||||||
|
// not just assiginable to each other.
|
||||||
|
// Example of use case: depenendency like time.Time that we want to be bindable
|
||||||
|
// only to time.Time inputs and not to a service with a `String() string` method that time.Time struct implements too.
|
||||||
|
Explicit bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Explicitly sets Explicit option to true.
|
||||||
|
// See `Dependency.Explicit` field godoc for more.
|
||||||
|
//
|
||||||
|
// Returns itself.
|
||||||
|
func (d *Dependency) Explicitly() *Dependency {
|
||||||
|
d.Explicit = true
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dependency) String() string {
|
||||||
|
sourceLine := d.Source.String()
|
||||||
|
val := d.OriginalValue
|
||||||
|
if val == nil {
|
||||||
|
val = d.Handle
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s (%#+v)", sourceLine, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDependency converts a function or a function which accepts other dependencies or static struct value to a *Dependency.
|
||||||
|
//
|
||||||
|
// See `Container.Handler` for more.
|
||||||
|
func NewDependency(dependency interface{}, funcDependencies ...*Dependency) *Dependency {
|
||||||
|
if dependency == nil {
|
||||||
|
panic(fmt.Sprintf("bad value: nil: %T", dependency))
|
||||||
|
}
|
||||||
|
|
||||||
|
if d, ok := dependency.(*Dependency); ok {
|
||||||
|
// already a *Dependency.
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
v := valueOf(dependency)
|
||||||
|
if !goodVal(v) {
|
||||||
|
panic(fmt.Sprintf("bad value: %#+v", dependency))
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := &Dependency{
|
||||||
|
Source: newSource(v),
|
||||||
|
OriginalValue: dependency,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resolveDependency(v, dest, funcDependencies...) {
|
||||||
|
panic(fmt.Sprintf("bad value: could not resolve a dependency from: %#+v", dependency))
|
||||||
|
}
|
||||||
|
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
// DependencyResolver func(v reflect.Value, dest *Dependency) bool
|
||||||
|
// Resolver DependencyResolver
|
||||||
|
|
||||||
|
func resolveDependency(v reflect.Value, dest *Dependency, funcDependencies ...*Dependency) bool {
|
||||||
|
return fromDependencyHandler(v, dest) ||
|
||||||
|
fromStructValue(v, dest) ||
|
||||||
|
fromFunc(v, dest) ||
|
||||||
|
len(funcDependencies) > 0 && fromDependentFunc(v, dest, funcDependencies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDependencyHandler(v reflect.Value, dest *Dependency) bool {
|
||||||
|
// It's already on the desired form, just return it.
|
||||||
|
dependency := dest.OriginalValue
|
||||||
|
handler, ok := dependency.(DependencyHandler)
|
||||||
|
if !ok {
|
||||||
|
handler, ok = dependency.(func(context.Context, *Input) (reflect.Value, error))
|
||||||
|
if !ok {
|
||||||
|
// It's almost a handler, only the second `Input` argument is missing.
|
||||||
|
if h, is := dependency.(func(context.Context) (reflect.Value, error)); is {
|
||||||
|
handler = func(ctx context.Context, _ *Input) (reflect.Value, error) {
|
||||||
|
return h(ctx)
|
||||||
|
}
|
||||||
|
ok = is
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
dest.Handle = handler
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromStructValue(v reflect.Value, dest *Dependency) bool {
|
||||||
|
if !isFunc(v) {
|
||||||
|
// It's just a static value.
|
||||||
|
handler := func(context.Context, *Input) (reflect.Value, error) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dest.DestType = v.Type()
|
||||||
|
dest.Static = true
|
||||||
|
dest.Handle = handler
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromFunc(v reflect.Value, dest *Dependency) bool {
|
||||||
|
if !isFunc(v) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
typ := v.Type()
|
||||||
|
numIn := typ.NumIn()
|
||||||
|
numOut := typ.NumOut()
|
||||||
|
|
||||||
|
if numIn == 0 {
|
||||||
|
panic("bad value: function has zero inputs")
|
||||||
|
}
|
||||||
|
|
||||||
|
if numOut == 0 {
|
||||||
|
panic("bad value: function has zero outputs")
|
||||||
|
}
|
||||||
|
|
||||||
|
if numOut == 2 && !isError(typ.Out(1)) {
|
||||||
|
panic("bad value: second output should be an error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if numOut > 2 {
|
||||||
|
// - at least one output value
|
||||||
|
// - maximum of two output values
|
||||||
|
// - second output value should be a type of error.
|
||||||
|
panic(fmt.Sprintf("bad value: function has invalid number of output arguments: %v", numOut))
|
||||||
|
}
|
||||||
|
|
||||||
|
var handler DependencyHandler
|
||||||
|
|
||||||
|
firstIsContext := isContext(typ.In(0))
|
||||||
|
secondIsInput := numIn == 2 && typ.In(1) == inputTyp
|
||||||
|
onlyContext := (numIn == 1 && firstIsContext) || (numIn == 2 && firstIsContext && typ.IsVariadic())
|
||||||
|
|
||||||
|
if onlyContext || (firstIsContext && secondIsInput) {
|
||||||
|
handler = handlerFromFunc(v, typ)
|
||||||
|
}
|
||||||
|
|
||||||
|
if handler == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
dest.DestType = typ.Out(0)
|
||||||
|
dest.Handle = handler
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerFromFunc(v reflect.Value, typ reflect.Type) DependencyHandler {
|
||||||
|
// * func(Context, *Input) <T>, func(Context) <T>
|
||||||
|
// * func(Context) <T>, func(Context) <T>
|
||||||
|
// * func(Context, *Input) <T>, func(Context) (<T>, error)
|
||||||
|
// * func(Context) <T>, func(Context) (<T>, error)
|
||||||
|
|
||||||
|
hasErrorOut := typ.NumOut() == 2 // if two, always an error type here.
|
||||||
|
hasInputIn := typ.NumIn() == 2 && typ.In(1) == inputTyp
|
||||||
|
|
||||||
|
return func(ctx context.Context, input *Input) (reflect.Value, error) {
|
||||||
|
inputs := ctx.ReflectValue()
|
||||||
|
if hasInputIn {
|
||||||
|
inputs = append(inputs, input.selfValue)
|
||||||
|
}
|
||||||
|
results := v.Call(inputs)
|
||||||
|
if hasErrorOut {
|
||||||
|
return results[0], toError(results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return results[0], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDependentFunc(v reflect.Value, dest *Dependency, funcDependencies []*Dependency) bool {
|
||||||
|
// * func(<D>...) returns <T>
|
||||||
|
// * func(<D>...) returns error
|
||||||
|
// * func(<D>...) returns <T>, error
|
||||||
|
|
||||||
|
typ := v.Type()
|
||||||
|
if !isFunc(v) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings := getBindingsForFunc(v, funcDependencies, -1 /* parameter bindings are disabled for depent dependencies */)
|
||||||
|
numIn := typ.NumIn()
|
||||||
|
numOut := typ.NumOut()
|
||||||
|
|
||||||
|
firstOutIsError := numOut == 1 && isError(typ.Out(0))
|
||||||
|
secondOutIsError := numOut == 2 && isError(typ.Out(1))
|
||||||
|
|
||||||
|
handler := func(ctx context.Context, _ *Input) (reflect.Value, error) {
|
||||||
|
inputs := make([]reflect.Value, numIn)
|
||||||
|
|
||||||
|
for _, binding := range bindings {
|
||||||
|
input, err := binding.Dependency.Handle(ctx, binding.Input)
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrSeeOther {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyValue, err
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs[binding.Input.Index] = input
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs := v.Call(inputs)
|
||||||
|
if firstOutIsError {
|
||||||
|
return emptyValue, toError(outputs[0])
|
||||||
|
} else if secondOutIsError {
|
||||||
|
return outputs[0], toError(outputs[1])
|
||||||
|
}
|
||||||
|
return outputs[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dest.DestType = typ.Out(0)
|
||||||
|
dest.Handle = handler
|
||||||
|
return true
|
||||||
|
}
|
89
hero/dependency_source.go
Normal file
89
hero/dependency_source.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package hero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Source struct {
|
||||||
|
File string
|
||||||
|
Line int
|
||||||
|
Caller string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSource(fn reflect.Value) Source {
|
||||||
|
var (
|
||||||
|
callerFileName string
|
||||||
|
callerLineNumber int
|
||||||
|
callerName string
|
||||||
|
)
|
||||||
|
|
||||||
|
switch fn.Kind() {
|
||||||
|
case reflect.Func, reflect.Chan, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Slice:
|
||||||
|
pc := fn.Pointer()
|
||||||
|
fpc := runtime.FuncForPC(pc)
|
||||||
|
if fpc != nil {
|
||||||
|
callerFileName, callerLineNumber = fpc.FileLine(pc)
|
||||||
|
callerName = fpc.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
if callerFileName == "" {
|
||||||
|
callerFileName, callerLineNumber = getCaller()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wd, _ := os.Getwd()
|
||||||
|
if relFile, err := filepath.Rel(wd, callerFileName); err == nil {
|
||||||
|
callerFileName = "./" + relFile
|
||||||
|
}
|
||||||
|
|
||||||
|
return Source{
|
||||||
|
File: callerFileName,
|
||||||
|
Line: callerLineNumber,
|
||||||
|
Caller: callerName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSource() Source {
|
||||||
|
filename, line := getCaller()
|
||||||
|
return Source{
|
||||||
|
File: filename,
|
||||||
|
Line: line,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Source) String() string {
|
||||||
|
return fmt.Sprintf("%s:%d", s.File, s.Line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://golang.org/doc/go1.9#callersframes
|
||||||
|
func getCaller() (string, int) {
|
||||||
|
var pcs [32]uintptr
|
||||||
|
n := runtime.Callers(4, pcs[:])
|
||||||
|
frames := runtime.CallersFrames(pcs[:n])
|
||||||
|
|
||||||
|
for {
|
||||||
|
frame, more := frames.Next()
|
||||||
|
file := frame.File
|
||||||
|
|
||||||
|
if strings.HasSuffix(file, "_test.go") {
|
||||||
|
return file, frame.Line
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(file, "/kataras/iris") || strings.Contains(file, "/kataras/iris/_examples") || strings.Contains(file, "iris-contrib/examples") {
|
||||||
|
return file, frame.Line
|
||||||
|
}
|
||||||
|
|
||||||
|
if !more {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "???", 0
|
||||||
|
}
|
170
hero/dependency_test.go
Normal file
170
hero/dependency_test.go
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
package hero_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
. "github.com/kataras/iris/v12/hero"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testDependencyTest struct {
|
||||||
|
Dependency interface{}
|
||||||
|
Expected interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDependency(t *testing.T) {
|
||||||
|
var tests = []testDependencyTest{
|
||||||
|
{
|
||||||
|
Dependency: "myValue",
|
||||||
|
Expected: "myValue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dependency: struct{ Name string }{"name"},
|
||||||
|
Expected: struct{ Name string }{"name"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dependency: func(context.Context, *Input) (reflect.Value, error) {
|
||||||
|
return reflect.ValueOf(42), nil
|
||||||
|
},
|
||||||
|
Expected: 42,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dependency: DependencyHandler(func(context.Context, *Input) (reflect.Value, error) {
|
||||||
|
return reflect.ValueOf(255), nil
|
||||||
|
}),
|
||||||
|
Expected: 255,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dependency: func(context.Context) (reflect.Value, error) {
|
||||||
|
return reflect.ValueOf("OK without Input"), nil
|
||||||
|
},
|
||||||
|
Expected: "OK without Input",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
Dependency: func(context.Context, ...string) (reflect.Value, error) {
|
||||||
|
return reflect.ValueOf("OK variadic ignored"), nil
|
||||||
|
},
|
||||||
|
Expected: "OK variadic ignored",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
Dependency: func(context.Context) reflect.Value {
|
||||||
|
return reflect.ValueOf("OK without Input and error")
|
||||||
|
},
|
||||||
|
Expected: "OK without Input and error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
Dependency: func(context.Context, ...int) reflect.Value {
|
||||||
|
return reflect.ValueOf("OK without error and variadic ignored")
|
||||||
|
},
|
||||||
|
Expected: "OK without error and variadic ignored",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
Dependency: func(context.Context) interface{} {
|
||||||
|
return "1"
|
||||||
|
},
|
||||||
|
Expected: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
Dependency: func(context.Context) interface{} {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
Expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testDependencies(t, tests)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test dependencies that depend on previous one(s).
|
||||||
|
func TestDependentDependency(t *testing.T) {
|
||||||
|
msgBody := "prefix: it is a deep dependency"
|
||||||
|
newMsgBody := msgBody + " new"
|
||||||
|
var tests = []testDependencyTest{
|
||||||
|
// test three level depth and error.
|
||||||
|
{ // 0
|
||||||
|
Dependency: &testServiceImpl{prefix: "prefix:"},
|
||||||
|
Expected: &testServiceImpl{prefix: "prefix:"},
|
||||||
|
},
|
||||||
|
{ // 1
|
||||||
|
Dependency: func(service testService) testMessage {
|
||||||
|
return testMessage{Body: service.Say("it is a deep") + " dependency"}
|
||||||
|
},
|
||||||
|
Expected: testMessage{Body: msgBody},
|
||||||
|
},
|
||||||
|
{ // 2
|
||||||
|
Dependency: func(msg testMessage) string {
|
||||||
|
return msg.Body
|
||||||
|
},
|
||||||
|
Expected: msgBody,
|
||||||
|
},
|
||||||
|
{ // 3
|
||||||
|
Dependency: func(msg testMessage) error {
|
||||||
|
return fmt.Errorf(msg.Body)
|
||||||
|
},
|
||||||
|
Expected: fmt.Errorf(msgBody),
|
||||||
|
},
|
||||||
|
// Test depend on more than one previous registered dependencies and require a before-previous one.
|
||||||
|
{ // 4
|
||||||
|
Dependency: func(body string, msg testMessage) string {
|
||||||
|
if body != msg.Body {
|
||||||
|
t.Fatalf("body[%s] != msg.Body[%s]", body, msg.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return body + " new"
|
||||||
|
},
|
||||||
|
Expected: newMsgBody,
|
||||||
|
},
|
||||||
|
// Test dependency order by expecting the first <string> returning value and not the later-on registered dependency(#4).
|
||||||
|
// 5
|
||||||
|
{
|
||||||
|
Dependency: func(body string) string {
|
||||||
|
return body
|
||||||
|
},
|
||||||
|
Expected: newMsgBody,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testDependencies(t, tests)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDependencies(t *testing.T, tests []testDependencyTest) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
c := New()
|
||||||
|
for i, tt := range tests {
|
||||||
|
d := c.Register(tt.Dependency)
|
||||||
|
|
||||||
|
if d == nil {
|
||||||
|
t.Fatalf("[%d] expected %#+v to be converted to a valid dependency", i, tt)
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := d.Handle(context.NewContext(nil), &Input{})
|
||||||
|
|
||||||
|
if expectError := isError(reflect.TypeOf(tt.Expected)); expectError {
|
||||||
|
val = reflect.ValueOf(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("[%d] expected a nil error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !val.CanInterface() {
|
||||||
|
t.Fatalf("[%d] expected output value to be accessible: %T", i, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := fmt.Sprintf("%#+v", tt.Expected), fmt.Sprintf("%#+v", val.Interface()); expected != got {
|
||||||
|
t.Fatalf("[%d] expected return value to be:\n%s\nbut got:\n%s", i, expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// t.Logf("[%d] %s", i, d)
|
||||||
|
// t.Logf("[%d] output: %#+v", i, val.Interface())
|
||||||
|
}
|
||||||
|
}
|
211
hero/di/di.go
211
hero/di/di.go
|
@ -1,211 +0,0 @@
|
||||||
// Package di provides dependency injection for the Iris Hero and Iris MVC new features.
|
|
||||||
// It's used internally by "hero" and "mvc" packages.
|
|
||||||
package di
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
// ErrorHandler is the optional interface to handle errors per hero func,
|
|
||||||
// see `mvc/Application#HandleError` for MVC application-level error handler registration too.
|
|
||||||
//
|
|
||||||
// Handles non-nil errors return from a hero handler or a controller's method (see `DispatchFuncResult`)
|
|
||||||
// and (from v12.1.8) the error may return from a request-scoped dynamic dependency (see `MakeReturnValue`).
|
|
||||||
ErrorHandler interface {
|
|
||||||
HandleError(ctx context.Context, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorHandlerFunc implements the `ErrorHandler`.
|
|
||||||
// It describes the type defnition for an error handler.
|
|
||||||
ErrorHandlerFunc func(ctx context.Context, err error)
|
|
||||||
)
|
|
||||||
|
|
||||||
// HandleError fires when the `DispatchFuncResult` or `MakereturnValue` return a non-nil error.
|
|
||||||
func (fn ErrorHandlerFunc) HandleError(ctx context.Context, err error) {
|
|
||||||
fn(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultErrorHandler is the default error handler will be fired on
|
|
||||||
// any error from registering a request-scoped dynamic dependency and on a controller's method failure.
|
|
||||||
var DefaultErrorHandler ErrorHandler = ErrorHandlerFunc(func(ctx context.Context, err error) {
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.StatusCode(400)
|
|
||||||
ctx.WriteString(err.Error())
|
|
||||||
ctx.StopExecution()
|
|
||||||
})
|
|
||||||
|
|
||||||
var emptyValue reflect.Value
|
|
||||||
|
|
||||||
// DefaultFallbackBinder used to bind any oprhan inputs. Its error is handled by the `ErrorHandler`.
|
|
||||||
var DefaultFallbackBinder FallbackBinder = func(ctx context.Context, input OrphanInput) (newValue reflect.Value, err error) {
|
|
||||||
wasPtr := input.Type.Kind() == reflect.Ptr
|
|
||||||
|
|
||||||
newValue = reflect.New(IndirectType(input.Type))
|
|
||||||
ptr := newValue.Interface()
|
|
||||||
|
|
||||||
switch ctx.GetContentTypeRequested() {
|
|
||||||
case context.ContentXMLHeaderValue:
|
|
||||||
err = ctx.ReadXML(ptr)
|
|
||||||
case context.ContentYAMLHeaderValue:
|
|
||||||
err = ctx.ReadYAML(ptr)
|
|
||||||
case context.ContentFormHeaderValue:
|
|
||||||
err = ctx.ReadQuery(ptr)
|
|
||||||
case context.ContentFormMultipartHeaderValue:
|
|
||||||
err = ctx.ReadForm(ptr)
|
|
||||||
default:
|
|
||||||
err = ctx.ReadJSON(ptr)
|
|
||||||
// json
|
|
||||||
}
|
|
||||||
|
|
||||||
// if err != nil {
|
|
||||||
// return emptyValue, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
if !wasPtr {
|
|
||||||
newValue = newValue.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
return newValue, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Struct is being used to return a new injector based on
|
|
||||||
// a struct value instance, if it contains fields that the types of those
|
|
||||||
// are matching with one or more of the `Values` then they are binded
|
|
||||||
// with the injector's `Inject` and `InjectElem` methods.
|
|
||||||
func Struct(s interface{}, values ...reflect.Value) *StructInjector {
|
|
||||||
if s == nil {
|
|
||||||
return &StructInjector{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MakeStructInjector(
|
|
||||||
ValueOf(s),
|
|
||||||
SortByNumMethods,
|
|
||||||
Values(values).CloneWithFieldsOf(s)...,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Func is being used to return a new injector based on
|
|
||||||
// a function, if it contains input arguments that the types of those
|
|
||||||
// are matching with one or more of the `Values` then they are binded
|
|
||||||
// to the function's input argument when called
|
|
||||||
// with the injector's `Inject` method.
|
|
||||||
func Func(fn interface{}, values ...reflect.Value) *FuncInjector {
|
|
||||||
if fn == nil {
|
|
||||||
return &FuncInjector{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MakeFuncInjector(
|
|
||||||
ValueOf(fn),
|
|
||||||
values...,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// D is the Dependency Injection container,
|
|
||||||
// it contains the Values that can be changed before the injectors.
|
|
||||||
// `Struct` and the `Func` methods returns an injector for specific
|
|
||||||
// struct instance-value or function.
|
|
||||||
type D struct {
|
|
||||||
Values
|
|
||||||
|
|
||||||
fallbackBinder FallbackBinder
|
|
||||||
errorHandler ErrorHandler
|
|
||||||
sorter Sorter
|
|
||||||
}
|
|
||||||
|
|
||||||
// OrphanInput represents an input without registered dependency.
|
|
||||||
// Used to help the framework (or the caller) auto-resolve it by the request.
|
|
||||||
type OrphanInput struct {
|
|
||||||
// Index int // function or struct field index.
|
|
||||||
Type reflect.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
// FallbackBinder represents a handler of oprhan input values, handler's input arguments or controller's fields.
|
|
||||||
type FallbackBinder func(ctx context.Context, input OrphanInput) (reflect.Value, error)
|
|
||||||
|
|
||||||
// New creates and returns a new Dependency Injection container.
|
|
||||||
// See `Values` field and `Func` and `Struct` methods for more.
|
|
||||||
func New() *D {
|
|
||||||
return &D{
|
|
||||||
errorHandler: DefaultErrorHandler,
|
|
||||||
fallbackBinder: DefaultFallbackBinder,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FallbackBinder adds a binder which will handle any oprhan input values.
|
|
||||||
// See `FallbackBinder` type.
|
|
||||||
func (d *D) FallbackBinder(fallbackBinder FallbackBinder) *D {
|
|
||||||
d.fallbackBinder = fallbackBinder
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorHandler adds a handler which will be fired when a handler's second output argument is error and it's not nil
|
|
||||||
// or when a request-scoped dynamic function dependency's second output argument is error and it's not nil.
|
|
||||||
func (d *D) ErrorHandler(errorHandler ErrorHandler) *D {
|
|
||||||
d.errorHandler = errorHandler
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort sets the fields and valid bindable values sorter for struct injection.
|
|
||||||
func (d *D) Sort(with Sorter) *D {
|
|
||||||
d.sorter = with
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone returns a new Dependency Injection container, it adopts the
|
|
||||||
// parent's (current "D") hijacker, good func type checker, sorter and all dependencies values.
|
|
||||||
func (d *D) Clone() *D {
|
|
||||||
return &D{
|
|
||||||
Values: d.Values.Clone(),
|
|
||||||
fallbackBinder: d.fallbackBinder,
|
|
||||||
errorHandler: d.errorHandler,
|
|
||||||
sorter: d.sorter,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Struct is being used to return a new injector based on
|
|
||||||
// a struct value instance, if it contains fields that the types of those
|
|
||||||
// are matching with one or more of the `Values` then they are binded
|
|
||||||
// with the injector's `Inject` and `InjectElem` methods.
|
|
||||||
func (d *D) Struct(s interface{}) *StructInjector {
|
|
||||||
if s == nil {
|
|
||||||
return &StructInjector{}
|
|
||||||
}
|
|
||||||
|
|
||||||
injector := MakeStructInjector(
|
|
||||||
ValueOf(s),
|
|
||||||
d.sorter,
|
|
||||||
d.Values.CloneWithFieldsOf(s)...,
|
|
||||||
)
|
|
||||||
|
|
||||||
injector.ErrorHandler = d.errorHandler
|
|
||||||
injector.FallbackBinder = d.fallbackBinder
|
|
||||||
|
|
||||||
return injector
|
|
||||||
}
|
|
||||||
|
|
||||||
// Func is being used to return a new injector based on
|
|
||||||
// a function, if it contains input arguments that the types of those
|
|
||||||
// are matching with one or more of the `Values` then they are binded
|
|
||||||
// to the function's input argument when called
|
|
||||||
// with the injector's `Inject` method.
|
|
||||||
func (d *D) Func(fn interface{}) *FuncInjector {
|
|
||||||
if fn == nil {
|
|
||||||
return &FuncInjector{}
|
|
||||||
}
|
|
||||||
|
|
||||||
injector := MakeFuncInjector(
|
|
||||||
ValueOf(fn),
|
|
||||||
d.Values...,
|
|
||||||
)
|
|
||||||
|
|
||||||
injector.ErrorHandler = d.errorHandler
|
|
||||||
injector.FallbackBinder = d.fallbackBinder
|
|
||||||
|
|
||||||
return injector
|
|
||||||
}
|
|
263
hero/di/func.go
263
hero/di/func.go
|
@ -1,263 +0,0 @@
|
||||||
package di
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
targetFuncInput struct {
|
|
||||||
Object *BindObject
|
|
||||||
InputIndex int
|
|
||||||
}
|
|
||||||
|
|
||||||
// FuncInjector keeps the data that are needed in order to do the binding injection
|
|
||||||
// as fast as possible and with the best possible and safest way.
|
|
||||||
FuncInjector struct {
|
|
||||||
// the original function, is being used
|
|
||||||
// only the .Call, which is referring to the same function, always.
|
|
||||||
fn reflect.Value
|
|
||||||
typ reflect.Type
|
|
||||||
FallbackBinder FallbackBinder
|
|
||||||
ErrorHandler ErrorHandler
|
|
||||||
|
|
||||||
inputs []*targetFuncInput
|
|
||||||
// Length is the number of the valid, final binded input arguments.
|
|
||||||
Length int
|
|
||||||
// Valid is True when `Length` is > 0, it's statically set-ed for
|
|
||||||
// performance reasons.
|
|
||||||
Has bool
|
|
||||||
|
|
||||||
lost []*missingInput // Author's note: don't change this to a map.
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type missingInput struct {
|
|
||||||
index int // the function's input argument's index.
|
|
||||||
found bool
|
|
||||||
remaining Values
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *FuncInjector) miss(index int, remaining Values) {
|
|
||||||
s.lost = append(s.lost, &missingInput{
|
|
||||||
index: index,
|
|
||||||
remaining: remaining,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeFuncInjector returns a new func injector, which will be the object
|
|
||||||
// that the caller should use to bind input arguments of the "fn" function.
|
|
||||||
//
|
|
||||||
// The hijack and the goodFunc are optional, the "values" is the dependencies collection.
|
|
||||||
func MakeFuncInjector(fn reflect.Value, values ...reflect.Value) *FuncInjector {
|
|
||||||
typ := IndirectType(fn.Type())
|
|
||||||
s := &FuncInjector{
|
|
||||||
fn: fn,
|
|
||||||
typ: typ,
|
|
||||||
FallbackBinder: DefaultFallbackBinder,
|
|
||||||
ErrorHandler: DefaultErrorHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !IsFunc(typ) {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
defer s.refresh()
|
|
||||||
|
|
||||||
n := typ.NumIn()
|
|
||||||
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
inTyp := typ.In(i)
|
|
||||||
|
|
||||||
if b, ok := tryBindContext(inTyp); ok {
|
|
||||||
s.inputs = append(s.inputs, &targetFuncInput{
|
|
||||||
InputIndex: i,
|
|
||||||
Object: b,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
matched := false
|
|
||||||
|
|
||||||
for j, v := range values {
|
|
||||||
if s.addValue(i, v) {
|
|
||||||
matched = true
|
|
||||||
// remove this value, so it will not try to get binded
|
|
||||||
// again, a next value even with the same type is able to be
|
|
||||||
// used to other input arg. One value per input argument, order
|
|
||||||
// matters if same type of course.
|
|
||||||
// if len(values) > j+1 {
|
|
||||||
values = append(values[:j], values[j+1:]...)
|
|
||||||
//}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: (already working on it) clean up or even re-write the whole di, hero and some of the mvc,
|
|
||||||
// this is a dirty but working-solution for #1449.
|
|
||||||
// Limitations:
|
|
||||||
// - last input argument
|
|
||||||
// - not able to customize it other than DefaultFallbackBinder on MVC (on hero it can be customized)
|
|
||||||
// - the "di" package is now depends on context package which is not an import-cycle issue, it's not imported there.
|
|
||||||
if i == n-1 {
|
|
||||||
if v.Type() == autoBindingTyp && s.FallbackBinder != nil {
|
|
||||||
|
|
||||||
canFallback := true
|
|
||||||
if k := inTyp.Kind(); k == reflect.Ptr {
|
|
||||||
if inTyp.Elem().Kind() != reflect.Struct {
|
|
||||||
canFallback = false
|
|
||||||
}
|
|
||||||
} else if k != reflect.Struct {
|
|
||||||
canFallback = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if canFallback {
|
|
||||||
matched = true
|
|
||||||
|
|
||||||
s.inputs = append(s.inputs, &targetFuncInput{
|
|
||||||
InputIndex: i,
|
|
||||||
Object: &BindObject{
|
|
||||||
Type: inTyp,
|
|
||||||
BindType: Dynamic,
|
|
||||||
ReturnValue: func(ctx context.Context) reflect.Value {
|
|
||||||
value, err := s.FallbackBinder(ctx, OrphanInput{Type: inTyp})
|
|
||||||
if err != nil {
|
|
||||||
if s.ErrorHandler != nil {
|
|
||||||
s.ErrorHandler.HandleError(ctx, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !matched {
|
|
||||||
// if no binding for this input argument,
|
|
||||||
// this will make the func injector invalid state,
|
|
||||||
// but before this let's make a list of failed
|
|
||||||
// inputs, so they can be used for a re-try
|
|
||||||
// with different set of binding "values".
|
|
||||||
s.miss(i, values) // send the remaining dependencies values.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *FuncInjector) refresh() {
|
|
||||||
s.Length = len(s.inputs)
|
|
||||||
s.Has = s.Length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// AutoBindingValue a fake type to expliclty set the return value of hero.AutoBinding.
|
|
||||||
type AutoBindingValue struct{}
|
|
||||||
|
|
||||||
var autoBindingTyp = reflect.TypeOf(AutoBindingValue{})
|
|
||||||
|
|
||||||
func (s *FuncInjector) addValue(inputIndex int, value reflect.Value) bool {
|
|
||||||
defer s.refresh()
|
|
||||||
|
|
||||||
if s.typ.NumIn() < inputIndex {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
inTyp := s.typ.In(inputIndex)
|
|
||||||
|
|
||||||
// the binded values to the func's inputs.
|
|
||||||
b, err := MakeBindObject(value, s.ErrorHandler)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.IsAssignable(inTyp) {
|
|
||||||
// fmt.Printf("binded input index: %d for type: %s and value: %v with dependency: %v\n",
|
|
||||||
// inputIndex, b.Type.String(), inTyp.String(), b)
|
|
||||||
s.inputs = append(s.inputs, &targetFuncInput{
|
|
||||||
InputIndex: inputIndex,
|
|
||||||
Object: &b,
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry used to add missing dependencies, i.e path parameter builtin bindings if not already exists
|
|
||||||
// in the `hero.Handler`, once, only for that func injector.
|
|
||||||
func (s *FuncInjector) Retry(retryFn func(inIndex int, inTyp reflect.Type, remainingValues Values) (reflect.Value, bool)) bool {
|
|
||||||
for _, missing := range s.lost {
|
|
||||||
if missing.found {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidIndex := missing.index
|
|
||||||
|
|
||||||
inTyp := s.typ.In(invalidIndex)
|
|
||||||
v, ok := retryFn(invalidIndex, inTyp, missing.remaining)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.addValue(invalidIndex, v) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// if this value completes an invalid index
|
|
||||||
// then remove this from the invalid input indexes.
|
|
||||||
missing.found = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.Length == s.typ.NumIn()
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a debug trace text.
|
|
||||||
func (s *FuncInjector) String() (trace string) {
|
|
||||||
for i, in := range s.inputs {
|
|
||||||
bindmethodTyp := bindTypeString(in.Object.BindType)
|
|
||||||
typIn := s.typ.In(in.InputIndex)
|
|
||||||
// remember: on methods that are part of a struct (i.e controller)
|
|
||||||
// the input index = 1 is the begggining instead of the 0,
|
|
||||||
// because the 0 is the controller receiver pointer of the method.
|
|
||||||
trace += fmt.Sprintf("[%d] %s binding: '%s' for input position: %d and type: '%s'\n",
|
|
||||||
i+1, bindmethodTyp, in.Object.Type.String(), in.InputIndex, typIn.String())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject accepts an already created slice of input arguments
|
|
||||||
// and fills them, the "ctx" is optional and it's used
|
|
||||||
// on the dependencies that depends on one or more input arguments, these are the "ctx".
|
|
||||||
func (s *FuncInjector) Inject(ctx context.Context, in *[]reflect.Value) {
|
|
||||||
args := *in
|
|
||||||
for _, input := range s.inputs {
|
|
||||||
input.Object.Assign(ctx, func(v reflect.Value) {
|
|
||||||
// fmt.Printf("assign input index: %d for value: %v of type: %s\n",
|
|
||||||
// input.InputIndex, v.String(), v.Type().Name())
|
|
||||||
|
|
||||||
args[input.InputIndex] = v
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
*in = args
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call calls the "Inject" with a new slice of input arguments
|
|
||||||
// that are computed by the length of the input argument from the MakeFuncInjector's "fn" function.
|
|
||||||
//
|
|
||||||
// If the function needs a receiver, so
|
|
||||||
// the caller should be able to in[0] = receiver before injection,
|
|
||||||
// then the `Inject` method should be used instead.
|
|
||||||
func (s *FuncInjector) Call(ctx context.Context) []reflect.Value {
|
|
||||||
in := make([]reflect.Value, s.Length)
|
|
||||||
s.Inject(ctx, &in)
|
|
||||||
return s.fn.Call(in)
|
|
||||||
}
|
|
|
@ -1,151 +0,0 @@
|
||||||
package di
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BindType is the type of a binded object/value, it's being used to
|
|
||||||
// check if the value is accessible after a function call with a "ctx" when needed ( Dynamic type)
|
|
||||||
// or it's just a struct value (a service | Static type).
|
|
||||||
type BindType uint32
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Static is the simple assignable value, a static value.
|
|
||||||
Static BindType = iota
|
|
||||||
// Dynamic returns a value but it depends on some input arguments from the caller,
|
|
||||||
// on serve time.
|
|
||||||
Dynamic
|
|
||||||
)
|
|
||||||
|
|
||||||
func bindTypeString(typ BindType) string {
|
|
||||||
switch typ {
|
|
||||||
case Dynamic:
|
|
||||||
return "Dynamic"
|
|
||||||
default:
|
|
||||||
return "Static"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BindObject contains the dependency value's read-only information.
|
|
||||||
// FuncInjector and StructInjector keeps information about their
|
|
||||||
// input arguments/or fields, these properties contain a `BindObject` inside them.
|
|
||||||
type BindObject struct {
|
|
||||||
Type reflect.Type // the Type of 'Value' or the type of the returned 'ReturnValue' .
|
|
||||||
Value reflect.Value
|
|
||||||
|
|
||||||
BindType BindType
|
|
||||||
ReturnValue func(ctx context.Context) reflect.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeBindObject accepts any "v" value, struct, pointer or a function
|
|
||||||
// and a type checker that is used to check if the fields (if "v.elem()" is struct)
|
|
||||||
// or the input arguments (if "v.elem()" is func)
|
|
||||||
// are valid to be included as the final object's dependencies, even if the caller added more
|
|
||||||
// the "di" is smart enough to select what each "v" needs and what not before serve time.
|
|
||||||
func MakeBindObject(v reflect.Value, errorHandler ErrorHandler) (b BindObject, err error) {
|
|
||||||
if IsFunc(v) {
|
|
||||||
b.BindType = Dynamic
|
|
||||||
b.ReturnValue, b.Type, err = MakeReturnValue(v, errorHandler)
|
|
||||||
} else {
|
|
||||||
b.BindType = Static
|
|
||||||
b.Type = v.Type()
|
|
||||||
b.Value = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func tryBindContext(fieldOrFuncInput reflect.Type) (*BindObject, bool) {
|
|
||||||
if !IsContext(fieldOrFuncInput) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
// this is being used on both func injector and struct injector.
|
|
||||||
// if the func's input argument or the struct's field is a type of Context
|
|
||||||
// then we can do a fast binding using the ctxValue
|
|
||||||
// which is used as slice of reflect.Value, because of the final method's `Call`.
|
|
||||||
return &BindObject{
|
|
||||||
Type: contextTyp,
|
|
||||||
BindType: Dynamic,
|
|
||||||
ReturnValue: func(ctx context.Context) reflect.Value {
|
|
||||||
return ctx.ReflectValue()[0]
|
|
||||||
},
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
var errBad = errors.New("bad")
|
|
||||||
|
|
||||||
// MakeReturnValue takes any function
|
|
||||||
// that accept custom values and returns something,
|
|
||||||
// it returns a binder function, which accepts a slice of reflect.Value
|
|
||||||
// and returns a single one reflect.Value for that.
|
|
||||||
// It's being used to resolve the input parameters on a "x" consumer faster.
|
|
||||||
//
|
|
||||||
// The "fn" can have the following form:
|
|
||||||
// `func(myService) MyViewModel`.
|
|
||||||
//
|
|
||||||
// The return type of the "fn" should be a value instance, not a pointer.
|
|
||||||
// The binder function should return just one value.
|
|
||||||
func MakeReturnValue(fn reflect.Value, errorHandler ErrorHandler) (func(context.Context) reflect.Value, reflect.Type, error) {
|
|
||||||
typ := IndirectType(fn.Type())
|
|
||||||
|
|
||||||
// invalid if not a func.
|
|
||||||
if typ.Kind() != reflect.Func {
|
|
||||||
return nil, typ, errBad
|
|
||||||
}
|
|
||||||
|
|
||||||
n := typ.NumOut()
|
|
||||||
|
|
||||||
// invalid if not returns one single value or two values but the second is not an error.
|
|
||||||
if !(n == 1 || (n == 2 && IsError(typ.Out(1)))) {
|
|
||||||
return nil, typ, errBad
|
|
||||||
}
|
|
||||||
|
|
||||||
if !goodFunc(typ) {
|
|
||||||
return nil, typ, errBad
|
|
||||||
}
|
|
||||||
|
|
||||||
firstOutTyp := typ.Out(0)
|
|
||||||
firstZeroOutVal := reflect.New(firstOutTyp).Elem()
|
|
||||||
|
|
||||||
bf := func(ctx context.Context) reflect.Value {
|
|
||||||
results := fn.Call(ctx.ReflectValue())
|
|
||||||
if n == 2 {
|
|
||||||
// two, second is always error.
|
|
||||||
errVal := results[1]
|
|
||||||
if !errVal.IsNil() {
|
|
||||||
if errorHandler != nil {
|
|
||||||
errorHandler.HandleError(ctx, errVal.Interface().(error))
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstZeroOutVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v := results[0]
|
|
||||||
if !v.IsValid() { // check the first value, second is error.
|
|
||||||
return firstZeroOutVal
|
|
||||||
}
|
|
||||||
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
return bf, firstOutTyp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsAssignable checks if "to" type can be used as "b.Value/ReturnValue".
|
|
||||||
func (b *BindObject) IsAssignable(to reflect.Type) bool {
|
|
||||||
return equalTypes(b.Type, to)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign sets the values to a setter, "toSetter" contains the setter, so the caller
|
|
||||||
// can use it for multiple and different structs/functions as well.
|
|
||||||
func (b *BindObject) Assign(ctx context.Context, toSetter func(reflect.Value)) {
|
|
||||||
if b.BindType == Dynamic {
|
|
||||||
toSetter(b.ReturnValue(ctx))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toSetter(b.Value)
|
|
||||||
}
|
|
|
@ -1,261 +0,0 @@
|
||||||
package di
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EmptyIn is just an empty slice of reflect.Value.
|
|
||||||
var EmptyIn = []reflect.Value{}
|
|
||||||
|
|
||||||
// IsZero returns true if a value is nil.
|
|
||||||
// Remember; fields to be checked should be exported otherwise it returns false.
|
|
||||||
// Notes for users:
|
|
||||||
// Boolean's zero value is false, even if not set-ed.
|
|
||||||
// UintXX are not zero on 0 because they are pointers to.
|
|
||||||
func IsZero(v reflect.Value) bool {
|
|
||||||
switch v.Kind() {
|
|
||||||
case reflect.Struct:
|
|
||||||
zero := true
|
|
||||||
for i := 0; i < v.NumField(); i++ {
|
|
||||||
zero = zero && IsZero(v.Field(i))
|
|
||||||
}
|
|
||||||
|
|
||||||
if typ := v.Type(); typ != nil && v.IsValid() {
|
|
||||||
f, ok := typ.MethodByName("IsZero")
|
|
||||||
// if not found
|
|
||||||
// if has input arguments (1 is for the value receiver, so > 1 for the actual input args)
|
|
||||||
// if output argument is not boolean
|
|
||||||
// then skip this IsZero user-defined function.
|
|
||||||
if !ok || f.Type.NumIn() > 1 || f.Type.NumOut() != 1 && f.Type.Out(0).Kind() != reflect.Bool {
|
|
||||||
return zero
|
|
||||||
}
|
|
||||||
|
|
||||||
method := v.Method(f.Index)
|
|
||||||
// no needed check but:
|
|
||||||
if method.IsValid() && !method.IsNil() {
|
|
||||||
// it shouldn't panic here.
|
|
||||||
zero = method.Call(EmptyIn)[0].Interface().(bool)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return zero
|
|
||||||
case reflect.Func, reflect.Map, reflect.Slice:
|
|
||||||
return v.IsNil()
|
|
||||||
case reflect.Array:
|
|
||||||
zero := true
|
|
||||||
for i := 0; i < v.Len(); i++ {
|
|
||||||
zero = zero && IsZero(v.Index(i))
|
|
||||||
}
|
|
||||||
return zero
|
|
||||||
}
|
|
||||||
// if not any special type then use the reflect's .Zero
|
|
||||||
// usually for fields, but remember if it's boolean and it's false
|
|
||||||
// then it's zero, even if set-ed.
|
|
||||||
|
|
||||||
if !v.CanInterface() {
|
|
||||||
// if can't interface, i.e return value from unexported field or method then return false
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
zero := reflect.Zero(v.Type())
|
|
||||||
return v.Interface() == zero.Interface()
|
|
||||||
}
|
|
||||||
|
|
||||||
var errTyp = reflect.TypeOf((*error)(nil)).Elem()
|
|
||||||
|
|
||||||
// IsError returns true if "typ" is type of `error`.
|
|
||||||
func IsError(typ reflect.Type) bool {
|
|
||||||
return typ.Implements(errTyp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IndirectValue returns the reflect.Value that "v" points to.
|
|
||||||
// If "v" is a nil pointer, Indirect returns a zero Value.
|
|
||||||
// If "v" is not a pointer, Indirect returns v.
|
|
||||||
func IndirectValue(v reflect.Value) reflect.Value {
|
|
||||||
if k := v.Kind(); k == reflect.Ptr { //|| k == reflect.Interface {
|
|
||||||
return v.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValueOf returns the reflect.Value of "o".
|
|
||||||
// If "o" is already a reflect.Value returns "o".
|
|
||||||
func ValueOf(o interface{}) reflect.Value {
|
|
||||||
if v, ok := o.(reflect.Value); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
return reflect.ValueOf(o)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValuesOf same as `ValueOf` but accepts a slice of
|
|
||||||
// somethings and returns a slice of reflect.Value.
|
|
||||||
func ValuesOf(valuesAsInterface []interface{}) (values []reflect.Value) {
|
|
||||||
for _, v := range valuesAsInterface {
|
|
||||||
values = append(values, ValueOf(v))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// IndirectType returns the value of a pointer-type "typ".
|
|
||||||
// If "typ" is a pointer, array, chan, map or slice it returns its Elem,
|
|
||||||
// otherwise returns the typ as it's.
|
|
||||||
func IndirectType(typ reflect.Type) reflect.Type {
|
|
||||||
switch typ.Kind() {
|
|
||||||
case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
|
|
||||||
return typ.Elem()
|
|
||||||
}
|
|
||||||
return typ
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsNil same as `reflect.IsNil` but a bit safer to use, returns false if not a correct type.
|
|
||||||
func IsNil(v reflect.Value) bool {
|
|
||||||
k := v.Kind()
|
|
||||||
switch k {
|
|
||||||
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
|
|
||||||
return v.IsNil()
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func goodVal(v reflect.Value) bool {
|
|
||||||
switch v.Kind() {
|
|
||||||
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice:
|
|
||||||
if v.IsNil() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.IsValid()
|
|
||||||
}
|
|
||||||
|
|
||||||
var contextTyp = reflect.TypeOf((*context.Context)(nil)).Elem()
|
|
||||||
|
|
||||||
// IsContext returns true if the "inTyp" is a type of Context.
|
|
||||||
func IsContext(inTyp reflect.Type) bool {
|
|
||||||
return inTyp.Implements(contextTyp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func goodFunc(fn reflect.Type) bool {
|
|
||||||
// valid if that single input arg is a typeof context.Context
|
|
||||||
// or first argument is context.Context and second argument is a variadic, which is ignored (i.e new sessions#Start).
|
|
||||||
return (fn.NumIn() == 1 || (fn.NumIn() == 2 && fn.IsVariadic())) && IsContext(fn.In(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsFunc returns true if the passed type is function.
|
|
||||||
func IsFunc(kindable interface {
|
|
||||||
Kind() reflect.Kind
|
|
||||||
}) bool {
|
|
||||||
return kindable.Kind() == reflect.Func
|
|
||||||
}
|
|
||||||
|
|
||||||
var reflectValueType = reflect.TypeOf(reflect.Value{})
|
|
||||||
|
|
||||||
func equalTypes(got reflect.Type, expected reflect.Type) bool {
|
|
||||||
if got == expected {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// fmt.Printf("got: %s expected: %s\n", got.String(), expected.String())
|
|
||||||
// if accepts an interface, check if the given "got" type does
|
|
||||||
// implement this "expected" user handler's input argument.
|
|
||||||
if expected.Kind() == reflect.Interface {
|
|
||||||
// fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", expected.String(), got.String())
|
|
||||||
// return got.Implements(expected)
|
|
||||||
// return expected.AssignableTo(got)
|
|
||||||
return got.AssignableTo(expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// for controller's fields only.
|
|
||||||
func structFieldIgnored(f reflect.StructField) bool {
|
|
||||||
if !f.Anonymous {
|
|
||||||
return true // if not anonymous(embedded), ignore it.
|
|
||||||
}
|
|
||||||
|
|
||||||
s := f.Tag.Get("ignore")
|
|
||||||
return s == "true" // if has an ignore tag then ignore it.
|
|
||||||
}
|
|
||||||
|
|
||||||
// for controller's fields only. Explicit set a stateless to a field
|
|
||||||
// in order to make the controller a Stateless one even if no other dynamic dependencies exist.
|
|
||||||
func structFieldStateless(f reflect.StructField) bool {
|
|
||||||
s := f.Tag.Get("stateless")
|
|
||||||
return s == "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
type field struct {
|
|
||||||
Type reflect.Type
|
|
||||||
Name string // the actual name.
|
|
||||||
Index []int // the index of the field, slice if it's part of a embedded struct
|
|
||||||
CanSet bool // is true if it's exported.
|
|
||||||
}
|
|
||||||
|
|
||||||
// NumFields returns the total number of fields, and the embedded, even if the embedded struct is not exported,
|
|
||||||
// it will check for its exported fields.
|
|
||||||
func NumFields(elemTyp reflect.Type, skipUnexported bool) int {
|
|
||||||
return len(lookupFields(elemTyp, skipUnexported, nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func lookupFields(elemTyp reflect.Type, skipUnexported bool, parentIndex []int) (fields []field) {
|
|
||||||
if elemTyp.Kind() != reflect.Struct {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, n := 0, elemTyp.NumField(); i < n; i++ {
|
|
||||||
f := elemTyp.Field(i)
|
|
||||||
if IndirectType(f.Type).Kind() == reflect.Struct &&
|
|
||||||
!structFieldIgnored(f) && !structFieldStateless(f) {
|
|
||||||
fields = append(fields, lookupFields(f.Type, skipUnexported, append(parentIndex, i))...)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip unexported fields here,
|
|
||||||
// after the check for embedded structs, these can be binded if their
|
|
||||||
// fields are exported.
|
|
||||||
isExported := f.PkgPath == ""
|
|
||||||
if skipUnexported && !isExported {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
index := []int{i}
|
|
||||||
if len(parentIndex) > 0 {
|
|
||||||
index = append(parentIndex, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
field := field{
|
|
||||||
Type: f.Type,
|
|
||||||
Name: f.Name,
|
|
||||||
Index: index,
|
|
||||||
CanSet: isExported,
|
|
||||||
}
|
|
||||||
|
|
||||||
fields = append(fields, field)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupNonZeroFieldsValues lookup for filled fields based on the "v" struct value instance.
|
|
||||||
// It returns a slice of reflect.Value (same type as `Values`) that can be binded,
|
|
||||||
// like the end-developer's custom values.
|
|
||||||
func LookupNonZeroFieldsValues(v reflect.Value, skipUnexported bool) (bindValues []reflect.Value) {
|
|
||||||
elem := IndirectValue(v)
|
|
||||||
fields := lookupFields(IndirectType(v.Type()), skipUnexported, nil)
|
|
||||||
|
|
||||||
for _, f := range fields {
|
|
||||||
if fieldVal := elem.FieldByIndex(f.Index); /*f.Type.Kind() == reflect.Ptr &&*/
|
|
||||||
goodVal(fieldVal) && !IsZero(fieldVal) {
|
|
||||||
// fmt.Printf("[%d][field index = %d] append to bindValues: %s = %s\n", i, f.Index[0], f.Name, fieldVal.String())
|
|
||||||
bindValues = append(bindValues, fieldVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,341 +0,0 @@
|
||||||
package di
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Scope is the struct injector's struct value scope/permant state.
|
|
||||||
// See `Stateless` and `Singleton`.
|
|
||||||
type Scope uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Stateless is the scope that the struct should be different on each binding,
|
|
||||||
// think it like `Request Scoped`, per-request struct for mvc.
|
|
||||||
Stateless Scope = iota
|
|
||||||
// Singleton is the scope that the struct is the same
|
|
||||||
// between calls, it has no dynamic dependencies or
|
|
||||||
// any unexported fields that is not seted on creation,
|
|
||||||
// so it doesn't need to be created on each call/request.
|
|
||||||
Singleton
|
|
||||||
)
|
|
||||||
|
|
||||||
// read-only on runtime.
|
|
||||||
var scopeNames = map[Scope]string{
|
|
||||||
Stateless: "Stateless",
|
|
||||||
Singleton: "Singleton",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return "Stateless" for 0 or "Singleton" for 1.
|
|
||||||
func (scope Scope) String() string {
|
|
||||||
name, ok := scopeNames[scope]
|
|
||||||
if !ok {
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
|
||||||
targetStructField struct {
|
|
||||||
Object *BindObject
|
|
||||||
FieldIndex []int
|
|
||||||
// ValueIndex is used mostly for debugging, it's the order of the registered binded value targets to that field.
|
|
||||||
ValueIndex int
|
|
||||||
}
|
|
||||||
|
|
||||||
// StructInjector keeps the data that are needed in order to do the binding injection
|
|
||||||
// as fast as possible and with the best possible and safest way.
|
|
||||||
StructInjector struct {
|
|
||||||
initRef reflect.Value
|
|
||||||
initRefAsSlice []reflect.Value // useful when the struct is passed on a func as input args via reflection.
|
|
||||||
elemType reflect.Type
|
|
||||||
//
|
|
||||||
fields []*targetStructField
|
|
||||||
// is true when contains bindable fields and it's a valid target struct,
|
|
||||||
// it maybe 0 but struct may contain unexported fields or exported but no bindable (Stateless)
|
|
||||||
// see `setState`.
|
|
||||||
Has bool
|
|
||||||
CanInject bool // if any bindable fields when the state is NOT singleton.
|
|
||||||
Scope Scope
|
|
||||||
|
|
||||||
FallbackBinder FallbackBinder
|
|
||||||
ErrorHandler ErrorHandler
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *StructInjector) countBindType(typ BindType) (n int) {
|
|
||||||
for _, f := range s.fields {
|
|
||||||
if f.Object.BindType == typ {
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sorter is the type for sort customization of a struct's fields
|
|
||||||
// and its available bindable values.
|
|
||||||
//
|
|
||||||
// Sorting applies only when a field can accept more than one registered value.
|
|
||||||
type Sorter func(t1 reflect.Type, t2 reflect.Type) bool
|
|
||||||
|
|
||||||
// SortByNumMethods is a builtin sorter to sort fields and values
|
|
||||||
// based on their type and its number of methods, highest number of methods goes first.
|
|
||||||
//
|
|
||||||
// It is the default sorter on package-level struct injector function `Struct`.
|
|
||||||
var SortByNumMethods Sorter = func(t1 reflect.Type, t2 reflect.Type) bool {
|
|
||||||
if t1.Kind() != t2.Kind() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if k := t1.Kind(); k == reflect.Interface || k == reflect.Struct {
|
|
||||||
return t1.NumMethod() > t2.NumMethod()
|
|
||||||
} else if k != reflect.Struct {
|
|
||||||
return false // non-structs goes last.
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeStructInjector returns a new struct injector, which will be the object
|
|
||||||
// that the caller should use to bind exported fields or
|
|
||||||
// embedded unexported fields that contain exported fields
|
|
||||||
// of the "v" struct value or pointer.
|
|
||||||
//
|
|
||||||
// The hijack and the goodFunc are optional, the "values" is the dependencies collection.
|
|
||||||
func MakeStructInjector(v reflect.Value, sorter Sorter, values ...reflect.Value) *StructInjector {
|
|
||||||
s := &StructInjector{
|
|
||||||
initRef: v,
|
|
||||||
initRefAsSlice: []reflect.Value{v},
|
|
||||||
elemType: IndirectType(v.Type()),
|
|
||||||
FallbackBinder: DefaultFallbackBinder,
|
|
||||||
ErrorHandler: DefaultErrorHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optionally check and keep good values only here,
|
|
||||||
// but not required because they are already checked by users of this function.
|
|
||||||
//
|
|
||||||
// for i, v := range values {
|
|
||||||
// if !goodVal(v) || IsZero(v) {
|
|
||||||
// if last := len(values) - 1; last > i {
|
|
||||||
// values = append(values[:i], values[i+1:]...)
|
|
||||||
// } else {
|
|
||||||
// values = values[0:last]
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
visited := make(map[int]struct{}) // add a visited to not add twice a single value (09-Jul-2019).
|
|
||||||
fields := lookupFields(s.elemType, true, nil)
|
|
||||||
|
|
||||||
// for idx, val := range values {
|
|
||||||
// fmt.Printf("[%d] value type [%s] value name [%s]\n", idx, val.Type().String(), val.String())
|
|
||||||
// }
|
|
||||||
|
|
||||||
if len(fields) > 1 && sorter != nil {
|
|
||||||
sort.Slice(fields, func(i, j int) bool {
|
|
||||||
return sorter(fields[i].Type, fields[j].Type)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range fields {
|
|
||||||
// fmt.Printf("[%d] field type [%s] value name [%s]\n", idx, f.Type.String(), f.Name)
|
|
||||||
if b, ok := tryBindContext(f.Type); ok {
|
|
||||||
s.fields = append(s.fields, &targetStructField{
|
|
||||||
FieldIndex: f.Index,
|
|
||||||
Object: b,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var possibleValues []*targetStructField
|
|
||||||
|
|
||||||
for idx, val := range values {
|
|
||||||
if _, alreadySet := visited[idx]; alreadySet {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// the binded values to the struct's fields.
|
|
||||||
b, err := MakeBindObject(val, nil)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
// return s // if error stop here.
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.IsAssignable(f.Type) {
|
|
||||||
possibleValues = append(possibleValues, &targetStructField{
|
|
||||||
ValueIndex: idx,
|
|
||||||
FieldIndex: f.Index,
|
|
||||||
Object: &b,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if l := len(possibleValues); l == 0 {
|
|
||||||
continue
|
|
||||||
} else if l > 1 && sorter != nil {
|
|
||||||
sort.Slice(possibleValues, func(i, j int) bool {
|
|
||||||
// if first.Object.BindType != second.Object.BindType {
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if first.Object.BindType != Static { // dynamic goes last.
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
return sorter(possibleValues[i].Object.Type, possibleValues[j].Object.Type)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
tf := possibleValues[0]
|
|
||||||
visited[tf.ValueIndex] = struct{}{}
|
|
||||||
// fmt.Printf("bind the object to the field: %s at index: %#v and type: %s\n", f.Name, f.Index, f.Type.String())
|
|
||||||
s.fields = append(s.fields, tf)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Has = len(s.fields) > 0
|
|
||||||
// set the overall state of this injector.
|
|
||||||
s.fillStruct()
|
|
||||||
s.setState()
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the state, once.
|
|
||||||
// Here the "initRef" have already the static bindings and the manually-filled fields.
|
|
||||||
func (s *StructInjector) setState() {
|
|
||||||
// note for zero length of struct's fields:
|
|
||||||
// if struct doesn't contain any field
|
|
||||||
// so both of the below variables will be 0,
|
|
||||||
// so it's a singleton.
|
|
||||||
// At the other hand the `s.HasFields` maybe false
|
|
||||||
// but the struct may contain UNEXPORTED fields or non-bindable fields (request-scoped on both cases)
|
|
||||||
// so a new controller/struct at the caller side should be initialized on each request,
|
|
||||||
// we should not depend on the `HasFields` for singleton or no, this is the reason I
|
|
||||||
// added the `.State` now.
|
|
||||||
|
|
||||||
staticBindingsFieldsLength := s.countBindType(Static)
|
|
||||||
allStructFieldsLength := NumFields(s.elemType, false)
|
|
||||||
// check if unexported(and exported) fields are set-ed manually or via binding (at this time we have all fields set-ed inside the "initRef")
|
|
||||||
// i.e &Controller{unexportedField: "my value"}
|
|
||||||
// or dependencies values = "my value" and Controller struct {Field string}
|
|
||||||
// if so then set the temp staticBindingsFieldsLength to that number, so for example:
|
|
||||||
// if static binding length is 0
|
|
||||||
// but an unexported field is set-ed then act that as singleton.
|
|
||||||
|
|
||||||
if allStructFieldsLength > staticBindingsFieldsLength {
|
|
||||||
structFieldsUnexportedNonZero := LookupNonZeroFieldsValues(s.initRef, false)
|
|
||||||
staticBindingsFieldsLength = len(structFieldsUnexportedNonZero)
|
|
||||||
}
|
|
||||||
|
|
||||||
// println("allStructFieldsLength: ", allStructFieldsLength)
|
|
||||||
// println("staticBindingsFieldsLength: ", staticBindingsFieldsLength)
|
|
||||||
|
|
||||||
// if the number of static values binded is equal to the
|
|
||||||
// total struct's fields(including unexported fields this time) then set as singleton.
|
|
||||||
if staticBindingsFieldsLength == allStructFieldsLength {
|
|
||||||
s.Scope = Singleton
|
|
||||||
// the default is `Stateless`, which means that a new instance should be created
|
|
||||||
// on each inject action by the caller.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.CanInject = s.Scope == Stateless && s.Has
|
|
||||||
}
|
|
||||||
|
|
||||||
// fill the static bindings values once.
|
|
||||||
func (s *StructInjector) fillStruct() {
|
|
||||||
if !s.Has {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// if field is Static then set it to the value that passed by the caller,
|
|
||||||
// so will have the static bindings already and we can just use that value instead
|
|
||||||
// of creating new instance.
|
|
||||||
destElem := IndirectValue(s.initRef)
|
|
||||||
for _, f := range s.fields {
|
|
||||||
// if field is Static then set it to the value that passed by the caller,
|
|
||||||
// so will have the static bindings already and we can just use that value instead
|
|
||||||
// of creating new instance.
|
|
||||||
if f.Object.BindType == Static {
|
|
||||||
destElem.FieldByIndex(f.FieldIndex).Set(f.Object.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a debug trace message.
|
|
||||||
func (s *StructInjector) String() (trace string) {
|
|
||||||
for i, f := range s.fields {
|
|
||||||
elemField := s.elemType.FieldByIndex(f.FieldIndex)
|
|
||||||
|
|
||||||
format := "\t[%d] %s binding: %#+v for field '%s %s'"
|
|
||||||
if len(s.fields) > i+1 {
|
|
||||||
format += "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
if !f.Object.Value.IsValid() {
|
|
||||||
continue // probably a Context.
|
|
||||||
}
|
|
||||||
|
|
||||||
valuePresent := f.Object.Value.Interface()
|
|
||||||
|
|
||||||
if f.Object.BindType == Dynamic {
|
|
||||||
valuePresent = f.Object.Type.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
trace += fmt.Sprintf(format, i+1, bindTypeString(f.Object.BindType), valuePresent, elemField.Name, elemField.Type.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject accepts a destination struct and any optional context value(s),
|
|
||||||
// hero and mvc takes only one context value and this is the `context.Context`.
|
|
||||||
// It applies the bindings to the "dest" struct. It calls the InjectElem.
|
|
||||||
func (s *StructInjector) Inject(ctx context.Context, dest interface{}) {
|
|
||||||
if dest == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
v := IndirectValue(ValueOf(dest))
|
|
||||||
s.InjectElem(ctx, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InjectElem same as `Inject` but accepts a reflect.Value and bind the necessary fields directly.
|
|
||||||
func (s *StructInjector) InjectElem(ctx context.Context, destElem reflect.Value) {
|
|
||||||
for _, f := range s.fields {
|
|
||||||
f.Object.Assign(ctx, func(v reflect.Value) {
|
|
||||||
ff := destElem.FieldByIndex(f.FieldIndex)
|
|
||||||
if !v.Type().AssignableTo(ff.Type()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
destElem.FieldByIndex(f.FieldIndex).Set(v)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Acquire returns a new value of the struct or
|
|
||||||
// the same struct that is used for resolving the dependencies.
|
|
||||||
// If the scope is marked as singleton then it returns the first instance,
|
|
||||||
// otherwise it creates new and returns it.
|
|
||||||
//
|
|
||||||
// See `Singleton` and `Stateless` for more.
|
|
||||||
func (s *StructInjector) Acquire() reflect.Value {
|
|
||||||
if s.Scope == Singleton {
|
|
||||||
return s.initRef
|
|
||||||
}
|
|
||||||
return reflect.New(s.elemType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AcquireSlice same as `Acquire` but it returns a slice of
|
|
||||||
// values structs, this can be used when a struct is passed as an input parameter
|
|
||||||
// on a function, again if singleton then it returns a pre-created slice which contains
|
|
||||||
// the first struct value given by the struct injector's user.
|
|
||||||
func (s *StructInjector) AcquireSlice() []reflect.Value {
|
|
||||||
if s.Scope == Singleton {
|
|
||||||
return s.initRefAsSlice
|
|
||||||
}
|
|
||||||
return []reflect.Value{reflect.New(s.elemType)}
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
package di
|
|
||||||
|
|
||||||
import "reflect"
|
|
||||||
|
|
||||||
// Values is a shortcut of []reflect.Value,
|
|
||||||
// it makes easier to remove and add dependencies.
|
|
||||||
type Values []reflect.Value
|
|
||||||
|
|
||||||
// NewValues returns new empty (dependencies) values.
|
|
||||||
func NewValues() Values {
|
|
||||||
return Values{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone returns a copy of the current values.
|
|
||||||
func (bv Values) Clone() Values {
|
|
||||||
if n := len(bv); n > 0 {
|
|
||||||
values := make(Values, n)
|
|
||||||
copy(values, bv)
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloneWithFieldsOf will return a copy of the current values
|
|
||||||
// plus the "s" struct's fields that are filled(non-zero) by the caller.
|
|
||||||
func (bv Values) CloneWithFieldsOf(s interface{}) Values {
|
|
||||||
values := bv.Clone()
|
|
||||||
|
|
||||||
// add the manual filled fields to the dependencies.
|
|
||||||
filledFieldValues := LookupNonZeroFieldsValues(ValueOf(s), true)
|
|
||||||
|
|
||||||
for i, filled := range filledFieldValues {
|
|
||||||
for _, v := range values {
|
|
||||||
// do NOT keep duplicate equal values (09-Jul-2019).
|
|
||||||
if reflect.DeepEqual(v, filled) {
|
|
||||||
if last := len(filledFieldValues) - 1; last > i {
|
|
||||||
filledFieldValues = append(filledFieldValues[:i], filledFieldValues[i+1:]...)
|
|
||||||
} else {
|
|
||||||
filledFieldValues = filledFieldValues[0:last]
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
values = append(values, filledFieldValues...)
|
|
||||||
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the length of the current "bv" values slice.
|
|
||||||
func (bv Values) Len() int {
|
|
||||||
return len(bv)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds values as dependencies, if the struct's fields
|
|
||||||
// or the function's input arguments needs them, they will be defined as
|
|
||||||
// bindings (at build-time) and they will be used (at serve-time).
|
|
||||||
func (bv *Values) Add(values ...interface{}) {
|
|
||||||
bv.AddValues(ValuesOf(values)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddValues same as `Add` but accepts reflect.Value dependencies instead of interface{}
|
|
||||||
// and appends them to the list if they pass some checks.
|
|
||||||
func (bv *Values) AddValues(values ...reflect.Value) {
|
|
||||||
for _, v := range values {
|
|
||||||
if !goodVal(v) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
*bv = append(*bv, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove unbinds a binding value based on the type,
|
|
||||||
// it returns true if at least one field is not binded anymore.
|
|
||||||
//
|
|
||||||
// The "n" indicates the number of elements to remove, if <=0 then it's 1,
|
|
||||||
// this is useful because you may have bind more than one value to two or more fields
|
|
||||||
// with the same type.
|
|
||||||
func (bv *Values) Remove(value interface{}, n int) bool {
|
|
||||||
return bv.remove(reflect.TypeOf(value), n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bv *Values) remove(typ reflect.Type, n int) (ok bool) {
|
|
||||||
input := *bv
|
|
||||||
for i, in := range input {
|
|
||||||
if equalTypes(in.Type(), typ) {
|
|
||||||
ok = true
|
|
||||||
input = input[:i+copy(input[i:], input[i+1:])]
|
|
||||||
if n > 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*bv = input
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Has returns true if a binder responsible to
|
|
||||||
// bind and return a type of "typ" is already registered to this controller.
|
|
||||||
func (bv Values) Has(value interface{}) bool {
|
|
||||||
return bv.valueTypeExists(reflect.TypeOf(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bv Values) valueTypeExists(typ reflect.Type) bool {
|
|
||||||
for _, in := range bv {
|
|
||||||
if equalTypes(in.Type(), typ) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddOnce binds a value to the controller's field with the same type,
|
|
||||||
// if it's not binded already.
|
|
||||||
//
|
|
||||||
// Returns false if binded already or the value is not the proper one for binding,
|
|
||||||
// otherwise true.
|
|
||||||
func (bv *Values) AddOnce(value interface{}) bool {
|
|
||||||
return bv.addIfNotExists(reflect.ValueOf(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bv *Values) addIfNotExists(v reflect.Value) bool {
|
|
||||||
|
|
||||||
typ := v.Type() // no element, raw things here.
|
|
||||||
|
|
||||||
if !goodVal(v) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if bv.valueTypeExists(typ) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
bv.Add(v)
|
|
||||||
return true
|
|
||||||
}
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/context"
|
"github.com/kataras/iris/v12/context"
|
||||||
"github.com/kataras/iris/v12/hero/di"
|
|
||||||
|
|
||||||
"github.com/fatih/structs"
|
"github.com/fatih/structs"
|
||||||
)
|
)
|
||||||
|
@ -58,80 +57,15 @@ type compatibleErr interface {
|
||||||
Error() string
|
Error() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultErrStatusCode is the default error status code (400)
|
// dispatchErr writes the error to the response.
|
||||||
// when the response contains an error which is not nil.
|
func dispatchErr(ctx context.Context, status int, err error) bool {
|
||||||
var DefaultErrStatusCode = 400
|
if err == nil {
|
||||||
|
return false
|
||||||
// DispatchErr writes the error to the response.
|
|
||||||
func DispatchErr(ctx context.Context, status int, err error) {
|
|
||||||
if status < 400 {
|
|
||||||
status = DefaultErrStatusCode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.StatusCode(status)
|
ctx.StatusCode(status)
|
||||||
if text := err.Error(); text != "" {
|
DefaultErrorHandler.HandleError(ctx, err)
|
||||||
ctx.WriteString(text)
|
return true
|
||||||
ctx.StopExecution()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DispatchCommon is being used internally to send
|
|
||||||
// commonly used data to the response writer with a smart way.
|
|
||||||
func DispatchCommon(ctx context.Context,
|
|
||||||
statusCode int, contentType string, content []byte, v interface{}, err error, found bool) {
|
|
||||||
// if we have a false boolean as a return value
|
|
||||||
// then skip everything and fire a not found,
|
|
||||||
// we even don't care about the given status code or the object or the content.
|
|
||||||
if !found {
|
|
||||||
ctx.NotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
status := statusCode
|
|
||||||
if status == 0 {
|
|
||||||
status = 200
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
DispatchErr(ctx, status, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// write the status code, the rest will need that before any write ofc.
|
|
||||||
ctx.StatusCode(status)
|
|
||||||
if contentType == "" {
|
|
||||||
// to respect any ctx.ContentType(...) call
|
|
||||||
// especially if v is not nil.
|
|
||||||
contentType = ctx.GetContentType()
|
|
||||||
}
|
|
||||||
|
|
||||||
if v != nil {
|
|
||||||
if d, ok := v.(Result); ok {
|
|
||||||
// write the content type now (internal check for empty value)
|
|
||||||
ctx.ContentType(contentType)
|
|
||||||
d.Dispatch(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(contentType, context.ContentJavascriptHeaderValue) {
|
|
||||||
_, err = ctx.JSONP(v)
|
|
||||||
} else if strings.HasPrefix(contentType, context.ContentXMLHeaderValue) {
|
|
||||||
_, err = ctx.XML(v, context.XML{Indent: " "})
|
|
||||||
} else {
|
|
||||||
// defaults to json if content type is missing or its application/json.
|
|
||||||
_, err = ctx.JSON(v, context.JSON{Indent: " "})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
DispatchErr(ctx, status, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.ContentType(contentType)
|
|
||||||
// .Write even len(content) == 0 , this should be called in order to call the internal tryWriteHeader,
|
|
||||||
// it will not cost anything.
|
|
||||||
ctx.Write(content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DispatchFuncResult is being used internally to resolve
|
// DispatchFuncResult is being used internally to resolve
|
||||||
|
@ -163,9 +97,9 @@ func DispatchCommon(ctx context.Context,
|
||||||
// Result or (Result, error) and so on...
|
// Result or (Result, error) and so on...
|
||||||
//
|
//
|
||||||
// where Get is an HTTP METHOD.
|
// where Get is an HTTP METHOD.
|
||||||
func DispatchFuncResult(ctx context.Context, errorHandler di.ErrorHandler, values []reflect.Value) {
|
func dispatchFuncResult(ctx context.Context, values []reflect.Value) error {
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -184,10 +118,6 @@ func DispatchFuncResult(ctx context.Context, errorHandler di.ErrorHandler, value
|
||||||
// for content type (or json default) and send the custom data object
|
// for content type (or json default) and send the custom data object
|
||||||
// except when found == false or err != nil.
|
// except when found == false or err != nil.
|
||||||
custom interface{}
|
custom interface{}
|
||||||
// if not nil then check for its status code,
|
|
||||||
// if not status code or < 400 then set it as DefaultErrStatusCode
|
|
||||||
// and fire the error's text.
|
|
||||||
err error
|
|
||||||
// if false then skip everything and fire 404.
|
// if false then skip everything and fire 404.
|
||||||
found = true // defaults to true of course, otherwise will break :)
|
found = true // defaults to true of course, otherwise will break :)
|
||||||
)
|
)
|
||||||
|
@ -291,23 +221,16 @@ func DispatchFuncResult(ctx context.Context, errorHandler di.ErrorHandler, value
|
||||||
// it's raw content, get the latest
|
// it's raw content, get the latest
|
||||||
content = value
|
content = value
|
||||||
case compatibleErr:
|
case compatibleErr:
|
||||||
if value == nil || di.IsNil(v) {
|
if value == nil || isNil(v) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if errorHandler != nil {
|
|
||||||
errorHandler.HandleError(ctx, value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = value
|
|
||||||
if statusCode < 400 {
|
if statusCode < 400 {
|
||||||
statusCode = DefaultErrStatusCode
|
statusCode = DefaultErrStatusCode
|
||||||
}
|
}
|
||||||
// break on first error, error should be in the end but we
|
|
||||||
// need to know break the dispatcher if any error.
|
|
||||||
// at the end; we don't want to write anything to the response if error is not nil.
|
|
||||||
|
|
||||||
|
ctx.StatusCode(statusCode)
|
||||||
|
return value
|
||||||
default:
|
default:
|
||||||
// else it's a custom struct or a dispatcher, we'll decide later
|
// else it's a custom struct or a dispatcher, we'll decide later
|
||||||
// because content type and status code matters
|
// because content type and status code matters
|
||||||
|
@ -316,7 +239,7 @@ func DispatchFuncResult(ctx context.Context, errorHandler di.ErrorHandler, value
|
||||||
if custom == nil {
|
if custom == nil {
|
||||||
// if it's a pointer to struct/map.
|
// if it's a pointer to struct/map.
|
||||||
|
|
||||||
if di.IsNil(v) {
|
if isNil(v) {
|
||||||
// if just a ptr to struct with no content type given
|
// if just a ptr to struct with no content type given
|
||||||
// then try to get the previous response writer's content type,
|
// then try to get the previous response writer's content type,
|
||||||
// and if that is empty too then force-it to application/json
|
// and if that is empty too then force-it to application/json
|
||||||
|
@ -338,7 +261,61 @@ func DispatchFuncResult(ctx context.Context, errorHandler di.ErrorHandler, value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchCommon(ctx, statusCode, contentType, content, custom, err, found)
|
return dispatchCommon(ctx, statusCode, contentType, content, custom, found)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchCommon is being used internally to send
|
||||||
|
// commonly used data to the response writer with a smart way.
|
||||||
|
func dispatchCommon(ctx context.Context,
|
||||||
|
statusCode int, contentType string, content []byte, v interface{}, found bool) error {
|
||||||
|
// if we have a false boolean as a return value
|
||||||
|
// then skip everything and fire a not found,
|
||||||
|
// we even don't care about the given status code or the object or the content.
|
||||||
|
if !found {
|
||||||
|
ctx.NotFound()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
status := statusCode
|
||||||
|
if status == 0 {
|
||||||
|
status = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the status code, the rest will need that before any write ofc.
|
||||||
|
ctx.StatusCode(status)
|
||||||
|
if contentType == "" {
|
||||||
|
// to respect any ctx.ContentType(...) call
|
||||||
|
// especially if v is not nil.
|
||||||
|
contentType = ctx.GetContentType()
|
||||||
|
}
|
||||||
|
|
||||||
|
if v != nil {
|
||||||
|
if d, ok := v.(Result); ok {
|
||||||
|
// write the content type now (internal check for empty value)
|
||||||
|
ctx.ContentType(contentType)
|
||||||
|
d.Dispatch(ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if strings.HasPrefix(contentType, context.ContentJavascriptHeaderValue) {
|
||||||
|
_, err = ctx.JSONP(v)
|
||||||
|
} else if strings.HasPrefix(contentType, context.ContentXMLHeaderValue) {
|
||||||
|
_, err = ctx.XML(v, context.XML{Indent: " "})
|
||||||
|
} else {
|
||||||
|
// defaults to json if content type is missing or its application/json.
|
||||||
|
_, err = ctx.JSON(v, context.JSON{Indent: " "})
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ContentType(contentType)
|
||||||
|
// .Write even len(content) == 0 , this should be called in order to call the internal tryWriteHeader,
|
||||||
|
// it will not cost anything.
|
||||||
|
_, err := ctx.Write(content)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response completes the `methodfunc.Result` interface.
|
// Response completes the `methodfunc.Result` interface.
|
||||||
|
@ -401,7 +378,12 @@ func (r Response) Dispatch(ctx context.Context) {
|
||||||
r.Content = []byte(s)
|
r.Content = []byte(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchCommon(ctx, r.Code, r.ContentType, r.Content, r.Object, r.Err, true)
|
if dispatchErr(ctx, r.Code, r.Err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := dispatchCommon(ctx, r.Code, r.ContentType, r.Content, r.Object, true)
|
||||||
|
dispatchErr(ctx, r.Code, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// View completes the `hero.Result` interface.
|
// View completes the `hero.Result` interface.
|
||||||
|
@ -445,13 +427,7 @@ func ensureExt(s string) string {
|
||||||
// Dispatch writes the template filename, template layout and (any) data to the client.
|
// Dispatch writes the template filename, template layout and (any) data to the client.
|
||||||
// Completes the `Result` interface.
|
// Completes the `Result` interface.
|
||||||
func (r View) Dispatch(ctx context.Context) { // r as Response view.
|
func (r View) Dispatch(ctx context.Context) { // r as Response view.
|
||||||
if r.Err != nil {
|
if dispatchErr(ctx, r.Code, r.Err) {
|
||||||
if r.Code < 400 {
|
|
||||||
r.Code = DefaultErrStatusCode
|
|
||||||
}
|
|
||||||
ctx.StatusCode(r.Code)
|
|
||||||
ctx.WriteString(r.Err.Error())
|
|
||||||
ctx.StopExecution()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,7 +458,7 @@ func (r View) Dispatch(ctx context.Context) { // r as Response view.
|
||||||
setViewData(ctx, m)
|
setViewData(ctx, m)
|
||||||
} else if m, ok := r.Data.(context.Map); ok {
|
} else if m, ok := r.Data.(context.Map); ok {
|
||||||
setViewData(ctx, m)
|
setViewData(ctx, m)
|
||||||
} else if di.IndirectValue(reflect.ValueOf(r.Data)).Kind() == reflect.Struct {
|
} else if reflect.Indirect(reflect.ValueOf(r.Data)).Kind() == reflect.Struct {
|
||||||
setViewData(ctx, structs.Map(r))
|
setViewData(ctx, structs.Map(r))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
162
hero/handler.go
162
hero/handler.go
|
@ -3,91 +3,115 @@ package hero
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/context"
|
"github.com/kataras/iris/v12/context"
|
||||||
"github.com/kataras/iris/v12/hero/di"
|
|
||||||
|
|
||||||
"github.com/kataras/golog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// var genericFuncTyp = reflect.TypeOf(func(context.Context) reflect.Value { return reflect.Value{} })
|
type (
|
||||||
|
ErrorHandler interface {
|
||||||
|
HandleError(context.Context, error)
|
||||||
|
}
|
||||||
|
ErrorHandlerFunc func(context.Context, error)
|
||||||
|
)
|
||||||
|
|
||||||
// // IsGenericFunc reports whether the "inTyp" is a type of func(Context) interface{}.
|
func (fn ErrorHandlerFunc) HandleError(ctx context.Context, err error) {
|
||||||
// func IsGenericFunc(inTyp reflect.Type) bool {
|
fn(ctx, err)
|
||||||
// return inTyp == genericFuncTyp
|
|
||||||
// }
|
|
||||||
|
|
||||||
// checks if "handler" is context.Handler: func(context.Context).
|
|
||||||
func isContextHandler(handler interface{}) (context.Handler, bool) {
|
|
||||||
h, ok := handler.(context.Handler)
|
|
||||||
return h, ok
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateHandler(handler interface{}) error {
|
var (
|
||||||
if typ := reflect.TypeOf(handler); !di.IsFunc(typ) {
|
// DefaultErrStatusCode is the default error status code (400)
|
||||||
return fmt.Errorf("handler expected to be a kind of func but got typeof(%s)", typ.String())
|
// when the response contains a non-nil error or a request-scoped binding error occur.
|
||||||
}
|
DefaultErrStatusCode = 400
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeHandler accepts a "handler" function which can accept any input arguments that match
|
// DefaultErrorHandler is the default error handler which is fired
|
||||||
// with the "values" types and any output result, that matches the hero types, like string, int (string,int),
|
// when a function returns a non-nil error or a request-scoped dependency failed to binded.
|
||||||
// custom structs, Result(View | Response) and anything that you can imagine,
|
DefaultErrorHandler = ErrorHandlerFunc(func(ctx context.Context, err error) {
|
||||||
// and returns a low-level `context/iris.Handler` which can be used anywhere in the Iris Application,
|
if status := ctx.GetStatusCode(); status == 0 || !context.StatusCodeNotSuccessful(status) {
|
||||||
// as middleware or as simple route handler or party handler or subdomain handler-router.
|
ctx.StatusCode(DefaultErrStatusCode)
|
||||||
func makeHandler(handler interface{}, errorHandler di.ErrorHandler, values ...reflect.Value) (context.Handler, error) {
|
|
||||||
if err := validateHandler(handler); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if h, is := isContextHandler(handler); is {
|
|
||||||
golog.Warnf("the standard API to register a context handler could be used instead")
|
|
||||||
return h, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fn := reflect.ValueOf(handler)
|
|
||||||
n := fn.Type().NumIn()
|
|
||||||
|
|
||||||
if n == 0 {
|
|
||||||
h := func(ctx context.Context) {
|
|
||||||
DispatchFuncResult(ctx, nil, fn.Call(di.EmptyIn))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return h, nil
|
ctx.WriteString(err.Error())
|
||||||
|
ctx.StopExecution()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrSeeOther may be returned from a dependency handler to skip a specific dependency
|
||||||
|
// based on custom logic.
|
||||||
|
ErrSeeOther = fmt.Errorf("see other")
|
||||||
|
// ErrStopExecution may be returned from a dependency handler to stop
|
||||||
|
// and return the execution of the function without error (it calls ctx.StopExecution() too).
|
||||||
|
// It may be occurred from request-scoped dependencies as well.
|
||||||
|
ErrStopExecution = fmt.Errorf("stop execution")
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeHandler(fn interface{}, c *Container) context.Handler {
|
||||||
|
if fn == nil {
|
||||||
|
panic("makeHandler: function is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
funcInjector := di.Func(fn, values...)
|
// 0. A normal handler.
|
||||||
funcInjector.ErrorHandler = errorHandler
|
if handler, ok := isHandler(fn); ok {
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
valid := funcInjector.Length == n
|
// 1. A handler which returns just an error, handle it faster.
|
||||||
|
if handlerWithErr, ok := isHandlerWithError(fn); ok {
|
||||||
if !valid {
|
return func(ctx context.Context) {
|
||||||
// is invalid when input len and values are not match
|
if err := handlerWithErr(ctx); err != nil {
|
||||||
// or their types are not match, we will take look at the
|
c.GetErrorHandler(ctx).HandleError(ctx, err)
|
||||||
// second statement, here we will re-try it
|
}
|
||||||
// using binders for path parameters: string, int, int64, uint8, uint64, bool and so on.
|
|
||||||
// We don't have access to the path, so neither to the macros here,
|
|
||||||
// but in mvc. So we have to do it here.
|
|
||||||
valid = funcInjector.Retry(new(params).resolve)
|
|
||||||
if !valid {
|
|
||||||
pc := fn.Pointer()
|
|
||||||
fpc := runtime.FuncForPC(pc)
|
|
||||||
callerFileName, callerLineNumber := fpc.FileLine(pc)
|
|
||||||
callerName := fpc.Name()
|
|
||||||
|
|
||||||
err := fmt.Errorf("input arguments length(%d) and valid binders length(%d) are not equal for typeof '%s' which is defined at %s:%d by %s",
|
|
||||||
n, funcInjector.Length, fn.Type().String(), callerFileName, callerLineNumber, callerName)
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h := func(ctx context.Context) {
|
v := valueOf(fn)
|
||||||
// in := make([]reflect.Value, n, n)
|
numIn := v.Type().NumIn()
|
||||||
// funcInjector.Inject(&in, reflect.ValueOf(ctx))
|
|
||||||
// DispatchFuncResult(ctx, fn.Call(in))
|
bindings := getBindingsForFunc(v, c.Dependencies, c.ParamStartIndex)
|
||||||
DispatchFuncResult(ctx, nil, funcInjector.Call(ctx))
|
|
||||||
|
return func(ctx context.Context) {
|
||||||
|
inputs := make([]reflect.Value, numIn)
|
||||||
|
|
||||||
|
for _, binding := range bindings {
|
||||||
|
input, err := binding.Dependency.Handle(ctx, binding.Input)
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrSeeOther {
|
||||||
|
continue
|
||||||
|
} else if err == ErrStopExecution {
|
||||||
|
ctx.StopExecution()
|
||||||
|
return // return without error.
|
||||||
|
}
|
||||||
|
|
||||||
|
c.GetErrorHandler(ctx).HandleError(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs[binding.Input.Index] = input
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs := v.Call(inputs)
|
||||||
|
if err := dispatchFuncResult(ctx, outputs); err != nil {
|
||||||
|
c.GetErrorHandler(ctx).HandleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHandler(fn interface{}) (context.Handler, bool) {
|
||||||
|
if handler, ok := fn.(context.Handler); ok {
|
||||||
|
return handler, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
return h, nil
|
if handler, ok := fn.(func(context.Context)); ok {
|
||||||
|
return handler, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHandlerWithError(fn interface{}) (func(context.Context) error, bool) {
|
||||||
|
if handlerWithErr, ok := fn.(func(context.Context) error); ok {
|
||||||
|
return handlerWithErr, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
package hero_test
|
package hero_test
|
||||||
|
|
||||||
// black-box
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/kataras/iris/v12"
|
"github.com/kataras/iris/v12"
|
||||||
"github.com/kataras/iris/v12/httptest"
|
|
||||||
|
|
||||||
. "github.com/kataras/iris/v12/hero"
|
. "github.com/kataras/iris/v12/hero"
|
||||||
|
"github.com/kataras/iris/v12/httptest"
|
||||||
)
|
)
|
||||||
|
|
||||||
// dynamic func
|
// dynamic func
|
||||||
|
@ -29,25 +26,25 @@ func testBinderFunc(ctx iris.Context) testUserStruct {
|
||||||
|
|
||||||
// service
|
// service
|
||||||
type (
|
type (
|
||||||
// these TestService and TestServiceImpl could be in lowercase, unexported
|
// these testService and testServiceImpl could be in lowercase, unexported
|
||||||
// but the `Say` method should be exported however we have those exported
|
// but the `Say` method should be exported however we have those exported
|
||||||
// because of the controller handler test.
|
// because of the controller handler test.
|
||||||
TestService interface {
|
testService interface {
|
||||||
Say(string) string
|
Say(string) string
|
||||||
}
|
}
|
||||||
TestServiceImpl struct {
|
testServiceImpl struct {
|
||||||
prefix string
|
prefix string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *TestServiceImpl) Say(message string) string {
|
func (s *testServiceImpl) Say(message string) string {
|
||||||
return s.prefix + " " + message
|
return s.prefix + " " + message
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// binders, as user-defined
|
// binders, as user-defined
|
||||||
testBinderFuncUserStruct = testBinderFunc
|
testBinderFuncUserStruct = testBinderFunc
|
||||||
testBinderService = &TestServiceImpl{prefix: "say"}
|
testBinderService = &testServiceImpl{prefix: "say"}
|
||||||
testBinderFuncParam = func(ctx iris.Context) string {
|
testBinderFuncParam = func(ctx iris.Context) string {
|
||||||
return ctx.Params().Get("param")
|
return ctx.Params().Get("param")
|
||||||
}
|
}
|
||||||
|
@ -60,7 +57,7 @@ var (
|
||||||
}
|
}
|
||||||
|
|
||||||
// just one input arg, the service which is binded by the #2 service binder.
|
// just one input arg, the service which is binded by the #2 service binder.
|
||||||
testConsumeServiceHandler = func(service TestService) string {
|
testConsumeServiceHandler = func(service testService) string {
|
||||||
return service.Say("something")
|
return service.Say("something")
|
||||||
}
|
}
|
||||||
// just one input arg, a standar string which is binded by the #3 func(ctx) any binder.
|
// just one input arg, a standar string which is binded by the #3 func(ctx) any binder.
|
||||||
|
@ -70,7 +67,9 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHandler(t *testing.T) {
|
func TestHandler(t *testing.T) {
|
||||||
Register(testBinderFuncUserStruct, testBinderService, testBinderFuncParam)
|
Register(testBinderFuncUserStruct)
|
||||||
|
Register(testBinderService)
|
||||||
|
Register(testBinderFuncParam)
|
||||||
var (
|
var (
|
||||||
h1 = Handler(testConsumeUserHandler)
|
h1 = Handler(testConsumeUserHandler)
|
||||||
h2 = Handler(testConsumeServiceHandler)
|
h2 = Handler(testConsumeServiceHandler)
|
||||||
|
@ -112,7 +111,7 @@ func TestBindFunctionAsFunctionInputArgument(t *testing.T) {
|
||||||
return ctx.PostValue // or FormValue, the same here.
|
return ctx.PostValue // or FormValue, the same here.
|
||||||
}
|
}
|
||||||
|
|
||||||
h := New().Register(postsBinder).Handler(func(get func(string) string) string {
|
h := New(postsBinder).Handler(func(get func(string) string) string {
|
||||||
// send the `ctx.PostValue/FormValue("username")` value
|
// send the `ctx.PostValue/FormValue("username")` value
|
||||||
// to the client.
|
// to the client.
|
||||||
return get("username")
|
return get("username")
|
||||||
|
@ -127,9 +126,8 @@ func TestBindFunctionAsFunctionInputArgument(t *testing.T) {
|
||||||
Expect().Status(iris.StatusOK).Body().Equal(expectedUsername)
|
Expect().Status(iris.StatusOK).Body().Equal(expectedUsername)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAutoBinding(t *testing.T) {
|
func TestPayloadBinding(t *testing.T) {
|
||||||
h := New()
|
h := New()
|
||||||
h.Register(AutoBinding)
|
|
||||||
|
|
||||||
postHandler := h.Handler(func(input *testUserStruct /* ptr */) string {
|
postHandler := h.Handler(func(input *testUserStruct /* ptr */) string {
|
||||||
return input.Username
|
return input.Username
|
||||||
|
@ -147,3 +145,57 @@ func TestAutoBinding(t *testing.T) {
|
||||||
e.POST("/").WithJSON(iris.Map{"username": "makis"}).Expect().Status(httptest.StatusOK).Body().Equal("makis")
|
e.POST("/").WithJSON(iris.Map{"username": "makis"}).Expect().Status(httptest.StatusOK).Body().Equal("makis")
|
||||||
e.POST("/2").WithJSON(iris.Map{"username": "kataras"}).Expect().Status(httptest.StatusOK).Body().Equal("kataras")
|
e.POST("/2").WithJSON(iris.Map{"username": "kataras"}).Expect().Status(httptest.StatusOK).Body().Equal("kataras")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Author's notes:
|
||||||
|
If aksed or required by my company, make the following test to pass but think downsides of code complexity and performance-cost
|
||||||
|
before begin the implementation of it.
|
||||||
|
- Dependencies without depending on other values can be named "root-level dependencies"
|
||||||
|
- Dependencies could be linked (a new .DependsOn?) to a "root-level dependency"(or by theirs same-level deps too?) with much
|
||||||
|
more control if "root-level dependencies" are named, e.g.:
|
||||||
|
b.Register("db", &myDBImpl{})
|
||||||
|
b.Register("user_dep", func(db myDB) User{...}).DependsOn("db")
|
||||||
|
b.Handler(func(user User) error{...})
|
||||||
|
b.Handler(func(ctx iris.Context, reuseDB myDB) {...})
|
||||||
|
Why linked over automatically? Because more thna one dependency can implement the same input and
|
||||||
|
end-user does not care about ordering the registered ones.
|
||||||
|
Link with `DependsOn` SHOULD be optional, if exists then limit the available dependencies,
|
||||||
|
`DependsOn` SHOULD accept comma-separated values, e.g. "db, otherdep" and SHOULD also work
|
||||||
|
by calling it multiple times i.e `Depends("db").DependsOn("otherdep")`.
|
||||||
|
Handlers should also be able to explicitly limit the list of
|
||||||
|
their available dependencies per-handler, a `.DependsOn` feature SHOULD exist there too.
|
||||||
|
|
||||||
|
Also, note that with the new implementation a `*hero.Input` value can be accepted on dynamic dependencies,
|
||||||
|
that value contains an `Options.Dependencies` field which lists all the registered dependencies,
|
||||||
|
so, in theory, end-developers could achieve same results by hand-code(inside the dependency's function body).
|
||||||
|
|
||||||
|
26 Feb 2020. Gerasimos Maropoulos
|
||||||
|
______________________________________________
|
||||||
|
*/
|
||||||
|
|
||||||
|
type testMessage struct {
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDependentDependencies(t *testing.T) {
|
||||||
|
b := New()
|
||||||
|
b.Register(&testServiceImpl{prefix: "prefix:"})
|
||||||
|
b.Register(func(service testService) testMessage {
|
||||||
|
return testMessage{Body: service.Say("it is a deep") + " dependency"}
|
||||||
|
})
|
||||||
|
var (
|
||||||
|
h1 = b.Handler(func(msg testMessage) string {
|
||||||
|
return msg.Body
|
||||||
|
})
|
||||||
|
h2 = b.Handler(func(reuse testService) string {
|
||||||
|
return reuse.Say("message")
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
app := iris.New()
|
||||||
|
app.Get("/h1", h1)
|
||||||
|
app.Get("/h2", h2)
|
||||||
|
|
||||||
|
e := httptest.New(t, app)
|
||||||
|
e.GET("/h1").Expect().Status(httptest.StatusOK).Body().Equal("prefix: it is a deep dependency")
|
||||||
|
e.GET("/h2").Expect().Status(httptest.StatusOK).Body().Equal("prefix: message")
|
||||||
|
}
|
||||||
|
|
122
hero/hero.go
122
hero/hero.go
|
@ -1,122 +0,0 @@
|
||||||
package hero
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/kataras/iris/v12/context"
|
|
||||||
"github.com/kataras/iris/v12/hero/di"
|
|
||||||
|
|
||||||
"github.com/kataras/golog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// def is the default hero value which can be used for dependencies share.
|
|
||||||
var def = New()
|
|
||||||
|
|
||||||
// Hero contains the Dependencies which will be binded
|
|
||||||
// to the controller(s) or handler(s) that can be created
|
|
||||||
// using the Hero's `Handler` and `Controller` methods.
|
|
||||||
//
|
|
||||||
// This is not exported for being used by everyone, use it only when you want
|
|
||||||
// to share heroes between multi mvc.go#Application
|
|
||||||
// or make custom hero handlers that can be used on the standard
|
|
||||||
// iris' APIBuilder. The last one reason is the most useful here,
|
|
||||||
// although end-devs can use the `MakeHandler` as well.
|
|
||||||
//
|
|
||||||
// For a more high-level structure please take a look at the "mvc.go#Application".
|
|
||||||
type Hero struct {
|
|
||||||
values di.Values
|
|
||||||
errorHandler di.ErrorHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a new Hero, a container for dependencies and a factory
|
|
||||||
// for handlers and controllers, this is used internally by the `mvc#Application` structure.
|
|
||||||
// Please take a look at the structure's documentation for more information.
|
|
||||||
func New() *Hero {
|
|
||||||
return &Hero{
|
|
||||||
values: di.NewValues(),
|
|
||||||
errorHandler: di.DefaultErrorHandler,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dependencies returns the dependencies collection if the default hero,
|
|
||||||
// those can be modified at any way but before the consumer `Handler`.
|
|
||||||
func Dependencies() *di.Values {
|
|
||||||
return def.Dependencies()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dependencies returns the dependencies collection of this hero,
|
|
||||||
// those can be modified at any way but before the consumer `Handler`.
|
|
||||||
func (h *Hero) Dependencies() *di.Values {
|
|
||||||
return &h.values
|
|
||||||
}
|
|
||||||
|
|
||||||
// AutoBinding used to be registered as dependency to try to automatically
|
|
||||||
// map and bind the inputs that are not already binded with a dependency.
|
|
||||||
//
|
|
||||||
// See `DefaultFallbackBinder`.
|
|
||||||
var AutoBinding = di.AutoBindingValue{}
|
|
||||||
|
|
||||||
// Register adds one or more values as dependencies.
|
|
||||||
// The value can be a single struct value-instance or a function
|
|
||||||
// which has one input and one output, the input should be
|
|
||||||
// an `iris.Context` and the output can be any type, that output type
|
|
||||||
// will be binded to the handler's input argument, if matching.
|
|
||||||
//
|
|
||||||
// Example: `.Register(loggerService{prefix: "dev"}, func(ctx iris.Context) User {...})`.
|
|
||||||
func Register(values ...interface{}) *Hero {
|
|
||||||
return def.Register(values...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register adds one or more values as dependencies.
|
|
||||||
// The value can be a single struct value-instance or a function
|
|
||||||
// which has one input and one output, the input should be
|
|
||||||
// an `iris.Context` and the output can be any type, that output type
|
|
||||||
// will be binded to the handler's input argument, if matching.
|
|
||||||
//
|
|
||||||
// Example: `.Register(loggerService{prefix: "dev"}, func(ctx iris.Context) User {...})`.
|
|
||||||
func (h *Hero) Register(values ...interface{}) *Hero {
|
|
||||||
h.values.Add(values...)
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone creates and returns a new hero with the default Dependencies.
|
|
||||||
// It copies the default's dependencies and returns a new hero.
|
|
||||||
func Clone() *Hero {
|
|
||||||
return def.Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone creates and returns a new hero with the parent's(current) Dependencies.
|
|
||||||
// It copies the current "h" dependencies and returns a new hero.
|
|
||||||
func (h *Hero) Clone() *Hero {
|
|
||||||
child := New()
|
|
||||||
child.values = h.values.Clone()
|
|
||||||
return child
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorHandler sets a handler for this hero instance
|
|
||||||
// which will be fired when a handler's second output argument is error and it's not nil
|
|
||||||
// or when a request-scoped dynamic function dependency's second output argument is error and it's not nil.
|
|
||||||
func (h *Hero) ErrorHandler(errorHandler func(ctx context.Context, err error)) *Hero {
|
|
||||||
h.errorHandler = di.ErrorHandlerFunc(errorHandler)
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler accepts a "handler" function which can accept any input arguments that match
|
|
||||||
// with the Hero's `Dependencies` and any output result; like string, int (string,int),
|
|
||||||
// custom structs, Result(View | Response) and anything you can imagine.
|
|
||||||
// It returns a standard `iris/context.Handler` which can be used anywhere in an Iris Application,
|
|
||||||
// as middleware or as simple route handler or subdomain's handler.
|
|
||||||
func Handler(handler interface{}) context.Handler {
|
|
||||||
return def.Handler(handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler accepts a handler "fn" function which can accept any input arguments that match
|
|
||||||
// with the Hero's `Dependencies` and any output result; like string, int (string,int),
|
|
||||||
// custom structs, Result(View | Response) and anything you can imagine.
|
|
||||||
// It returns a standard `iris/context.Handler` which can be used anywhere in an Iris Application,
|
|
||||||
// as middleware or as simple route handler or subdomain's handler.
|
|
||||||
func (h *Hero) Handler(fn interface{}) context.Handler {
|
|
||||||
handler, err := makeHandler(fn, h.errorHandler, h.values.Clone()...)
|
|
||||||
if err != nil {
|
|
||||||
golog.Errorf("hero handler: %v", err)
|
|
||||||
}
|
|
||||||
return handler
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package hero
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/context"
|
|
||||||
"github.com/kataras/iris/v12/hero/di"
|
|
||||||
)
|
|
||||||
|
|
||||||
// weak because we don't have access to the path, neither
|
|
||||||
// the macros, so this is just a guess based on the index of the path parameter,
|
|
||||||
// the function's path parameters should be like a chain, in the same order as
|
|
||||||
// the caller registers a route's path.
|
|
||||||
// A context or any value(s) can be in front or back or even between them.
|
|
||||||
type params struct {
|
|
||||||
// the next function input index of where the next path parameter
|
|
||||||
// should be inside the CONTEXT.
|
|
||||||
next int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *params) resolve(index int, typ reflect.Type, _ di.Values) (reflect.Value, bool) {
|
|
||||||
currentParamIndex := p.next
|
|
||||||
v, ok := context.ParamResolverByTypeAndIndex(typ, currentParamIndex)
|
|
||||||
|
|
||||||
p.next = p.next + 1
|
|
||||||
return v, ok
|
|
||||||
}
|
|
|
@ -19,7 +19,7 @@ func TestPathParams(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
handlerWithOtherBetweenThem := h.Handler(func(firstname string, f func() string, lastname string) {
|
handlerWithOtherBetweenThem := h.Handler(func(firstname string, f func() string, lastname string) {
|
||||||
got = f() + firstname + lastname
|
got = firstname + lastname
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx := context.NewContext(nil)
|
ctx := context.NewContext(nil)
|
||||||
|
@ -28,20 +28,20 @@ func TestPathParams(t *testing.T) {
|
||||||
handler(ctx)
|
handler(ctx)
|
||||||
expected := "GerasimosMaropoulos"
|
expected := "GerasimosMaropoulos"
|
||||||
if got != expected {
|
if got != expected {
|
||||||
t.Fatalf("expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got)
|
t.Fatalf("[0] expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
got = ""
|
got = ""
|
||||||
handlerWithOther(ctx)
|
handlerWithOther(ctx)
|
||||||
expected = "GerasimosMaropoulos"
|
expected = "GerasimosMaropoulos"
|
||||||
if got != expected {
|
if got != expected {
|
||||||
t.Fatalf("expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got)
|
t.Fatalf("[1] expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
got = ""
|
got = ""
|
||||||
handlerWithOtherBetweenThem(ctx)
|
handlerWithOtherBetweenThem(ctx)
|
||||||
expected = "GerasimosMaropoulos"
|
expected = "GerasimosMaropoulos"
|
||||||
if got != expected {
|
if got != expected {
|
||||||
t.Fatalf("expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got)
|
t.Fatalf("[2] expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
242
hero/reflect.go
Normal file
242
hero/reflect.go
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
package hero
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdContext "context"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
"github.com/kataras/iris/v12/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func valueOf(v interface{}) reflect.Value {
|
||||||
|
if v, ok := v.(reflect.Value); ok {
|
||||||
|
// check if it's already a reflect.Value.
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflect.ValueOf(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// indirectType returns the value of a pointer-type "typ".
|
||||||
|
// If "typ" is a pointer, array, chan, map or slice it returns its Elem,
|
||||||
|
// otherwise returns the typ as it's.
|
||||||
|
func indirectType(typ reflect.Type) reflect.Type {
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
|
||||||
|
return typ.Elem()
|
||||||
|
}
|
||||||
|
return typ
|
||||||
|
}
|
||||||
|
|
||||||
|
func goodVal(v reflect.Value) bool {
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice:
|
||||||
|
if v.IsNil() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.IsValid()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFunc(kindable interface{ Kind() reflect.Kind }) bool {
|
||||||
|
return kindable.Kind() == reflect.Func
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputTyp = reflect.TypeOf((*Input)(nil))
|
||||||
|
var timeTyp = reflect.TypeOf((*time.Time)(nil)).Elem()
|
||||||
|
|
||||||
|
var errTyp = reflect.TypeOf((*error)(nil)).Elem()
|
||||||
|
|
||||||
|
// isError returns true if "typ" is type of `error`.
|
||||||
|
func isError(typ reflect.Type) bool {
|
||||||
|
return typ.Implements(errTyp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toError(v reflect.Value) error {
|
||||||
|
if v.IsNil() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.Interface().(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextTyp = reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||||
|
|
||||||
|
// isContext returns true if the "typ" is a type of Context.
|
||||||
|
func isContext(typ reflect.Type) bool {
|
||||||
|
return typ.Implements(contextTyp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdContextTyp = reflect.TypeOf((*stdContext.Context)(nil)).Elem()
|
||||||
|
|
||||||
|
// isStdContext returns true if the "typ" is a type of standard Context.
|
||||||
|
func isStdContext(typ reflect.Type) bool {
|
||||||
|
return typ.Implements(stdContextTyp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sessionTyp = reflect.TypeOf((*sessions.Session)(nil))
|
||||||
|
|
||||||
|
// isStdContext returns true if the "typ" is a type of standard Context.
|
||||||
|
func isSession(typ reflect.Type) bool {
|
||||||
|
return typ == sessionTyp
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorHandlerTyp = reflect.TypeOf((*ErrorHandler)(nil)).Elem()
|
||||||
|
|
||||||
|
func isErrorHandler(typ reflect.Type) bool {
|
||||||
|
return typ.Implements(errorHandlerTyp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var emptyValue reflect.Value
|
||||||
|
|
||||||
|
func equalTypes(binding reflect.Type, input reflect.Type) bool {
|
||||||
|
if binding == input {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Printf("got: %s expected: %s\n", got.String(), expected.String())
|
||||||
|
// if accepts an interface, check if the given "got" type does
|
||||||
|
// implement this "expected" user handler's input argument.
|
||||||
|
if input.Kind() == reflect.Interface {
|
||||||
|
// fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", expected.String(), got.String())
|
||||||
|
// return got.Implements(expected)
|
||||||
|
// return expected.AssignableTo(got)
|
||||||
|
return binding.AssignableTo(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dependency: func(...) interface{} { return "string" }
|
||||||
|
// expected input: string.
|
||||||
|
if binding.Kind() == reflect.Interface {
|
||||||
|
return input.AssignableTo(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func structFieldIgnored(f reflect.StructField) bool {
|
||||||
|
if !f.Anonymous {
|
||||||
|
return true // if not anonymous(embedded), ignore it.
|
||||||
|
}
|
||||||
|
|
||||||
|
s := f.Tag.Get("ignore")
|
||||||
|
return s == "true" // if has an ignore tag then ignore it.
|
||||||
|
}
|
||||||
|
|
||||||
|
// all except non-zero.
|
||||||
|
func lookupFields(elem reflect.Value, skipUnexported bool, onlyZeros bool, parentIndex []int) (fields []reflect.StructField) {
|
||||||
|
elemTyp := elem.Type()
|
||||||
|
for i, n := 0, elem.NumField(); i < n; i++ {
|
||||||
|
fieldValue := elem.Field(i)
|
||||||
|
|
||||||
|
field := elemTyp.Field(i)
|
||||||
|
|
||||||
|
// embed any fields from other structs.
|
||||||
|
if indirectType(field.Type).Kind() == reflect.Struct && !structFieldIgnored(field) {
|
||||||
|
fields = append(fields, lookupFields(fieldValue, skipUnexported, onlyZeros, append(parentIndex, i))...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if onlyZeros && !isZero(fieldValue) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip unexported fields here.
|
||||||
|
if isExported := field.PkgPath == ""; skipUnexported && !isExported {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
index := []int{i}
|
||||||
|
if len(parentIndex) > 0 {
|
||||||
|
index = append(parentIndex, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
field.Index = index
|
||||||
|
|
||||||
|
fields = append(fields, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupNonZeroFieldValues(elem reflect.Value) (nonZeroFields []reflect.StructField) {
|
||||||
|
fields := lookupFields(elem, true, false, nil)
|
||||||
|
for _, f := range fields {
|
||||||
|
if fieldVal := elem.FieldByIndex(f.Index); goodVal(fieldVal) && !isZero(fieldVal) {
|
||||||
|
/* && f.Type.Kind() == reflect.Ptr &&*/
|
||||||
|
nonZeroFields = append(nonZeroFields, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// isZero returns true if a value is nil.
|
||||||
|
// Remember; fields to be checked should be exported otherwise it returns false.
|
||||||
|
// Notes for users:
|
||||||
|
// Boolean's zero value is false, even if not set-ed.
|
||||||
|
// UintXX are not zero on 0 because they are pointers to.
|
||||||
|
func isZero(v reflect.Value) bool {
|
||||||
|
// switch v.Kind() {
|
||||||
|
// case reflect.Struct:
|
||||||
|
// zero := true
|
||||||
|
// for i := 0; i < v.NumField(); i++ {
|
||||||
|
// f := v.Field(i)
|
||||||
|
// if f.Type().PkgPath() != "" {
|
||||||
|
// continue // unexported.
|
||||||
|
// }
|
||||||
|
// zero = zero && isZero(f)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if typ := v.Type(); typ != nil && v.IsValid() {
|
||||||
|
// f, ok := typ.MethodByName("IsZero")
|
||||||
|
// // if not found
|
||||||
|
// // if has input arguments (1 is for the value receiver, so > 1 for the actual input args)
|
||||||
|
// // if output argument is not boolean
|
||||||
|
// // then skip this IsZero user-defined function.
|
||||||
|
// if !ok || f.Type.NumIn() > 1 || f.Type.NumOut() != 1 && f.Type.Out(0).Kind() != reflect.Bool {
|
||||||
|
// return zero
|
||||||
|
// }
|
||||||
|
|
||||||
|
// method := v.Method(f.Index)
|
||||||
|
// // no needed check but:
|
||||||
|
// if method.IsValid() && !method.IsNil() {
|
||||||
|
// // it shouldn't panic here.
|
||||||
|
// zero = method.Call([]reflect.Value{})[0].Interface().(bool)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return zero
|
||||||
|
// case reflect.Func, reflect.Map, reflect.Slice:
|
||||||
|
// return v.IsNil()
|
||||||
|
// case reflect.Array:
|
||||||
|
// zero := true
|
||||||
|
// for i := 0; i < v.Len(); i++ {
|
||||||
|
// zero = zero && isZero(v.Index(i))
|
||||||
|
// }
|
||||||
|
// return zero
|
||||||
|
// }
|
||||||
|
// if not any special type then use the reflect's .Zero
|
||||||
|
// usually for fields, but remember if it's boolean and it's false
|
||||||
|
// then it's zero, even if set-ed.
|
||||||
|
|
||||||
|
if !v.CanInterface() {
|
||||||
|
// if can't interface, i.e return value from unexported field or method then return false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
zero := reflect.Zero(v.Type())
|
||||||
|
return v.Interface() == zero.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNil same as `reflect.IsNil` but a bit safer to use, returns false if not a correct type.
|
||||||
|
func isNil(v reflect.Value) bool {
|
||||||
|
k := v.Kind()
|
||||||
|
switch k {
|
||||||
|
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
|
||||||
|
return v.IsNil()
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
39
hero/reflect_test.go
Normal file
39
hero/reflect_test.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package hero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testInterface interface {
|
||||||
|
Get() string
|
||||||
|
}
|
||||||
|
|
||||||
|
var testInterfaceTyp = reflect.TypeOf((*testInterface)(nil)).Elem()
|
||||||
|
|
||||||
|
type testImplPtr struct{}
|
||||||
|
|
||||||
|
func (*testImplPtr) Get() string { return "get_ptr" }
|
||||||
|
|
||||||
|
type testImpl struct{}
|
||||||
|
|
||||||
|
func (testImpl) Get() string { return "get" }
|
||||||
|
|
||||||
|
func TestEqualTypes(t *testing.T) {
|
||||||
|
of := reflect.TypeOf
|
||||||
|
|
||||||
|
var tests = map[reflect.Type]reflect.Type{
|
||||||
|
of("string"): of("input"),
|
||||||
|
of(42): of(10),
|
||||||
|
testInterfaceTyp: testInterfaceTyp,
|
||||||
|
of(new(testImplPtr)): testInterfaceTyp,
|
||||||
|
of(new(testImpl)): testInterfaceTyp,
|
||||||
|
of(testImpl{}): testInterfaceTyp,
|
||||||
|
}
|
||||||
|
|
||||||
|
for binding, input := range tests {
|
||||||
|
if !equalTypes(binding, input) {
|
||||||
|
t.Fatalf("expected type of: %s to be equal to the binded one of: %s", input, binding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +0,0 @@
|
||||||
package hero
|
|
||||||
|
|
||||||
// It's so easy, no need to be lived anywhere as builtin.. users should understand
|
|
||||||
// how easy it's by using it.
|
|
||||||
|
|
||||||
// // Session is a binder that will fill a *sessions.Session function input argument
|
|
||||||
// // or a Controller struct's field.
|
|
||||||
// func Session(sess *sessions.Sessions) func(context.Context) *sessions.Session {
|
|
||||||
// return sess.Start
|
|
||||||
// }
|
|
140
hero/struct.go
Normal file
140
hero/struct.go
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
package hero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sorter is the type for sort customization of a struct's fields
|
||||||
|
// and its available bindable values.
|
||||||
|
//
|
||||||
|
// Sorting applies only when a field can accept more than one registered value.
|
||||||
|
type Sorter func(t1 reflect.Type, t2 reflect.Type) bool
|
||||||
|
|
||||||
|
// sortByNumMethods is a builtin sorter to sort fields and values
|
||||||
|
// based on their type and its number of methods, highest number of methods goes first.
|
||||||
|
//
|
||||||
|
// It is the default sorter on struct injector of `hero.Struct` method.
|
||||||
|
var sortByNumMethods Sorter = func(t1 reflect.Type, t2 reflect.Type) bool {
|
||||||
|
if t1.Kind() != t2.Kind() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if k := t1.Kind(); k == reflect.Interface || k == reflect.Struct {
|
||||||
|
return t1.NumMethod() > t2.NumMethod()
|
||||||
|
} else if k != reflect.Struct {
|
||||||
|
return false // non-structs goes last.
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type Struct struct {
|
||||||
|
ptrType reflect.Type
|
||||||
|
ptrValue reflect.Value // the original ptr struct value.
|
||||||
|
elementType reflect.Type // the original struct type.
|
||||||
|
bindings []*Binding // struct field bindings.
|
||||||
|
|
||||||
|
Container *Container
|
||||||
|
Singleton bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeStruct(structPtr interface{}, c *Container) *Struct {
|
||||||
|
v := valueOf(structPtr)
|
||||||
|
typ := v.Type()
|
||||||
|
if typ.Kind() != reflect.Ptr || indirectType(typ).Kind() != reflect.Struct {
|
||||||
|
panic("binder: struct: should be a pointer to a struct value")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get struct's fields bindings.
|
||||||
|
bindings := getBindingsForStruct(v, c.Dependencies, c.ParamStartIndex, c.Sorter)
|
||||||
|
|
||||||
|
// length bindings of 0, means that it has no fields or all mapped deps are static.
|
||||||
|
// If static then Struct.Acquire will return the same "value" instance, otherwise it will create a new one.
|
||||||
|
singleton := true
|
||||||
|
elem := v.Elem()
|
||||||
|
for _, b := range bindings {
|
||||||
|
if b.Dependency.Static {
|
||||||
|
// Fill now.
|
||||||
|
input, err := b.Dependency.Handle(nil, b.Input)
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrSeeOther {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
elem.FieldByIndex(b.Input.StructFieldIndex).Set(input)
|
||||||
|
} else if !b.Dependency.Static {
|
||||||
|
singleton = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Struct{
|
||||||
|
ptrValue: v,
|
||||||
|
ptrType: typ,
|
||||||
|
elementType: elem.Type(),
|
||||||
|
bindings: bindings,
|
||||||
|
Singleton: singleton,
|
||||||
|
}
|
||||||
|
|
||||||
|
isErrHandler := isErrorHandler(typ)
|
||||||
|
|
||||||
|
newContainer := c.Clone()
|
||||||
|
// Add the controller dependency itself as func dependency but with a known type which should be explicit binding
|
||||||
|
// in order to keep its maximum priority.
|
||||||
|
newContainer.Register(s.Acquire).
|
||||||
|
Explicitly().
|
||||||
|
DestType = typ
|
||||||
|
|
||||||
|
newContainer.GetErrorHandler = func(ctx context.Context) ErrorHandler {
|
||||||
|
if isErrHandler {
|
||||||
|
return ctx.Controller().Interface().(ErrorHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.GetErrorHandler(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Container = newContainer
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Struct) Acquire(ctx context.Context) (reflect.Value, error) {
|
||||||
|
if s.Singleton {
|
||||||
|
ctx.Values().Set(context.ControllerContextKey, s.ptrValue)
|
||||||
|
return s.ptrValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl := ctx.Controller()
|
||||||
|
if ctrl.Kind() == reflect.Invalid {
|
||||||
|
ctrl = reflect.New(s.elementType)
|
||||||
|
ctx.Values().Set(context.ControllerContextKey, ctrl)
|
||||||
|
elem := ctrl.Elem()
|
||||||
|
for _, b := range s.bindings {
|
||||||
|
input, err := b.Dependency.Handle(ctx, b.Input)
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrSeeOther {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// return emptyValue, err
|
||||||
|
return ctrl, err
|
||||||
|
}
|
||||||
|
elem.FieldByIndex(b.Input.StructFieldIndex).Set(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Struct) MethodHandler(methodName string) context.Handler {
|
||||||
|
m, ok := s.ptrValue.Type().MethodByName(methodName)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("struct: method: %s does not exist", methodName))
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeHandler(m.Func, s.Container)
|
||||||
|
}
|
123
hero/struct_test.go
Normal file
123
hero/struct_test.go
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
package hero_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
. "github.com/kataras/iris/v12/hero"
|
||||||
|
"github.com/kataras/iris/v12/httptest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testStruct struct {
|
||||||
|
Ctx iris.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testStruct) MyHandler(name string) testOutput {
|
||||||
|
return fn(42, testInput{Name: name})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testStruct) MyHandler2(id int, in testInput) testOutput {
|
||||||
|
return fn(id, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testStruct) MyHandler3(in testInput) testOutput {
|
||||||
|
return fn(42, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testStruct) MyHandler4() {
|
||||||
|
c.Ctx.WriteString("MyHandler4")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStruct(t *testing.T) {
|
||||||
|
app := iris.New()
|
||||||
|
|
||||||
|
b := New()
|
||||||
|
s := b.Struct(&testStruct{})
|
||||||
|
|
||||||
|
postHandler := s.MethodHandler("MyHandler") // fallbacks such as {path} and {string} should registered first when same path.
|
||||||
|
app.Post("/{name:string}", postHandler)
|
||||||
|
postHandler2 := s.MethodHandler("MyHandler2")
|
||||||
|
app.Post("/{id:int}", postHandler2)
|
||||||
|
postHandler3 := s.MethodHandler("MyHandler3")
|
||||||
|
app.Post("/myHandler3", postHandler3)
|
||||||
|
getHandler := s.MethodHandler("MyHandler4")
|
||||||
|
app.Get("/myHandler4", getHandler)
|
||||||
|
|
||||||
|
e := httptest.New(t, app)
|
||||||
|
e.POST("/" + input.Name).Expect().Status(httptest.StatusOK).JSON().Equal(expectedOutput)
|
||||||
|
path := fmt.Sprintf("/%d", expectedOutput.ID)
|
||||||
|
e.POST(path).WithJSON(input).Expect().Status(httptest.StatusOK).JSON().Equal(expectedOutput)
|
||||||
|
e.POST("/myHandler3").WithJSON(input).Expect().Status(httptest.StatusOK).JSON().Equal(expectedOutput)
|
||||||
|
e.GET("/myHandler4").Expect().Status(httptest.StatusOK).Body().Equal("MyHandler4")
|
||||||
|
}
|
||||||
|
|
||||||
|
type testStructErrorHandler struct{}
|
||||||
|
|
||||||
|
func (s *testStructErrorHandler) HandleError(ctx iris.Context, err error) {
|
||||||
|
ctx.StatusCode(httptest.StatusConflict)
|
||||||
|
ctx.WriteString(err.Error())
|
||||||
|
ctx.StopExecution()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testStructErrorHandler) Handle(errText string) error {
|
||||||
|
return errors.New(errText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructErrorHandler(t *testing.T) {
|
||||||
|
b := New()
|
||||||
|
s := b.Struct(&testStructErrorHandler{})
|
||||||
|
|
||||||
|
app := iris.New()
|
||||||
|
app.Get("/{errText:string}", s.MethodHandler("Handle"))
|
||||||
|
|
||||||
|
expectedErrText := "an error"
|
||||||
|
e := httptest.New(t, app)
|
||||||
|
e.GET("/" + expectedErrText).Expect().Status(httptest.StatusConflict).Body().Equal(expectedErrText)
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
testServiceInterface1 interface {
|
||||||
|
Parse() string
|
||||||
|
}
|
||||||
|
|
||||||
|
testServiceImpl1 struct {
|
||||||
|
inner string
|
||||||
|
}
|
||||||
|
|
||||||
|
testServiceInterface2 interface {
|
||||||
|
}
|
||||||
|
|
||||||
|
testServiceImpl2 struct {
|
||||||
|
tf int
|
||||||
|
}
|
||||||
|
|
||||||
|
testControllerDependenciesSorter struct {
|
||||||
|
Service2 testServiceInterface2
|
||||||
|
Service1 testServiceInterface1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *testServiceImpl1) Parse() string {
|
||||||
|
return s.inner
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testControllerDependenciesSorter) Index() string {
|
||||||
|
return fmt.Sprintf("%#+v | %#+v", c.Service1, c.Service2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructFieldsSorter(t *testing.T) { // see https://github.com/kataras/iris/issues/1343
|
||||||
|
b := New()
|
||||||
|
b.Register(&testServiceImpl1{"parser"})
|
||||||
|
b.Register(&testServiceImpl2{24})
|
||||||
|
s := b.Struct(&testControllerDependenciesSorter{})
|
||||||
|
|
||||||
|
app := iris.New()
|
||||||
|
app.Get("/", s.MethodHandler("Index"))
|
||||||
|
|
||||||
|
e := httptest.New(t, app)
|
||||||
|
|
||||||
|
expectedBody := `&hero_test.testServiceImpl1{inner:"parser"} | &hero_test.testServiceImpl2{tf:24}`
|
||||||
|
e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal(expectedBody)
|
||||||
|
}
|
|
@ -8,10 +8,6 @@ import (
|
||||||
"github.com/kataras/iris/v12/context"
|
"github.com/kataras/iris/v12/context"
|
||||||
"github.com/kataras/iris/v12/core/router"
|
"github.com/kataras/iris/v12/core/router"
|
||||||
"github.com/kataras/iris/v12/hero"
|
"github.com/kataras/iris/v12/hero"
|
||||||
"github.com/kataras/iris/v12/hero/di"
|
|
||||||
"github.com/kataras/iris/v12/macro"
|
|
||||||
|
|
||||||
"github.com/kataras/golog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// BaseController is the optional controller interface, if it's
|
// BaseController is the optional controller interface, if it's
|
||||||
|
@ -41,7 +37,7 @@ type shared interface {
|
||||||
// it's called once per application.
|
// it's called once per application.
|
||||||
type BeforeActivation interface {
|
type BeforeActivation interface {
|
||||||
shared
|
shared
|
||||||
Dependencies() *di.Values
|
Dependencies() *hero.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
// AfterActivation is being used as the only one input argument of a
|
// AfterActivation is being used as the only one input argument of a
|
||||||
|
@ -54,8 +50,8 @@ type BeforeActivation interface {
|
||||||
// it's called once per application.
|
// it's called once per application.
|
||||||
type AfterActivation interface {
|
type AfterActivation interface {
|
||||||
shared
|
shared
|
||||||
DependenciesReadOnly() ValuesReadOnly
|
|
||||||
Singleton() bool
|
Singleton() bool
|
||||||
|
DependenciesReadOnly() []*hero.Dependency
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -66,12 +62,9 @@ var (
|
||||||
// ControllerActivator returns a new controller type info description.
|
// ControllerActivator returns a new controller type info description.
|
||||||
// Its functionality can be overridden by the end-dev.
|
// Its functionality can be overridden by the end-dev.
|
||||||
type ControllerActivator struct {
|
type ControllerActivator struct {
|
||||||
// the router is used on the `Activate` and can be used by end-dev on the `BeforeActivation`
|
app *Application
|
||||||
// to register any custom controller's methods as handlers.
|
|
||||||
router router.Party
|
|
||||||
|
|
||||||
macros macro.Macros
|
injector *hero.Struct
|
||||||
tmplParamStartIndex int
|
|
||||||
|
|
||||||
// initRef BaseController // the BaseController as it's passed from the end-dev.
|
// initRef BaseController // the BaseController as it's passed from the end-dev.
|
||||||
Value reflect.Value // the BaseController's Value.
|
Value reflect.Value // the BaseController's Value.
|
||||||
|
@ -85,17 +78,6 @@ type ControllerActivator struct {
|
||||||
// `GetRoute/GetRoutes(functionName)`.
|
// `GetRoute/GetRoutes(functionName)`.
|
||||||
routes map[string][]*router.Route
|
routes map[string][]*router.Route
|
||||||
|
|
||||||
// the bindings that comes from the Engine and the controller's filled fields if any.
|
|
||||||
// Can be bind-ed to the the new controller's fields and method that is fired
|
|
||||||
// on incoming requests.
|
|
||||||
dependencies di.Values
|
|
||||||
sorter di.Sorter
|
|
||||||
|
|
||||||
errorHandler di.ErrorHandler
|
|
||||||
|
|
||||||
// initialized on the first `Handle` or immediately when "servesWebsocket" is true.
|
|
||||||
injector *di.StructInjector
|
|
||||||
|
|
||||||
// true if this controller listens and serves to websocket events.
|
// true if this controller listens and serves to websocket events.
|
||||||
servesWebsocket bool
|
servesWebsocket bool
|
||||||
}
|
}
|
||||||
|
@ -103,7 +85,7 @@ type ControllerActivator struct {
|
||||||
// NameOf returns the package name + the struct type's name,
|
// NameOf returns the package name + the struct type's name,
|
||||||
// it's used to take the full name of an Controller, the `ControllerActivator#Name`.
|
// it's used to take the full name of an Controller, the `ControllerActivator#Name`.
|
||||||
func NameOf(v interface{}) string {
|
func NameOf(v interface{}) string {
|
||||||
elemTyp := di.IndirectType(di.ValueOf(v).Type())
|
elemTyp := indirectType(reflect.ValueOf(v).Type())
|
||||||
|
|
||||||
typName := elemTyp.Name()
|
typName := elemTyp.Name()
|
||||||
pkgPath := elemTyp.PkgPath()
|
pkgPath := elemTyp.PkgPath()
|
||||||
|
@ -112,16 +94,15 @@ func NameOf(v interface{}) string {
|
||||||
return fullname
|
return fullname
|
||||||
}
|
}
|
||||||
|
|
||||||
func newControllerActivator(router router.Party, controller interface{}, dependencies []reflect.Value, sorter di.Sorter, errorHandler di.ErrorHandler) *ControllerActivator {
|
func newControllerActivator(app *Application, controller interface{}) *ControllerActivator {
|
||||||
typ := reflect.TypeOf(controller)
|
typ := reflect.TypeOf(controller)
|
||||||
|
|
||||||
c := &ControllerActivator{
|
c := &ControllerActivator{
|
||||||
// give access to the Router to the end-devs if they need it for some reason,
|
// give access to the Router to the end-devs if they need it for some reason,
|
||||||
// i.e register done handlers.
|
// i.e register done handlers.
|
||||||
router: router,
|
app: app,
|
||||||
macros: *router.Macros(),
|
Value: reflect.ValueOf(controller),
|
||||||
Value: reflect.ValueOf(controller),
|
Type: typ,
|
||||||
Type: typ,
|
|
||||||
// the full name of the controller: its type including the package path.
|
// the full name of the controller: its type including the package path.
|
||||||
fullName: NameOf(controller),
|
fullName: NameOf(controller),
|
||||||
// set some methods that end-dev cann't use accidentally
|
// set some methods that end-dev cann't use accidentally
|
||||||
|
@ -131,14 +112,8 @@ func newControllerActivator(router router.Party, controller interface{}, depende
|
||||||
// if a new method is registered via `Handle` its function name
|
// if a new method is registered via `Handle` its function name
|
||||||
// is also appended to that slice.
|
// is also appended to that slice.
|
||||||
routes: whatReservedMethods(typ),
|
routes: whatReservedMethods(typ),
|
||||||
// CloneWithFieldsOf: include the manual fill-ed controller struct's fields to the dependencies.
|
|
||||||
dependencies: di.Values(dependencies).CloneWithFieldsOf(controller),
|
|
||||||
sorter: sorter,
|
|
||||||
errorHandler: errorHandler,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fpath, _ := macro.Parse(c.router.GetRelPath(), c.macros)
|
|
||||||
c.tmplParamStartIndex = len(fpath.Params)
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,39 +133,6 @@ func whatReservedMethods(typ reflect.Type) map[string][]*router.Route {
|
||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControllerActivator) markAsWebsocket() {
|
|
||||||
c.servesWebsocket = true
|
|
||||||
c.attachInjector()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dependencies returns the write and read access of the dependencies that are
|
|
||||||
// came from the parent MVC Application, with this you can customize
|
|
||||||
// the dependencies per controller, used at the `BeforeActivation`.
|
|
||||||
func (c *ControllerActivator) Dependencies() *di.Values {
|
|
||||||
return &c.dependencies
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValuesReadOnly returns the read-only access type of the controller's dependencies.
|
|
||||||
// Used at `AfterActivation`.
|
|
||||||
type ValuesReadOnly interface {
|
|
||||||
// Has returns true if a binder responsible to
|
|
||||||
// bind and return a type of "typ" is already registered to this controller.
|
|
||||||
Has(value interface{}) bool
|
|
||||||
// Len returns the length of the values.
|
|
||||||
Len() int
|
|
||||||
// Clone returns a copy of the current values.
|
|
||||||
Clone() di.Values
|
|
||||||
// CloneWithFieldsOf will return a copy of the current values
|
|
||||||
// plus the "s" struct's fields that are filled(non-zero) by the caller.
|
|
||||||
CloneWithFieldsOf(s interface{}) di.Values
|
|
||||||
}
|
|
||||||
|
|
||||||
// DependenciesReadOnly returns the read-only access type of the controller's dependencies.
|
|
||||||
// Used at `AfterActivation`.
|
|
||||||
func (c *ControllerActivator) DependenciesReadOnly() ValuesReadOnly {
|
|
||||||
return c.dependencies
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the full name of the controller, its package name + the type name.
|
// Name returns the full name of the controller, its package name + the type name.
|
||||||
// Can used at both `BeforeActivation` and `AfterActivation`.
|
// Can used at both `BeforeActivation` and `AfterActivation`.
|
||||||
func (c *ControllerActivator) Name() string {
|
func (c *ControllerActivator) Name() string {
|
||||||
|
@ -206,7 +148,7 @@ func (c *ControllerActivator) Name() string {
|
||||||
//
|
//
|
||||||
// Can used at both `BeforeActivation` and `AfterActivation`.
|
// Can used at both `BeforeActivation` and `AfterActivation`.
|
||||||
func (c *ControllerActivator) Router() router.Party {
|
func (c *ControllerActivator) Router() router.Party {
|
||||||
return c.router
|
return c.app.Router
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRoute returns the first registered route based on the controller's method name.
|
// GetRoute returns the first registered route based on the controller's method name.
|
||||||
|
@ -252,9 +194,23 @@ func (c *ControllerActivator) GetRoutes(methodName string) []*router.Route {
|
||||||
// any unexported fields and all fields are services-like, static.
|
// any unexported fields and all fields are services-like, static.
|
||||||
func (c *ControllerActivator) Singleton() bool {
|
func (c *ControllerActivator) Singleton() bool {
|
||||||
if c.injector == nil {
|
if c.injector == nil {
|
||||||
panic("MVC: Singleton used on an invalid state the API gives access to it only `AfterActivation`, report this as bug")
|
panic("MVC: Singleton called from wrong state the API gives access to it only `AfterActivation`, report this as bug")
|
||||||
}
|
}
|
||||||
return c.injector.Scope == di.Singleton
|
return c.injector.Singleton
|
||||||
|
}
|
||||||
|
|
||||||
|
// DependenciesReadOnly returns a list of dependencies, including the controller's one.
|
||||||
|
func (c *ControllerActivator) DependenciesReadOnly() []*hero.Dependency {
|
||||||
|
if c.injector == nil {
|
||||||
|
panic("MVC: DependenciesReadOnly called from wrong state the API gives access to it only `AfterActivation`, report this as bug")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.injector.Container.Dependencies
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependencies returns a value which can manage the controller's dependencies.
|
||||||
|
func (c *ControllerActivator) Dependencies() *hero.Container {
|
||||||
|
return c.app.container
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks if a method is already registered.
|
// checks if a method is already registered.
|
||||||
|
@ -268,12 +224,19 @@ func (c *ControllerActivator) isReservedMethod(name string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControllerActivator) activate() {
|
func (c *ControllerActivator) attachInjector() {
|
||||||
c.parseMethods()
|
if c.injector == nil {
|
||||||
|
c.injector = c.app.container.Struct(c.Value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControllerActivator) addErr(err error) bool {
|
func (c *ControllerActivator) markAsWebsocket() {
|
||||||
return c.router.GetReporter().Err(err) != nil
|
c.servesWebsocket = true
|
||||||
|
c.attachInjector()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControllerActivator) activate() {
|
||||||
|
c.parseMethods()
|
||||||
}
|
}
|
||||||
|
|
||||||
// register all available, exported methods to handlers if possible.
|
// register all available, exported methods to handlers if possible.
|
||||||
|
@ -286,7 +249,7 @@ func (c *ControllerActivator) parseMethods() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControllerActivator) parseMethod(m reflect.Method) {
|
func (c *ControllerActivator) parseMethod(m reflect.Method) {
|
||||||
httpMethod, httpPath, err := parseMethod(*c.router.Macros(), m, c.isReservedMethod)
|
httpMethod, httpPath, err := parseMethod(c.app.Router.Macros(), m, c.isReservedMethod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != errSkip {
|
if err != errSkip {
|
||||||
c.addErr(fmt.Errorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.fullName, m.Name, err))
|
c.addErr(fmt.Errorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.fullName, m.Name, err))
|
||||||
|
@ -298,6 +261,10 @@ func (c *ControllerActivator) parseMethod(m reflect.Method) {
|
||||||
c.Handle(httpMethod, httpPath, m.Name)
|
c.Handle(httpMethod, httpPath, m.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ControllerActivator) addErr(err error) bool {
|
||||||
|
return c.app.Router.GetReporter().Err(err) != nil
|
||||||
|
}
|
||||||
|
|
||||||
// Handle registers a route based on a http method, the route's path
|
// Handle registers a route based on a http method, the route's path
|
||||||
// and a function name that belongs to the controller, it accepts
|
// and a function name that belongs to the controller, it accepts
|
||||||
// a forth, optionally, variadic parameter which is the before handlers.
|
// a forth, optionally, variadic parameter which is the before handlers.
|
||||||
|
@ -336,35 +303,10 @@ func (c *ControllerActivator) handleMany(method, path, funcName string, override
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the method from the controller type.
|
handler := c.handlerOf(funcName)
|
||||||
m, ok := c.Type.MethodByName(funcName)
|
|
||||||
if !ok {
|
|
||||||
c.addErr(fmt.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller",
|
|
||||||
funcName, c.fullName))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse a route template which contains the parameters organised.
|
|
||||||
tmpl, err := macro.Parse(path, c.macros)
|
|
||||||
if err != nil {
|
|
||||||
c.addErr(fmt.Errorf("MVC: fail to parse the path for '%s.%s': %v", c.fullName, funcName, err))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the function's input.
|
|
||||||
funcIn := getInputArgsFromFunc(m.Type)
|
|
||||||
// get the path parameters bindings from the template,
|
|
||||||
// use the function's input except the receiver which is the
|
|
||||||
// end-dev's controller pointer.
|
|
||||||
pathParams := getPathParamsForInput(c.tmplParamStartIndex, tmpl.Params, funcIn[1:]...)
|
|
||||||
// get the function's input arguments' bindings.
|
|
||||||
funcDependencies := c.dependencies.Clone()
|
|
||||||
funcDependencies.AddValues(pathParams...)
|
|
||||||
|
|
||||||
handler := c.handlerOf(m, funcDependencies)
|
|
||||||
|
|
||||||
// register the handler now.
|
// register the handler now.
|
||||||
routes := c.router.HandleMany(method, path, append(middleware, handler)...)
|
routes := c.app.Router.HandleMany(method, path, append(middleware, handler)...)
|
||||||
if routes == nil {
|
if routes == nil {
|
||||||
c.addErr(fmt.Errorf("MVC: unable to register a route for the path for '%s.%s'", c.fullName, funcName))
|
c.addErr(fmt.Errorf("MVC: unable to register a route for the path for '%s.%s'", c.fullName, funcName))
|
||||||
return nil
|
return nil
|
||||||
|
@ -390,79 +332,21 @@ func (c *ControllerActivator) handleMany(method, path, funcName string, override
|
||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|
||||||
var emptyIn = []reflect.Value{}
|
func (c *ControllerActivator) handlerOf(methodName string) context.Handler {
|
||||||
|
|
||||||
func (c *ControllerActivator) attachInjector() {
|
|
||||||
if c.injector == nil {
|
|
||||||
c.injector = di.MakeStructInjector(
|
|
||||||
di.ValueOf(c.Value),
|
|
||||||
c.sorter,
|
|
||||||
di.Values(c.dependencies).CloneWithFieldsOf(c.Value)...,
|
|
||||||
)
|
|
||||||
// c.injector = di.Struct(c.Value, c.dependencies...)
|
|
||||||
if !c.servesWebsocket {
|
|
||||||
golog.Debugf("MVC Controller [%s] [Scope=%s]", c.fullName, c.injector.Scope)
|
|
||||||
} else {
|
|
||||||
golog.Debugf("MVC Websocket Controller [%s]", c.fullName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.injector.Has {
|
|
||||||
golog.Debugf("Dependencies:\n%s", c.injector.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ControllerActivator) handlerOf(m reflect.Method, funcDependencies []reflect.Value) context.Handler {
|
|
||||||
// Remember:
|
|
||||||
// The `Handle->handlerOf` can be called from `BeforeActivation` event
|
|
||||||
// then, the c.injector is nil because
|
|
||||||
// we may not have the dependencies bind-ed yet.
|
|
||||||
// To solve this we're doing a check on the FIRST `Handle`,
|
|
||||||
// if c.injector is nil, then set it with the current bindings,
|
|
||||||
// these bindings can change after, so first add dependencies and after register routes.
|
|
||||||
c.attachInjector()
|
c.attachInjector()
|
||||||
|
|
||||||
// fmt.Printf("for %s | values: %s\n", funcName, funcDependencies)
|
handler := c.injector.MethodHandler(methodName)
|
||||||
funcInjector := di.Func(m.Func, funcDependencies...)
|
|
||||||
funcInjector.ErrorHandler = c.errorHandler
|
|
||||||
|
|
||||||
// fmt.Printf("actual injector's inputs length: %d\n", funcInjector.Length)
|
if isBaseController(c.Type) {
|
||||||
if funcInjector.Has {
|
|
||||||
golog.Debugf("MVC dependencies of method '%s.%s':\n%s", c.fullName, m.Name, funcInjector.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
implementsBase = isBaseController(c.Type)
|
|
||||||
implementsErrorHandler = isErrorHandler(c.Type)
|
|
||||||
hasBindableFields = c.injector.CanInject
|
|
||||||
hasBindableFuncInputs = funcInjector.Has
|
|
||||||
funcHasErrorOut = hasErrorOutArgs(m)
|
|
||||||
|
|
||||||
call = m.Func.Call
|
|
||||||
)
|
|
||||||
|
|
||||||
if !implementsBase && !hasBindableFields && !hasBindableFuncInputs && !implementsErrorHandler {
|
|
||||||
return func(ctx context.Context) {
|
return func(ctx context.Context) {
|
||||||
hero.DispatchFuncResult(ctx, c.errorHandler, call(c.injector.AcquireSlice()))
|
ctrl, err := c.injector.Acquire(ctx)
|
||||||
}
|
if err != nil {
|
||||||
}
|
if err != hero.ErrStopExecution {
|
||||||
|
c.injector.Container.GetErrorHandler(ctx).HandleError(ctx, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
n := m.Type.NumIn()
|
|
||||||
return func(ctx context.Context) {
|
|
||||||
var (
|
|
||||||
ctrl = c.injector.Acquire()
|
|
||||||
errorHandler = c.errorHandler
|
|
||||||
)
|
|
||||||
|
|
||||||
// inject struct fields first before the BeginRequest and EndRequest, if any,
|
|
||||||
// in order to be able to have access there.
|
|
||||||
if hasBindableFields {
|
|
||||||
c.injector.InjectElem(ctx, ctrl.Elem())
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if has BeginRequest & EndRequest, before try to bind the method's inputs.
|
|
||||||
if implementsBase {
|
|
||||||
// the Interface(). is faster than MethodByName or pre-selected methods.
|
|
||||||
b := ctrl.Interface().(BaseController)
|
b := ctrl.Interface().(BaseController)
|
||||||
// init the request.
|
// init the request.
|
||||||
b.BeginRequest(ctx)
|
b.BeginRequest(ctx)
|
||||||
|
@ -472,28 +356,11 @@ func (c *ControllerActivator) handlerOf(m reflect.Method, funcDependencies []ref
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer b.EndRequest(ctx)
|
handler(ctx)
|
||||||
|
|
||||||
|
b.EndRequest(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
if funcHasErrorOut && implementsErrorHandler {
|
|
||||||
errorHandler = ctrl.Interface().(di.ErrorHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasBindableFuncInputs {
|
|
||||||
// means that ctxValue is not initialized before by the controller's struct injector.
|
|
||||||
|
|
||||||
in := make([]reflect.Value, n)
|
|
||||||
in[0] = ctrl
|
|
||||||
funcInjector.Inject(ctx, &in)
|
|
||||||
|
|
||||||
if ctx.IsStopped() {
|
|
||||||
return // stop as soon as possible, although it would stop later on if `ctx.StopExecution` called.
|
|
||||||
}
|
|
||||||
|
|
||||||
hero.DispatchFuncResult(ctx, errorHandler, call(in))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
hero.DispatchFuncResult(ctx, errorHandler, ctrl.Method(m.Index).Call(emptyIn))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return handler
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,24 +12,24 @@ import (
|
||||||
|
|
||||||
// service
|
// service
|
||||||
type (
|
type (
|
||||||
// these TestService and TestServiceImpl could be in lowercase, unexported
|
// these testService and testServiceImpl could be in lowercase, unexported
|
||||||
// but the `Say` method should be exported however we have those exported
|
// but the `Say` method should be exported however we have those exported
|
||||||
// because of the controller handler test.
|
// because of the controller handler test.
|
||||||
TestService interface {
|
testService interface {
|
||||||
Say(string) string
|
Say(string) string
|
||||||
}
|
}
|
||||||
TestServiceImpl struct {
|
testServiceImpl struct {
|
||||||
prefix string
|
prefix string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *TestServiceImpl) Say(message string) string {
|
func (s *testServiceImpl) Say(message string) string {
|
||||||
return s.prefix + " " + message
|
return s.prefix + " " + message
|
||||||
}
|
}
|
||||||
|
|
||||||
type testControllerHandle struct {
|
type testControllerHandle struct {
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
Service TestService
|
Service testService
|
||||||
|
|
||||||
reqField string
|
reqField string
|
||||||
}
|
}
|
||||||
|
@ -41,12 +41,7 @@ func (c *testControllerHandle) BeforeActivation(b BeforeActivation) {
|
||||||
b.Handle("GET", "/hiparam/{ps:string}", "HiParamBy")
|
b.Handle("GET", "/hiparam/{ps:string}", "HiParamBy")
|
||||||
b.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy")
|
b.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy")
|
||||||
b.HandleMany("GET", "/custom/{ps:string} /custom2/{ps:string}", "CustomWithParameter")
|
b.HandleMany("GET", "/custom/{ps:string} /custom2/{ps:string}", "CustomWithParameter")
|
||||||
// if dynamic path exist
|
b.HandleMany("GET", "/custom3/{ps:string}/{pssecond:string}", "CustomWithParameters")
|
||||||
// then longest path should be registered first
|
|
||||||
// and the controller's method if wants to add path parameters
|
|
||||||
// dependency injection then they should accept the longest path parameters.
|
|
||||||
// See `testControllerHandle.CustomWithParameters`.
|
|
||||||
b.HandleMany("GET", "/custom3/{ps:string}/{pssecond:string} /custom3/{ps:string}", "CustomWithParameters")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// test `GetRoute` for custom routes.
|
// test `GetRoute` for custom routes.
|
||||||
|
@ -109,8 +104,6 @@ func (c *testControllerHandle) CustomWithParameter(param1 string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *testControllerHandle) CustomWithParameters(param1, param2 string) string {
|
func (c *testControllerHandle) CustomWithParameters(param1, param2 string) string {
|
||||||
// it returns empty string for requested path: /custom3/value1,
|
|
||||||
// see BeforeActivation.
|
|
||||||
return param1 + param2
|
return param1 + param2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +117,7 @@ func (c *testSmallController) GetHiParamEmptyInputWithCtxBy(ctx context.Context,
|
||||||
func TestControllerHandle(t *testing.T) {
|
func TestControllerHandle(t *testing.T) {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
m := New(app)
|
m := New(app)
|
||||||
m.Register(&TestServiceImpl{prefix: "service:"})
|
m.Register(&testServiceImpl{prefix: "service:"})
|
||||||
m.Handle(new(testControllerHandle))
|
m.Handle(new(testControllerHandle))
|
||||||
m.Handle(new(testSmallController))
|
m.Handle(new(testSmallController))
|
||||||
|
|
||||||
|
@ -160,8 +153,7 @@ func TestControllerHandle(t *testing.T) {
|
||||||
Body().Equal("value2")
|
Body().Equal("value2")
|
||||||
e.GET("/custom3/value1/value2").Expect().Status(httptest.StatusOK).
|
e.GET("/custom3/value1/value2").Expect().Status(httptest.StatusOK).
|
||||||
Body().Equal("value1value2")
|
Body().Equal("value1value2")
|
||||||
e.GET("/custom3/value1").Expect().Status(httptest.StatusOK).
|
e.GET("/custom3/value1").Expect().Status(httptest.StatusNotFound)
|
||||||
Body().Equal("value1")
|
|
||||||
|
|
||||||
e.GET("/hi/param/empty/input/with/ctx/value").Expect().Status(httptest.StatusOK).
|
e.GET("/hi/param/empty/input/with/ctx/value").Expect().Status(httptest.StatusOK).
|
||||||
Body().Equal("empty in but served with ctx.Params.Get('param2')= value == id == value")
|
Body().Equal("empty in but served with ctx.Params.Get('param2')= value == id == value")
|
||||||
|
|
|
@ -94,10 +94,10 @@ func genParamKey(argIdx int) string {
|
||||||
type methodParser struct {
|
type methodParser struct {
|
||||||
lexer *methodLexer
|
lexer *methodLexer
|
||||||
fn reflect.Method
|
fn reflect.Method
|
||||||
macros macro.Macros
|
macros *macro.Macros
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseMethod(macros macro.Macros, fn reflect.Method, skipper func(string) bool) (method, path string, err error) {
|
func parseMethod(macros *macro.Macros, fn reflect.Method, skipper func(string) bool) (method, path string, err error) {
|
||||||
if skipper(fn.Name) {
|
if skipper(fn.Name) {
|
||||||
return "", "", errSkip
|
return "", "", errSkip
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/kataras/iris/v12"
|
"github.com/kataras/iris/v12"
|
||||||
"github.com/kataras/iris/v12/context"
|
"github.com/kataras/iris/v12/context"
|
||||||
"github.com/kataras/iris/v12/core/router"
|
"github.com/kataras/iris/v12/core/router"
|
||||||
|
"github.com/kataras/iris/v12/hero"
|
||||||
"github.com/kataras/iris/v12/httptest"
|
"github.com/kataras/iris/v12/httptest"
|
||||||
|
|
||||||
. "github.com/kataras/iris/v12/mvc"
|
. "github.com/kataras/iris/v12/mvc"
|
||||||
|
@ -281,7 +282,7 @@ type testControllerBindDeep struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testControllerBindDeep) BeforeActivation(b BeforeActivation) {
|
func (t *testControllerBindDeep) BeforeActivation(b BeforeActivation) {
|
||||||
b.Dependencies().Add(func(ctx iris.Context) (v testCustomStruct, err error) {
|
b.Dependencies().Register(func(ctx iris.Context) (v testCustomStruct, err error) {
|
||||||
err = ctx.ReadJSON(&v)
|
err = ctx.ReadJSON(&v)
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
@ -467,7 +468,7 @@ type testControllerActivateListener struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *testControllerActivateListener) BeforeActivation(b BeforeActivation) {
|
func (c *testControllerActivateListener) BeforeActivation(b BeforeActivation) {
|
||||||
b.Dependencies().AddOnce(&testBindType{title: "default title"})
|
b.Dependencies().Register(&testBindType{title: "overrides the dependency but not the field"}) // overrides the `Register` previous calls.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *testControllerActivateListener) Get() string {
|
func (c *testControllerActivateListener) Get() string {
|
||||||
|
@ -485,17 +486,17 @@ func TestControllerActivateListener(t *testing.T) {
|
||||||
// or
|
// or
|
||||||
m.Party("/manual2").Handle(&testControllerActivateListener{
|
m.Party("/manual2").Handle(&testControllerActivateListener{
|
||||||
TitlePointer: &testBindType{
|
TitlePointer: &testBindType{
|
||||||
title: "my title",
|
title: "my manual title",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
e := httptest.New(t, app)
|
e := httptest.New(t, app)
|
||||||
e.GET("/").Expect().Status(iris.StatusOK).
|
e.GET("/").Expect().Status(iris.StatusOK).
|
||||||
Body().Equal("default title")
|
Body().Equal("overrides the dependency but not the field")
|
||||||
e.GET("/manual").Expect().Status(iris.StatusOK).
|
e.GET("/manual").Expect().Status(iris.StatusOK).
|
||||||
Body().Equal("my title")
|
Body().Equal("overrides the dependency but not the field")
|
||||||
e.GET("/manual2").Expect().Status(iris.StatusOK).
|
e.GET("/manual2").Expect().Status(iris.StatusOK).
|
||||||
Body().Equal("my title")
|
Body().Equal("my manual title")
|
||||||
}
|
}
|
||||||
|
|
||||||
type testControllerNotCreateNewDueManuallySettingAllFields struct {
|
type testControllerNotCreateNewDueManuallySettingAllFields struct {
|
||||||
|
@ -505,13 +506,12 @@ type testControllerNotCreateNewDueManuallySettingAllFields struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *testControllerNotCreateNewDueManuallySettingAllFields) AfterActivation(a AfterActivation) {
|
func (c *testControllerNotCreateNewDueManuallySettingAllFields) AfterActivation(a AfterActivation) {
|
||||||
if n := a.DependenciesReadOnly().Len(); n != 2 {
|
if n := len(a.DependenciesReadOnly()) - len(hero.BuiltinDependencies); n != 1 {
|
||||||
c.T.Fatalf(`expecting 2 dependency, the 'T' and the 'TitlePointer' that we manually insert
|
c.T.Fatalf(`expecting 1 dependency;
|
||||||
and the fields total length is 2 so it will not create a new controller on each request
|
- the 'T' and the 'TitlePointer' are manually binded (nonzero fields on initilization)
|
||||||
however the dependencies are available here
|
- controller has no more than these two fields, it's a singleton
|
||||||
although the struct injector is being ignored when
|
- however, the dependencies length here should be 1 because the injector's options handler dependencies contains the controller's value dependency itself
|
||||||
creating the controller's handlers because we set it to invalidate state at "newControllerActivator"
|
-- got dependencies length: %d`, n)
|
||||||
-- got dependencies length: %d`, n)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !a.Singleton() {
|
if !a.Singleton() {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
// +build go1.9
|
|
||||||
|
|
||||||
package mvc
|
package mvc
|
||||||
|
|
||||||
import "github.com/kataras/iris/v12/hero"
|
import "github.com/kataras/iris/v12/hero"
|
||||||
|
|
86
mvc/mvc.go
86
mvc/mvc.go
|
@ -7,26 +7,11 @@ import (
|
||||||
"github.com/kataras/iris/v12/context"
|
"github.com/kataras/iris/v12/context"
|
||||||
"github.com/kataras/iris/v12/core/router"
|
"github.com/kataras/iris/v12/core/router"
|
||||||
"github.com/kataras/iris/v12/hero"
|
"github.com/kataras/iris/v12/hero"
|
||||||
"github.com/kataras/iris/v12/hero/di"
|
|
||||||
"github.com/kataras/iris/v12/websocket"
|
"github.com/kataras/iris/v12/websocket"
|
||||||
|
|
||||||
"github.com/kataras/golog"
|
"github.com/kataras/golog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HeroDependencies let you share bindable dependencies between
|
|
||||||
// package-level hero's registered dependencies and all MVC instances that comes later.
|
|
||||||
//
|
|
||||||
// `hero.Register(...)`
|
|
||||||
// `myMVC := mvc.New(app.Party(...))`
|
|
||||||
// the "myMVC" registers the dependencies provided by the `hero.Register` func
|
|
||||||
// automatically.
|
|
||||||
//
|
|
||||||
// Set it to false to disable that behavior, you have to use the `mvc#Register`
|
|
||||||
// even if you had register dependencies with the `hero` package.
|
|
||||||
//
|
|
||||||
// Defaults to true.
|
|
||||||
var HeroDependencies = true
|
|
||||||
|
|
||||||
// Application is the high-level component of the "mvc" package.
|
// Application is the high-level component of the "mvc" package.
|
||||||
// It's the API that you will be using to register controllers among with their
|
// It's the API that you will be using to register controllers among with their
|
||||||
// dependencies that your controllers may expecting.
|
// dependencies that your controllers may expecting.
|
||||||
|
@ -39,23 +24,17 @@ var HeroDependencies = true
|
||||||
//
|
//
|
||||||
// See `mvc#New` for more.
|
// See `mvc#New` for more.
|
||||||
type Application struct {
|
type Application struct {
|
||||||
Dependencies di.Values
|
container *hero.Container
|
||||||
// Sorter is a `di.Sorter`, can be used to customize the order of controller's fields
|
|
||||||
// and their available bindable values to set.
|
|
||||||
// Sorting matters only when a field can accept more than one registered value.
|
|
||||||
// Defaults to nil; order of registration matters when more than one field can accept the same value.
|
|
||||||
Sorter di.Sorter
|
|
||||||
Router router.Party
|
Router router.Party
|
||||||
Controllers []*ControllerActivator
|
Controllers []*ControllerActivator
|
||||||
websocketControllers []websocket.ConnHandler
|
websocketControllers []websocket.ConnHandler
|
||||||
ErrorHandler di.ErrorHandler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newApp(subRouter router.Party, values di.Values) *Application {
|
func newApp(subRouter router.Party, container *hero.Container) *Application {
|
||||||
return &Application{
|
return &Application{
|
||||||
Router: subRouter,
|
Router: subRouter,
|
||||||
Dependencies: values,
|
container: container,
|
||||||
ErrorHandler: di.DefaultErrorHandler,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,12 +44,7 @@ func newApp(subRouter router.Party, values di.Values) *Application {
|
||||||
//
|
//
|
||||||
// Example: `New(app.Party("/todo"))` or `New(app)` as it's the same as `New(app.Party("/"))`.
|
// Example: `New(app.Party("/todo"))` or `New(app)` as it's the same as `New(app.Party("/"))`.
|
||||||
func New(party router.Party) *Application {
|
func New(party router.Party) *Application {
|
||||||
values := di.NewValues()
|
return newApp(party, party.GetContainer())
|
||||||
if HeroDependencies {
|
|
||||||
values = hero.Dependencies().Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
return newApp(party, values)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure creates a new controller and configures it,
|
// Configure creates a new controller and configures it,
|
||||||
|
@ -104,12 +78,6 @@ func (app *Application) Configure(configurators ...func(*Application)) *Applicat
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutoBinding used to be registered as dependency to try to automatically
|
|
||||||
// map and bind the inputs that are not already binded with a dependency.
|
|
||||||
//
|
|
||||||
// A shortcut of `hero.AutoBinding`. Read more at: `hero#DefaultFallbackBinder`.
|
|
||||||
var AutoBinding = hero.AutoBinding
|
|
||||||
|
|
||||||
// Register appends one or more values as dependencies.
|
// Register appends one or more values as dependencies.
|
||||||
// The value can be a single struct value-instance or a function
|
// The value can be a single struct value-instance or a function
|
||||||
// which has one input and one output, the input should be
|
// which has one input and one output, the input should be
|
||||||
|
@ -117,39 +85,32 @@ var AutoBinding = hero.AutoBinding
|
||||||
// will be bind-ed to the controller's field, if matching or to the
|
// will be bind-ed to the controller's field, if matching or to the
|
||||||
// controller's methods, if matching.
|
// controller's methods, if matching.
|
||||||
//
|
//
|
||||||
// These dependencies "values" can be changed per-controller as well,
|
// These dependencies "dependencies" can be changed per-controller as well,
|
||||||
// via controller's `BeforeActivation` and `AfterActivation` methods,
|
// via controller's `BeforeActivation` and `AfterActivation` methods,
|
||||||
// look the `Handle` method for more.
|
// look the `Handle` method for more.
|
||||||
//
|
//
|
||||||
// It returns this Application.
|
// It returns this Application.
|
||||||
//
|
//
|
||||||
// Example: `.Register(loggerService{prefix: "dev"}, func(ctx iris.Context) User {...})`.
|
// Example: `.Register(loggerService{prefix: "dev"}, func(ctx iris.Context) User {...})`.
|
||||||
func (app *Application) Register(values ...interface{}) *Application {
|
func (app *Application) Register(dependencies ...interface{}) *Application {
|
||||||
if len(values) > 0 && app.Dependencies.Len() == 0 && len(app.Controllers) > 0 {
|
if len(dependencies) > 0 && len(app.container.Dependencies) == len(hero.BuiltinDependencies) && len(app.Controllers) > 0 {
|
||||||
allControllerNamesSoFar := make([]string, len(app.Controllers))
|
allControllerNamesSoFar := make([]string, len(app.Controllers))
|
||||||
for i := range app.Controllers {
|
for i := range app.Controllers {
|
||||||
allControllerNamesSoFar[i] = app.Controllers[i].Name()
|
allControllerNamesSoFar[i] = app.Controllers[i].Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
golog.Warnf(`mvc.Application#Register called after mvc.Application#Handle.
|
golog.Warnf(`mvc.Application#Register called after mvc.Application#Handle.
|
||||||
The controllers[%s] may miss required dependencies.
|
The controllers[%s] may miss required dependencies.
|
||||||
Set the Logger's Level to "debug" to view the active dependencies per controller.`, strings.Join(allControllerNamesSoFar, ","))
|
Set the Logger's Level to "debug" to view the active dependencies per controller.`, strings.Join(allControllerNamesSoFar, ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Dependencies.Add(values...)
|
for _, dependency := range dependencies {
|
||||||
|
app.container.Register(dependency)
|
||||||
|
}
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
// SortByNumMethods is the same as `app.Sorter = di.SortByNumMethods` which
|
|
||||||
// prioritize fields and their available values (only if more than one)
|
|
||||||
// with the highest number of methods,
|
|
||||||
// this way an empty interface{} is getting the "thinnest" available value.
|
|
||||||
func (app *Application) SortByNumMethods() *Application {
|
|
||||||
app.Sorter = di.SortByNumMethods
|
|
||||||
return app
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle serves a controller for the current mvc application's Router.
|
// Handle serves a controller for the current mvc application's Router.
|
||||||
// It accept any custom struct which its functions will be transformed
|
// It accept any custom struct which its functions will be transformed
|
||||||
// to routes.
|
// to routes.
|
||||||
|
@ -214,12 +175,9 @@ func (app *Application) HandleWebsocket(controller interface{}) *websocket.Struc
|
||||||
return websocketController
|
return websocketController
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeInjector(injector *di.StructInjector) websocket.StructInjector {
|
func makeInjector(s *hero.Struct) websocket.StructInjector {
|
||||||
return func(_ reflect.Type, nsConn *websocket.NSConn) reflect.Value {
|
return func(_ reflect.Type, nsConn *websocket.NSConn) reflect.Value {
|
||||||
v := injector.Acquire()
|
v, _ := s.Acquire(websocket.GetContext(nsConn.Conn))
|
||||||
if injector.CanInject {
|
|
||||||
injector.InjectElem(websocket.GetContext(nsConn.Conn), v.Elem())
|
|
||||||
}
|
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -239,7 +197,7 @@ func (app *Application) GetNamespaces() websocket.Namespaces {
|
||||||
|
|
||||||
func (app *Application) handle(controller interface{}) *ControllerActivator {
|
func (app *Application) handle(controller interface{}) *ControllerActivator {
|
||||||
// initialize the controller's activator, nothing too magical so far.
|
// initialize the controller's activator, nothing too magical so far.
|
||||||
c := newControllerActivator(app.Router, controller, app.Dependencies, app.Sorter, app.ErrorHandler)
|
c := newControllerActivator(app, controller)
|
||||||
|
|
||||||
// check the controller's "BeforeActivation" or/and "AfterActivation" method(s) between the `activate`
|
// check the controller's "BeforeActivation" or/and "AfterActivation" method(s) between the `activate`
|
||||||
// call, which is simply parses the controller's methods, end-dev can register custom controller's methods
|
// call, which is simply parses the controller's methods, end-dev can register custom controller's methods
|
||||||
|
@ -265,8 +223,11 @@ func (app *Application) handle(controller interface{}) *ControllerActivator {
|
||||||
// HandleError registers a `hero.ErrorHandlerFunc` which will be fired when
|
// HandleError registers a `hero.ErrorHandlerFunc` which will be fired when
|
||||||
// application's controllers' functions returns an non-nil error.
|
// application's controllers' functions returns an non-nil error.
|
||||||
// Each controller can override it by implementing the `hero.ErrorHandler`.
|
// Each controller can override it by implementing the `hero.ErrorHandler`.
|
||||||
func (app *Application) HandleError(errorHandler func(ctx context.Context, err error)) *Application {
|
func (app *Application) HandleError(handler func(ctx context.Context, err error)) *Application {
|
||||||
app.ErrorHandler = di.ErrorHandlerFunc(errorHandler)
|
errorHandler := hero.ErrorHandlerFunc(handler)
|
||||||
|
app.container.GetErrorHandler = func(context.Context) hero.ErrorHandler {
|
||||||
|
return errorHandler
|
||||||
|
}
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,8 +236,7 @@ func (app *Application) HandleError(errorHandler func(ctx context.Context, err e
|
||||||
//
|
//
|
||||||
// Example: `.Clone(app.Party("/path")).Handle(new(TodoSubController))`.
|
// Example: `.Clone(app.Party("/path")).Handle(new(TodoSubController))`.
|
||||||
func (app *Application) Clone(party router.Party) *Application {
|
func (app *Application) Clone(party router.Party) *Application {
|
||||||
cloned := newApp(party, app.Dependencies.Clone())
|
cloned := newApp(party, app.container.Clone())
|
||||||
cloned.ErrorHandler = app.ErrorHandler
|
|
||||||
return cloned
|
return cloned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
23
mvc/param.go
23
mvc/param.go
|
@ -12,29 +12,6 @@ func getPathParamsForInput(startParamIndex int, params []macro.TemplateParam, fu
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// consumedParams := make(map[int]bool, 0)
|
|
||||||
// for _, in := range funcIn {
|
|
||||||
// for j, p := range params {
|
|
||||||
// if _, consumed := consumedParams[j]; consumed {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String())
|
|
||||||
// if m := macros.Lookup(p.Type); m != nil && m.GoType == in.Kind() {
|
|
||||||
// consumedParams[j] = true
|
|
||||||
// // fmt.Printf("param.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String())
|
|
||||||
// funcDep, ok := context.ParamResolverByKindAndIndex(m.GoType, p.Index)
|
|
||||||
// // funcDep, ok := context.ParamResolverByKindAndKey(in.Kind(), paramName)
|
|
||||||
// if !ok {
|
|
||||||
// // here we can add a logger about invalid parameter type although it should never happen here
|
|
||||||
// // unless the end-developer modified the macro/macros with a special type but not the context/ParamResolvers.
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
// values = append(values, funcDep)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
consumed := make(map[int]struct{})
|
consumed := make(map[int]struct{})
|
||||||
for _, in := range funcIn {
|
for _, in := range funcIn {
|
||||||
for j, param := range params {
|
for j, param := range params {
|
||||||
|
|
|
@ -3,12 +3,12 @@ package mvc
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/hero/di"
|
"github.com/kataras/iris/v12/hero"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
baseControllerTyp = reflect.TypeOf((*BaseController)(nil)).Elem()
|
baseControllerTyp = reflect.TypeOf((*BaseController)(nil)).Elem()
|
||||||
errorHandlerTyp = reflect.TypeOf((*di.ErrorHandler)(nil)).Elem()
|
errorHandlerTyp = reflect.TypeOf((*hero.ErrorHandler)(nil)).Elem()
|
||||||
errorTyp = reflect.TypeOf((*error)(nil)).Elem()
|
errorTyp = reflect.TypeOf((*error)(nil)).Elem()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,6 +16,17 @@ func isBaseController(ctrlTyp reflect.Type) bool {
|
||||||
return ctrlTyp.Implements(baseControllerTyp)
|
return ctrlTyp.Implements(baseControllerTyp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// indirectType returns the value of a pointer-type "typ".
|
||||||
|
// If "typ" is a pointer, array, chan, map or slice it returns its Elem,
|
||||||
|
// otherwise returns the typ as it's.
|
||||||
|
func indirectType(typ reflect.Type) reflect.Type {
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
|
||||||
|
return typ.Elem()
|
||||||
|
}
|
||||||
|
return typ
|
||||||
|
}
|
||||||
|
|
||||||
func isErrorHandler(ctrlTyp reflect.Type) bool {
|
func isErrorHandler(ctrlTyp reflect.Type) bool {
|
||||||
return ctrlTyp.Implements(errorHandlerTyp)
|
return ctrlTyp.Implements(errorHandlerTyp)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user