1
0
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:
Gerasimos (Makis) Maropoulos
2020-02-29 14:18:15 +02:00
parent 027eb5d6da
commit 5fc24812bc
54 changed files with 2916 additions and 2184 deletions

340
hero/binding.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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())
}
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)}
}

View File

@@ -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
}

View File

@@ -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))
}
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
View 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
View 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)
}
}
}

View File

@@ -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
View 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
View 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)
}