package hero import ( stdContext "context" "errors" "net" "net/http" "reflect" "strings" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/sessions" "github.com/kataras/golog" ) // Default is the default container value which can be used for dependencies share. var Default = New().WithLogger(golog.Default) // 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 { // Optional Logger to report dependencies and matched bindings // per struct, function and method. // By default it is set by the Party creator of this Container. Logger *golog.Logger // 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 // MarkExportedFieldsAsRequired reports whether all struct's fields // MUST be binded to a dependency from the `Dependencies` list field. // In-short, if it is set to true and if at least one exported field // of a struct is not binded to a dependency then // the entire application will exit with a panic message before server startup. MarkExportedFieldsAsRequired bool // DisablePayloadAutoBinding reports whether // a function's parameter or struct's field of struct type // should not be binded automatically to the request body (e.g. JSON) // if a dependency for that type is missing. // By default the binder will bind structs to request body, // set to true to disable that kind of behavior. DisablePayloadAutoBinding bool // DependencyMatcher holds the function that compares equality between // a dependency with an input. Defaults to DefaultMatchDependencyFunc. DependencyMatcher DependencyMatcher // 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. // Reports contains an ordered list of information about bindings for further analysys and testing. Reports []*Report // resultHandlers is a list of functions that serve the return struct value of a function handler. // Defaults to "defaultResultHandler" but it can be overridden. resultHandlers []func(next ResultHandler) ResultHandler } // A Report holds meta information about dependency sources and target values per package, // struct, struct's fields, struct's method, package-level function or closure. // E.g. main -> (*UserController) -> HandleHTTPError. type Report struct { // The name is the last part of the name of a struct or its methods or a function. // Each name is splited by its package.struct.field or package.funcName or package.func.inlineFunc. Name string // If it's a struct or package or function // then it contains children reports of each one of its methods or input parameters // respectfully. Reports []*Report Parent *Report Entries []ReportEntry } // A ReportEntry holds the information about a binding. type ReportEntry struct { InputPosition int // struct field position or parameter position. InputFieldName string // if it's a struct field, then this is its type name (we can't get param names). InputFieldType reflect.Type // the input's type. DependencyValue interface{} // the dependency value binded to that InputPosition of Name. DependencyFile string // the file DependencyLine int // and line number of the dependency's value. Static bool } func (r *Report) fill(bindings []*binding) { for _, b := range bindings { inputFieldName := b.Input.StructFieldName if inputFieldName == "" { // it's not a struct field, then type. inputFieldName = b.Input.Type.String() } // remove only the main one prefix. inputFieldName = strings.TrimPrefix(inputFieldName, "main.") fieldName := inputFieldName switch fieldName { case "*context.Context": inputFieldName = strings.Replace(inputFieldName, "*context", "iris", 1) case "hero.Code", "hero.Result", "hero.View", "hero.Response": inputFieldName = strings.Replace(inputFieldName, "hero", "mvc", 1) } entry := ReportEntry{ InputPosition: b.Input.Index, InputFieldName: inputFieldName, InputFieldType: b.Input.Type, DependencyValue: b.Dependency.OriginalValue, DependencyFile: b.Dependency.Source.File, DependencyLine: b.Dependency.Source.Line, Static: b.Dependency.Static, } r.Entries = append(r.Entries, entry) } } // fillReport adds a report to the Reports field. func (c *Container) fillReport(fullName string, bindings []*binding) { // r := c.getReport(fullName) r := &Report{ Name: fullName, } r.fill(bindings) c.Reports = append(c.Reports, r) } // BuiltinDependencies is a list of builtin dependencies that are added on Container's initilization. // 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(), // standard context dependency. newDependency(func(ctx *context.Context) stdContext.Context { return ctx.Request().Context() }, true, nil).Explicitly(), // iris session dependency. newDependency(func(ctx *context.Context) *sessions.Session { session := sessions.Get(ctx) if session == nil { ctx.Application().Logger().Debugf("binding: session is nil\nMaybe inside HandleHTTPError? Register it with app.UseRouter(sess.Handler()) to fix it") // let's don't panic here and let the application continue, now we support // not matched routes inside the controller through HandleHTTPError, // so each dependency can check if session was not nil or just use `UseRouter` instead of `Use` // to register the sessions middleware. } return session }, true, nil).Explicitly(), // application's logger. newDependency(func(ctx *context.Context) *golog.Logger { return ctx.Application().Logger() }, true, nil).Explicitly(), // time.Time to time.Now dependency. newDependency(func(ctx *context.Context) time.Time { return time.Now() }, true, nil).Explicitly(), // standard http Request dependency. newDependency(func(ctx *context.Context) *http.Request { return ctx.Request() }, true, nil).Explicitly(), // standard http ResponseWriter dependency. newDependency(func(ctx *context.Context) http.ResponseWriter { return ctx.ResponseWriter() }, true, nil).Explicitly(), // http headers dependency. newDependency(func(ctx *context.Context) http.Header { return ctx.Request().Header }, true, nil).Explicitly(), // Client IP. newDependency(func(ctx *context.Context) net.IP { return net.ParseIP(ctx.RemoteAddr()) }, true, 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(), // Context Error. May be nil newDependency(func(ctx *context.Context) Err { err := ctx.GetErr() if err == nil { return nil } return err }, true, nil).Explicitly(), // Context User, e.g. from basic authentication. newDependency(func(ctx *context.Context) context.User { u := ctx.User() if u == nil { return nil } return u }, true, nil), // 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{ Sorter: sortByNumMethods, Dependencies: deps, GetErrorHandler: func(*context.Context) ErrorHandler { return DefaultErrorHandler }, DependencyMatcher: DefaultDependencyMatcher, } for _, dependency := range dependencies { c.Register(dependency) } return c } // WithLogger injects a logger to use to debug dependencies and bindings. func (c *Container) WithLogger(logger *golog.Logger) *Container { c.Logger = logger 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.Logger = c.Logger cloned.GetErrorHandler = c.GetErrorHandler cloned.Sorter = c.Sorter cloned.DependencyMatcher = c.DependencyMatcher clonedDeps := make([]*Dependency, len(c.Dependencies)) copy(clonedDeps, c.Dependencies) cloned.Dependencies = clonedDeps cloned.DisablePayloadAutoBinding = c.DisablePayloadAutoBinding cloned.MarkExportedFieldsAsRequired = c.MarkExportedFieldsAsRequired cloned.resultHandlers = c.resultHandlers // Reports are not cloned. 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: // * {structValue} // * func(accepts ) returns or (, error) // * func(accepts iris.Context) returns or (, error) // * func(accepts1 iris.Context, accepts2 *hero.Input) returns or (, error) // // A Dependency can accept a previous registered dependency and return a new one or the same updated. // * func(accepts1 , accepts2 ) returns or (, error) or error // * func(acceptsPathParameter1 string, id uint64) returns or (, 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.DisablePayloadAutoBinding, 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) c.Dependencies = append([]*Dependency{d}, c.Dependencies...) } else { c.Dependencies = append(c.Dependencies, d) } return d } // UseResultHandler adds a result handler to the Container. // A result handler can be used to inject the returned struct value // from a request handler or to replace the default renderer. func (c *Container) UseResultHandler(handler func(next ResultHandler) ResultHandler) *Container { c.resultHandlers = append(c.resultHandlers, handler) return c } // 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(...) iris.Handler // - if are all static dependencies then // there is no reflection involved at serve-time. // // func(pathParameter string, ...) // - one or more path parameters (e.g. :uid, :string, :int, :path, :uint64) // are automatically binded to the first input Go standard types (string, int, uint64 and e.t.c.) // // func() error // - if a function returns an error then this error's text is sent to the client automatically. // // func() // - The result of the function is a dependency too. // If is a request-scope dependency (dynamic) then // this function will be called at every request. // // func() // - If is static dependency (e.g. a database or a service) then its result // can be used as a static dependency to the next dependencies or to the controller/function itself. func (c *Container) Handler(fn interface{}) context.Handler { return c.HandlerWithParams(fn, 0) } // HandlerWithParams same as `Handler` but it can receive a total path parameters counts // to resolve coblex path parameters input dependencies. func (c *Container) HandlerWithParams(fn interface{}, paramsCount int) context.Handler { return makeHandler(fn, c, paramsCount) } // Struct accepts a pointer to a struct value and returns a structure which // contains bindings for the struct's fields and a method to // extract a Handler from this struct's method. func (c *Container) Struct(ptrValue interface{}, partyParamsCount int) *Struct { return makeStruct(ptrValue, c, partyParamsCount) } // ErrMissingDependency may returned only from the `Container.Inject` method // when not a matching dependency found for "toPtr". var ErrMissingDependency = errors.New("missing dependency") // Inject SHOULD only be used outside of HTTP handlers (performance is not priority for this method) // as it does not pre-calculate the available list of bindings for the "toPtr" and the registered dependencies. // // It sets a static-only matching dependency to the value of "toPtr". // The parameter "toPtr" SHOULD be a pointer to a value corresponding to a dependency, // like input parameter of a handler or field of a struct. // // If no matching dependency found, the `Inject` method returns an `ErrMissingDependency` and // "toPtr" keeps its original state (e.g. nil). // // Example Code: // c.Register(&LocalDatabase{...}) // [...] // var db Database // err := c.Inject(&db) func (c *Container) Inject(toPtr interface{}) error { val := reflect.Indirect(valueOf(toPtr)) typ := val.Type() for _, d := range c.Dependencies { if d.Static && c.DependencyMatcher(d, typ) { v, err := d.Handle(nil, &Input{Type: typ}) if err != nil { if err == ErrSeeOther { continue } return err } val.Set(v) return nil } } return ErrMissingDependency }