From d254d48f346e81e86170da5010e86c5e2bf8a4cf Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Mon, 17 Jul 2023 18:30:45 +0300 Subject: [PATCH] Add mvc.Application.EnableStructDependents() and app.ConfigureContainer().EnableStructDependents() relative to: #2158 --- HISTORY.md | 2 ++ core/router/api_container.go | 8 ++++++++ go.mod | 6 +++--- go.sum | 13 ++++++------- hero/binding.go | 4 ++-- hero/binding_test.go | 4 ++-- hero/container.go | 34 +++++++++++++++++++++------------- hero/dependency.go | 34 ++++++++++++++++++++++++++++------ hero/reflect.go | 4 ++++ hero/struct.go | 2 +- mvc/mvc.go | 8 ++++++++ 11 files changed, 85 insertions(+), 34 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 9203a1370..b4bf672fc 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -23,6 +23,8 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene Changes apply to `master` branch. +- Add `mvc.Application.EnableStructDependents()` method to handle [#2158](https://github.com/kataras/iris/issues/2158). + - Fix [iris-premium#17](https://github.com/kataras/iris-premium/issues/17). - Replace [russross/blackfriday](github.com/russross/blackfriday/v2) with [gomarkdown](https://github.com/gomarkdown/markdown) as requested at [#2098](https://github.com/kataras/iris/issues/2098). diff --git a/core/router/api_container.go b/core/router/api_container.go index c1d9d9e90..0c571fa9c 100644 --- a/core/router/api_container.go +++ b/core/router/api_container.go @@ -95,6 +95,14 @@ func (api *APIContainer) EnableStrictMode(strictMode bool) *APIContainer { return api } +// EnableStructDependents sets the container's EnableStructDependents to true. +// It's used to automatically fill the dependencies of a struct's fields +// based on the previous registered dependencies, just like function inputs. +func (api *APIContainer) EnableStructDependents() *APIContainer { + api.Container.EnableStructDependents = true + return api +} + // SetDependencyMatcher replaces the function that compares equality between // a dependency and an input (struct field or function parameter). // diff --git a/go.mod b/go.mod index 2af1470ff..256d37918 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/fatih/structs v1.1.0 github.com/flosch/pongo2/v4 v4.0.2 github.com/golang/snappy v0.0.4 - github.com/gomarkdown/markdown v0.0.0-20230313173142-2ced44d5b584 + github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 github.com/google/uuid v1.3.0 github.com/gorilla/securecookie v1.1.1 github.com/iris-contrib/httpexpect/v2 v2.12.1 @@ -72,13 +72,13 @@ require ( github.com/iris-contrib/go.uuid v2.0.0+incompatible // indirect github.com/josharian/intern v1.0.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mediocregopher/radix/v3 v3.8.1 // indirect github.com/minio/highwayhash v1.0.2 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/nats-io/jwt/v2 v2.4.0 // indirect + github.com/nats-io/jwt/v2 v2.4.1 // indirect github.com/nats-io/nats.go v1.23.0 // indirect github.com/nats-io/nkeys v0.4.4 // indirect github.com/nats-io/nuid v1.0.1 // indirect diff --git a/go.sum b/go.sum index d53d3a833..82bfa11ee 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/gomarkdown/markdown v0.0.0-20230313173142-2ced44d5b584 h1:XaUmlCIi5hEY5GPUV6oXc5eytg9+FBH9/9fOKblHWEU= -github.com/gomarkdown/markdown v0.0.0-20230313173142-2ced44d5b584/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 h1:uK3X/2mt4tbSGoHvbLBHUny7CKiuwUip3MArtukol4E= +github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -135,8 +135,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mediocregopher/radix/v3 v3.8.1 h1:rOkHflVuulFKlwsLY01/M2cM2tWCjDoETcMqKbAWu1M= github.com/mediocregopher/radix/v3 v3.8.1/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw= @@ -151,8 +151,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/nats-io/jwt/v2 v2.4.0 h1:1woVcq37qhNwJOeZ4ZoRy5NJU5bvbtGsIammf2GpuJQ= -github.com/nats-io/jwt/v2 v2.4.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= +github.com/nats-io/jwt/v2 v2.4.1 h1:Y35W1dgbbz2SQUYDPCaclXcuqleVmpbRa7646Jf2EX4= +github.com/nats-io/jwt/v2 v2.4.1/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= github.com/nats-io/nats-server/v2 v2.9.11 h1:4y5SwWvWI59V5mcqtuoqKq6L9NDUydOP3Ekwuwl8cZI= github.com/nats-io/nats.go v1.23.0 h1:lR28r7IX44WjYgdiKz9GmUeW0uh/m33uD3yEjLZ2cOE= github.com/nats-io/nats.go v1.23.0/go.mod h1:ki/Scsa23edbh8IRZbCuNXR9TDcbvfaSijKtaqQgw+Q= @@ -291,7 +291,6 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/hero/binding.go b/hero/binding.go index 07fe12975..dfcf537c2 100644 --- a/hero/binding.go +++ b/hero/binding.go @@ -291,7 +291,7 @@ func getBindingsForFunc(fn reflect.Value, dependencies []*Dependency, disablePay return bindings } -func getBindingsForStruct(v reflect.Value, dependencies []*Dependency, markExportedFieldsAsRequired bool, disablePayloadAutoBinding bool, matchDependency DependencyMatcher, paramsCount int, sorter Sorter) (bindings []*binding) { +func getBindingsForStruct(v reflect.Value, dependencies []*Dependency, markExportedFieldsAsRequired bool, disablePayloadAutoBinding, enableStructDependents bool, matchDependency DependencyMatcher, paramsCount int, sorter Sorter) (bindings []*binding) { typ := indirectType(v.Type()) if typ.Kind() != reflect.Struct { panic(fmt.Sprintf("bindings: unresolved: not a struct type: %#+v", v)) @@ -303,7 +303,7 @@ func getBindingsForStruct(v reflect.Value, dependencies []*Dependency, markExpor 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(), disablePayloadAutoBinding, nil), + Dependency: newDependency(elem.FieldByIndex(f.Index).Interface(), disablePayloadAutoBinding, enableStructDependents, nil), Input: newStructFieldInput(f), }) } diff --git a/hero/binding_test.go b/hero/binding_test.go index 6781f807d..9b9c67800 100644 --- a/hero/binding_test.go +++ b/hero/binding_test.go @@ -524,7 +524,7 @@ func TestBindingsForStruct(t *testing.T) { } for i, tt := range tests { - bindings := getBindingsForStruct(reflect.ValueOf(tt.Value), tt.Registered, false, false, DefaultDependencyMatcher, 0, nil) + bindings := getBindingsForStruct(reflect.ValueOf(tt.Value), tt.Registered, false, false, false, DefaultDependencyMatcher, 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) @@ -565,5 +565,5 @@ func TestBindingsForStructMarkExportedFieldsAsRequred(t *testing.T) { } // should panic if fail. - _ = getBindingsForStruct(reflect.ValueOf(new(controller)), dependencies, true, true, DefaultDependencyMatcher, 0, nil) + _ = getBindingsForStruct(reflect.ValueOf(new(controller)), dependencies, true, true, false, DefaultDependencyMatcher, 0, nil) } diff --git a/hero/container.go b/hero/container.go index 437366a1a..06155c682 100644 --- a/hero/container.go +++ b/hero/container.go @@ -57,6 +57,13 @@ type Container struct { // if at least one input binding depends on the request and not in a static structure. DisableStructDynamicBindings bool + // StructDependents if true then the Container will try to resolve + // the fields of a struct value, if any, when it's a dependent struct value + // based on the previous registered dependencies. + // + // Defaults to false. + EnableStructDependents bool // this can be renamed to IndirectDependencies?. + // DependencyMatcher holds the function that compares equality between // a dependency with an input. Defaults to DefaultMatchDependencyFunc. DependencyMatcher DependencyMatcher @@ -146,11 +153,11 @@ func (c *Container) fillReport(fullName string, bindings []*binding) { // Contains the iris context, standard context, iris sessions and time dependencies. var BuiltinDependencies = []*Dependency{ // iris context dependency. - newDependency(func(ctx *context.Context) *context.Context { return ctx }, true, nil).Explicitly(), + newDependency(func(ctx *context.Context) *context.Context { return ctx }, true, false, nil).Explicitly(), // standard context dependency. newDependency(func(ctx *context.Context) stdContext.Context { return ctx.Request().Context() - }, true, nil).Explicitly(), + }, true, false, nil).Explicitly(), // iris session dependency. newDependency(func(ctx *context.Context) *sessions.Session { session := sessions.Get(ctx) @@ -163,35 +170,35 @@ var BuiltinDependencies = []*Dependency{ } return session - }, true, nil).Explicitly(), + }, true, false, nil).Explicitly(), // application's logger. newDependency(func(ctx *context.Context) *golog.Logger { return ctx.Application().Logger() - }, true, nil).Explicitly(), + }, true, false, nil).Explicitly(), // time.Time to time.Now dependency. newDependency(func(ctx *context.Context) time.Time { return time.Now() - }, true, nil).Explicitly(), + }, true, false, nil).Explicitly(), // standard http Request dependency. newDependency(func(ctx *context.Context) *http.Request { return ctx.Request() - }, true, nil).Explicitly(), + }, true, false, nil).Explicitly(), // standard http ResponseWriter dependency. newDependency(func(ctx *context.Context) http.ResponseWriter { return ctx.ResponseWriter() - }, true, nil).Explicitly(), + }, true, false, nil).Explicitly(), // http headers dependency. newDependency(func(ctx *context.Context) http.Header { return ctx.Request().Header - }, true, nil).Explicitly(), + }, true, false, nil).Explicitly(), // Client IP. newDependency(func(ctx *context.Context) net.IP { return net.ParseIP(ctx.RemoteAddr()) - }, true, nil).Explicitly(), + }, true, false, nil).Explicitly(), // Status Code (special type for MVC HTTP Error handler to not conflict with path parameters) newDependency(func(ctx *context.Context) Code { return Code(ctx.GetStatusCode()) - }, true, nil).Explicitly(), + }, true, false, nil).Explicitly(), // Context Error. May be nil newDependency(func(ctx *context.Context) Err { err := ctx.GetErr() @@ -199,7 +206,7 @@ var BuiltinDependencies = []*Dependency{ return nil } return err - }, true, nil).Explicitly(), + }, true, false, nil).Explicitly(), // Context User, e.g. from basic authentication. newDependency(func(ctx *context.Context) context.User { u := ctx.User() @@ -208,7 +215,7 @@ var BuiltinDependencies = []*Dependency{ } return u - }, true, nil), + }, true, false, nil), // payload and param bindings are dynamically allocated and declared at the end of the `binding` source file. } @@ -254,6 +261,7 @@ func (c *Container) Clone() *Container { cloned.Dependencies = clonedDeps cloned.DisablePayloadAutoBinding = c.DisablePayloadAutoBinding cloned.DisableStructDynamicBindings = c.DisableStructDynamicBindings + cloned.EnableStructDependents = c.EnableStructDependents cloned.MarkExportedFieldsAsRequired = c.MarkExportedFieldsAsRequired cloned.resultHandlers = c.resultHandlers // Reports are not cloned. @@ -291,7 +299,7 @@ func Register(dependency interface{}) *Dependency { // - Register(func(ctx iris.Context) User {...}) // - Register(func(User) OtherResponse {...}) func (c *Container) Register(dependency interface{}) *Dependency { - d := newDependency(dependency, c.DisablePayloadAutoBinding, c.DependencyMatcher, c.Dependencies...) + d := newDependency(dependency, c.DisablePayloadAutoBinding, c.EnableStructDependents, c.DependencyMatcher, 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) diff --git a/hero/dependency.go b/hero/dependency.go index 711ef28e0..d7aa7d4cf 100644 --- a/hero/dependency.go +++ b/hero/dependency.go @@ -38,6 +38,13 @@ type ( // Match holds the matcher. Defaults to the Container's one. Match DependencyMatchFunc + + // StructDependents if true then the Container will try to resolve + // the fields of a struct value, if any, when it's a dependent struct value + // based on the previous registered dependencies. + // + // Defaults to false. + StructDependents bool } ) @@ -50,6 +57,12 @@ func (d *Dependency) Explicitly() *Dependency { return d } +// EnableStructDependents sets StructDependents to true. +func (d *Dependency) EnableStructDependents() *Dependency { + d.StructDependents = true + return d +} + func (d *Dependency) String() string { sourceLine := d.Source.String() val := d.OriginalValue @@ -63,10 +76,16 @@ func (d *Dependency) String() string { // // See `Container.Handler` for more. func NewDependency(dependency interface{}, funcDependencies ...*Dependency) *Dependency { // used only on tests. - return newDependency(dependency, false, nil, funcDependencies...) + return newDependency(dependency, false, false, nil, funcDependencies...) } -func newDependency(dependency interface{}, disablePayloadAutoBinding bool, matchDependency DependencyMatcher, funcDependencies ...*Dependency) *Dependency { +func newDependency( + dependency interface{}, + disablePayloadAutoBinding bool, + enableStructDependents bool, + matchDependency DependencyMatcher, + funcDependencies ...*Dependency, +) *Dependency { if dependency == nil { panic(fmt.Sprintf("bad value: nil: %T", dependency)) } @@ -86,8 +105,9 @@ func newDependency(dependency interface{}, disablePayloadAutoBinding bool, match } dest := &Dependency{ - Source: newSource(v), - OriginalValue: dependency, + Source: newSource(v), + OriginalValue: dependency, + StructDependents: enableStructDependents, } dest.Match = ToDependencyMatchFunc(dest, matchDependency) @@ -171,7 +191,7 @@ func fromStructValueOrDependentStructValue(v reflect.Value, disablePayloadAutoBi return false } - if len(prevDependencies) == 0 { // As a non depedent struct. + if len(prevDependencies) == 0 || !dest.StructDependents { // As a non depedent struct. // We must make this check so we can avoid the auto-filling of // the dependencies from Iris builtin dependencies. return fromStructValue(v, dest) @@ -180,11 +200,13 @@ func fromStructValueOrDependentStructValue(v reflect.Value, disablePayloadAutoBi // Check if it's a builtin dependency (e.g an MVC Application (see mvc.go#newApp)), // if it's and registered without a Dependency wrapper, like the rest builtin dependencies, // then do NOT try to resolve its fields. + // + // Although EnableStructDependents is false by default, we must check if it's a builtin dependency for any case. if strings.HasPrefix(indirectType(v.Type()).PkgPath(), "github.com/kataras/iris/v12") { return fromStructValue(v, dest) } - bindings := getBindingsForStruct(v, prevDependencies, false, disablePayloadAutoBinding, DefaultDependencyMatcher, -1, nil) + bindings := getBindingsForStruct(v, prevDependencies, false, disablePayloadAutoBinding, dest.StructDependents, DefaultDependencyMatcher, -1, nil) if len(bindings) == 0 { return fromStructValue(v, dest) // same as above. } diff --git a/hero/reflect.go b/hero/reflect.go index e3881cbbc..d214f01bf 100644 --- a/hero/reflect.go +++ b/hero/reflect.go @@ -154,6 +154,10 @@ func lookupFields(elem reflect.Value, skipUnexported bool, onlyZeros bool, paren // Note: embedded pointers are not supported. // elem = reflect.Indirect(elem) elemTyp := elem.Type() + if elemTyp.Kind() == reflect.Pointer { + return + } + for i, n := 0, elem.NumField(); i < n; i++ { field := elemTyp.Field(i) fieldValue := elem.Field(i) diff --git a/hero/struct.go b/hero/struct.go index 536eea402..30f6eba8c 100644 --- a/hero/struct.go +++ b/hero/struct.go @@ -51,7 +51,7 @@ func makeStruct(structPtr interface{}, c *Container, partyParamsCount int) *Stru } // get struct's fields bindings. - bindings := getBindingsForStruct(v, c.Dependencies, c.MarkExportedFieldsAsRequired, c.DisablePayloadAutoBinding, c.DependencyMatcher, partyParamsCount, c.Sorter) + bindings := getBindingsForStruct(v, c.Dependencies, c.MarkExportedFieldsAsRequired, c.DisablePayloadAutoBinding, c.EnableStructDependents, c.DependencyMatcher, partyParamsCount, 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. diff --git a/mvc/mvc.go b/mvc/mvc.go index 3ae2e80e6..981387cbb 100644 --- a/mvc/mvc.go +++ b/mvc/mvc.go @@ -142,6 +142,14 @@ func (app *Application) SetControllersNoLog(disable bool) *Application { return app } +// EnableStructDependents will try to resolve +// the fields of a struct value, if any, when it's a dependent struct value +// based on the previous registered dependencies. +func (app *Application) EnableStructDependents() *Application { + app.container.EnableStructDependents = true + return app +} + // Register appends 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