mirror of
https://github.com/kataras/iris.git
synced 2025-12-18 02:17:05 +00: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:
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"
|
||||
|
||||
"github.com/kataras/iris/v12/context"
|
||||
"github.com/kataras/iris/v12/hero/di"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
)
|
||||
@@ -58,80 +57,15 @@ type compatibleErr interface {
|
||||
Error() string
|
||||
}
|
||||
|
||||
// DefaultErrStatusCode is the default error status code (400)
|
||||
// when the response contains an error which is not nil.
|
||||
var DefaultErrStatusCode = 400
|
||||
|
||||
// DispatchErr writes the error to the response.
|
||||
func DispatchErr(ctx context.Context, status int, err error) {
|
||||
if status < 400 {
|
||||
status = DefaultErrStatusCode
|
||||
// dispatchErr writes the error to the response.
|
||||
func dispatchErr(ctx context.Context, status int, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.StatusCode(status)
|
||||
if text := err.Error(); text != "" {
|
||||
ctx.WriteString(text)
|
||||
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)
|
||||
DefaultErrorHandler.HandleError(ctx, err)
|
||||
return true
|
||||
}
|
||||
|
||||
// DispatchFuncResult is being used internally to resolve
|
||||
@@ -163,9 +97,9 @@ func DispatchCommon(ctx context.Context,
|
||||
// Result or (Result, error) and so on...
|
||||
//
|
||||
// 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 {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
// except when found == false or err != nil.
|
||||
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.
|
||||
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
|
||||
content = value
|
||||
case compatibleErr:
|
||||
if value == nil || di.IsNil(v) {
|
||||
if value == nil || isNil(v) {
|
||||
continue
|
||||
}
|
||||
|
||||
if errorHandler != nil {
|
||||
errorHandler.HandleError(ctx, value)
|
||||
return
|
||||
}
|
||||
|
||||
err = value
|
||||
if statusCode < 400 {
|
||||
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:
|
||||
// else it's a custom struct or a dispatcher, we'll decide later
|
||||
// because content type and status code matters
|
||||
@@ -316,7 +239,7 @@ func DispatchFuncResult(ctx context.Context, errorHandler di.ErrorHandler, value
|
||||
if custom == nil {
|
||||
// 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
|
||||
// then try to get the previous response writer's content type,
|
||||
// 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.
|
||||
@@ -401,7 +378,12 @@ func (r Response) Dispatch(ctx context.Context) {
|
||||
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.
|
||||
@@ -445,13 +427,7 @@ func ensureExt(s string) string {
|
||||
// Dispatch writes the template filename, template layout and (any) data to the client.
|
||||
// Completes the `Result` interface.
|
||||
func (r View) Dispatch(ctx context.Context) { // r as Response view.
|
||||
if r.Err != nil {
|
||||
if r.Code < 400 {
|
||||
r.Code = DefaultErrStatusCode
|
||||
}
|
||||
ctx.StatusCode(r.Code)
|
||||
ctx.WriteString(r.Err.Error())
|
||||
ctx.StopExecution()
|
||||
if dispatchErr(ctx, r.Code, r.Err) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -482,7 +458,7 @@ func (r View) Dispatch(ctx context.Context) { // r as Response view.
|
||||
setViewData(ctx, m)
|
||||
} else if m, ok := r.Data.(context.Map); ok {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
162
hero/handler.go
162
hero/handler.go
@@ -3,91 +3,115 @@ package hero
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
"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 IsGenericFunc(inTyp reflect.Type) bool {
|
||||
// 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 (fn ErrorHandlerFunc) HandleError(ctx context.Context, err error) {
|
||||
fn(ctx, err)
|
||||
}
|
||||
|
||||
func validateHandler(handler interface{}) error {
|
||||
if typ := reflect.TypeOf(handler); !di.IsFunc(typ) {
|
||||
return fmt.Errorf("handler expected to be a kind of func but got typeof(%s)", typ.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var (
|
||||
// DefaultErrStatusCode is the default error status code (400)
|
||||
// when the response contains a non-nil error or a request-scoped binding error occur.
|
||||
DefaultErrStatusCode = 400
|
||||
|
||||
// makeHandler accepts a "handler" function which can accept any input arguments that match
|
||||
// with the "values" types and any output result, that matches the hero types, like string, int (string,int),
|
||||
// custom structs, Result(View | Response) and anything that you can imagine,
|
||||
// and returns a low-level `context/iris.Handler` which can be used anywhere in the Iris Application,
|
||||
// as middleware or as simple route handler or party handler or subdomain handler-router.
|
||||
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))
|
||||
// DefaultErrorHandler is the default error handler which is fired
|
||||
// when a function returns a non-nil error or a request-scoped dependency failed to binded.
|
||||
DefaultErrorHandler = ErrorHandlerFunc(func(ctx context.Context, err error) {
|
||||
if status := ctx.GetStatusCode(); status == 0 || !context.StatusCodeNotSuccessful(status) {
|
||||
ctx.StatusCode(DefaultErrStatusCode)
|
||||
}
|
||||
|
||||
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...)
|
||||
funcInjector.ErrorHandler = errorHandler
|
||||
// 0. A normal handler.
|
||||
if handler, ok := isHandler(fn); ok {
|
||||
return handler
|
||||
}
|
||||
|
||||
valid := funcInjector.Length == n
|
||||
|
||||
if !valid {
|
||||
// is invalid when input len and values are not match
|
||||
// or their types are not match, we will take look at the
|
||||
// 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
|
||||
// 1. A handler which returns just an error, handle it faster.
|
||||
if handlerWithErr, ok := isHandlerWithError(fn); ok {
|
||||
return func(ctx context.Context) {
|
||||
if err := handlerWithErr(ctx); err != nil {
|
||||
c.GetErrorHandler(ctx).HandleError(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h := func(ctx context.Context) {
|
||||
// in := make([]reflect.Value, n, n)
|
||||
// funcInjector.Inject(&in, reflect.ValueOf(ctx))
|
||||
// DispatchFuncResult(ctx, fn.Call(in))
|
||||
DispatchFuncResult(ctx, nil, funcInjector.Call(ctx))
|
||||
v := valueOf(fn)
|
||||
numIn := v.Type().NumIn()
|
||||
|
||||
bindings := getBindingsForFunc(v, c.Dependencies, c.ParamStartIndex)
|
||||
|
||||
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
|
||||
|
||||
// black-box
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/httptest"
|
||||
|
||||
. "github.com/kataras/iris/v12/hero"
|
||||
"github.com/kataras/iris/v12/httptest"
|
||||
)
|
||||
|
||||
// dynamic func
|
||||
@@ -29,25 +26,25 @@ func testBinderFunc(ctx iris.Context) testUserStruct {
|
||||
|
||||
// service
|
||||
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
|
||||
// because of the controller handler test.
|
||||
TestService interface {
|
||||
testService interface {
|
||||
Say(string) string
|
||||
}
|
||||
TestServiceImpl struct {
|
||||
testServiceImpl struct {
|
||||
prefix string
|
||||
}
|
||||
)
|
||||
|
||||
func (s *TestServiceImpl) Say(message string) string {
|
||||
func (s *testServiceImpl) Say(message string) string {
|
||||
return s.prefix + " " + message
|
||||
}
|
||||
|
||||
var (
|
||||
// binders, as user-defined
|
||||
testBinderFuncUserStruct = testBinderFunc
|
||||
testBinderService = &TestServiceImpl{prefix: "say"}
|
||||
testBinderService = &testServiceImpl{prefix: "say"}
|
||||
testBinderFuncParam = func(ctx iris.Context) string {
|
||||
return ctx.Params().Get("param")
|
||||
}
|
||||
@@ -60,7 +57,7 @@ var (
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
// 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) {
|
||||
Register(testBinderFuncUserStruct, testBinderService, testBinderFuncParam)
|
||||
Register(testBinderFuncUserStruct)
|
||||
Register(testBinderService)
|
||||
Register(testBinderFuncParam)
|
||||
var (
|
||||
h1 = Handler(testConsumeUserHandler)
|
||||
h2 = Handler(testConsumeServiceHandler)
|
||||
@@ -112,7 +111,7 @@ func TestBindFunctionAsFunctionInputArgument(t *testing.T) {
|
||||
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
|
||||
// to the client.
|
||||
return get("username")
|
||||
@@ -127,9 +126,8 @@ func TestBindFunctionAsFunctionInputArgument(t *testing.T) {
|
||||
Expect().Status(iris.StatusOK).Body().Equal(expectedUsername)
|
||||
}
|
||||
|
||||
func TestAutoBinding(t *testing.T) {
|
||||
func TestPayloadBinding(t *testing.T) {
|
||||
h := New()
|
||||
h.Register(AutoBinding)
|
||||
|
||||
postHandler := h.Handler(func(input *testUserStruct /* ptr */) string {
|
||||
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("/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) {
|
||||
got = f() + firstname + lastname
|
||||
got = firstname + lastname
|
||||
})
|
||||
|
||||
ctx := context.NewContext(nil)
|
||||
@@ -28,20 +28,20 @@ func TestPathParams(t *testing.T) {
|
||||
handler(ctx)
|
||||
expected := "GerasimosMaropoulos"
|
||||
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 = ""
|
||||
handlerWithOther(ctx)
|
||||
expected = "GerasimosMaropoulos"
|
||||
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 = ""
|
||||
handlerWithOtherBetweenThem(ctx)
|
||||
expected = "GerasimosMaropoulos"
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user