diff --git a/Makefile b/Makefile index c6fb8eb..00af97d 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ GO_TEST_COVERAGE_MODE ?= count GO_TEST_COVERAGE_FILE_NAME ?= coverage.out # Set a default `min_confidence` value for `golint` -GO_LINT_MIN_CONFIDENCE ?= 0.2 +GO_LINT_MIN_CONFIDENCE ?= 0.8 all: test diff --git a/README.md b/README.md index 586e227..979dd8b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ components of an application are bundles that are glued together using a depende | Symbol | Value | Description | | ------------- | --------------- | ----------------------- | -| DefCliRoot | cli.cmd.root | Add root cli command | -| DefCliVersion | cli.cmd.version | Add version cli command | +| DefCliRoot | cli.cmd.root | Cli root command | +| DefCliVersion | cli.cmd.version | Cli version command | +| DefContext | context | Current context | | DefRegistry | registry | A key/value registry | diff --git a/glue.go b/glue.go index 3195beb..b03d2b4 100644 --- a/glue.go +++ b/glue.go @@ -1,12 +1,14 @@ -// Package glue provide basic functionality. package glue import ( + "context" + "errors" "fmt" "os" + "os/signal" "path/filepath" - "reflect" "sync" + "syscall" "github.com/sarulabs/di/v2" "github.com/spf13/cobra" @@ -36,30 +38,18 @@ type ( DependsOn() []string } - // Registry interface. - Registry interface { - Get(name string) interface{} - Set(name string, value interface{}) - Fill(name string, value interface{}) error - } - // app is implementation of App. app struct { - mux sync.Mutex - scopes []string - bundles map[string]Bundle - registry Registry - container *di.Builder + ctx context.Context + mux sync.Mutex + scopes []string + bundles map[string]Bundle + builder *di.Builder + registry Registry } // optionFunc wraps a func so it satisfies the Option interface. optionFunc func(kernel *app) error - - // registry is implementation of Registry. - registry struct { - mux sync.RWMutex - values map[string]interface{} - } ) const ( @@ -69,7 +59,11 @@ const ( // DefCliVersion is version command definition name. DefCliVersion = "cli.cmd.version" + // DefContext is global context definition name. + DefContext = "context" + // DefRegistry is registry definition name. + // Deprecated: use Context instead of Registry. Will be removed in 3.0. DefRegistry = "registry" // TagCliCommand is tag to mark exported cli commands. @@ -79,15 +73,30 @@ const ( TagRootPersistentFlags = "cli.persistent_flags" ) +// ErrNilContext is error triggered when detected nil context in option value. +var ErrNilContext = errors.New("nil context") + +// Context option. +func Context(ctx context.Context) Option { + return optionFunc(func(a *app) error { + if ctx == nil { + return ErrNilContext + } + + a.ctx = ctx + return nil + }) +} + // Bundles option. func Bundles(bundles ...Bundle) Option { - return optionFunc(func(k *app) error { + return optionFunc(func(a *app) error { for _, bundle := range bundles { - if _, ok := k.bundles[bundle.Name()]; ok { + if _, ok := a.bundles[bundle.Name()]; ok { return fmt.Errorf(`trying to register two bundles with the same name "%s"`, bundle.Name()) } - k.bundles[bundle.Name()] = bundle + a.bundles[bundle.Name()] = bundle } return nil @@ -96,61 +105,57 @@ func Bundles(bundles ...Bundle) Option { // Scopes option. func Scopes(scopes ...string) Option { - return optionFunc(func(k *app) error { - k.scopes = scopes + return optionFunc(func(a *app) error { + a.scopes = scopes return nil }) } // Version option. func Version(version string) Option { - return optionFunc(func(k *app) error { - k.registry.Set("app.version", version) + return optionFunc(func(a *app) error { + a.withValue("app.version", version) return nil }) } // NewApp is app constructor. func NewApp(options ...Option) (_ App, err error) { - var ( - p = registry{ - values: make(map[string]interface{}), - } - k = app{ - bundles: make(map[string]Bundle, 8), - registry: &p, - } - ) + var a = app{ + ctx: context.Background(), + bundles: make(map[string]Bundle, 8), + registry: newRegistry(), + } // apply options for _, option := range options { - if err = option.apply(&k); err != nil { + if err = option.apply(&a); err != nil { return nil, err } } - // create di container - if k.container, err = di.NewBuilder(k.scopes...); err != nil { + // create di builder + if a.builder, err = di.NewBuilder(a.scopes...); err != nil { return nil, err } - // initialize container - if err = k.initContainer(); err != nil { + // initialize builder + if err = a.initBuilder(); err != nil { return nil, err } // register bundles - if err = k.registerBundles(); err != nil { + if err = a.registerBundles(); err != nil { return nil, err } - return &k, nil + return &a, nil } // Execute implementation. -func (k *app) Execute() (err error) { - k.mux.Lock() - defer k.mux.Unlock() +func (a *app) Execute() (err error) { + a.mux.Lock() + defer a.mux.Unlock() // app.path var appPath string @@ -158,33 +163,49 @@ func (k *app) Execute() (err error) { return err } - k.registry.Set("app.path", appPath) + a.registry.Set("app.path", appPath) + + // modify context + var cancelFunc = a.withCancel() + a.withValue("app.path", appPath) // build container - var context = k.container.Build() + var container = a.builder.Build() defer func() { + cancelFunc() + if err != nil { - _ = context.Delete() + _ = container.Delete() return } - err = context.Delete() + err = container.Delete() + }() + + // wait signal, cancel execution context + var sigChan = make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + go func() { + select { + case <-sigChan: + cancelFunc() + } }() // resolve cli root var root *cobra.Command - if err = context.Fill(DefCliRoot, &root); err != nil { + if err = container.Fill(DefCliRoot, &root); err != nil { return err } - // run cli root + // TODO: Pass context, when PR will merged. See: https://github.com/spf13/cobra/pull/893 err = root.Execute() - return err + return } -// initContainer initialize di container -func (k *app) initContainer() error { - return k.container.Add( +// initBuilder initialize di builder +func (a *app) initBuilder() error { + return a.builder.Add( di.Def{ Name: DefCliRoot, Build: func(ctn di.Container) (_ interface{}, err error) { @@ -200,6 +221,8 @@ func (k *app) initContainer() error { PersistentPreRun: func(cmd *cobra.Command, args []string) { registry.Set("cli.cmd", cmd) registry.Set("cli.args", args) + a.withValue("cli.cmd", cmd) + a.withValue("cli.args", args) }, } @@ -221,6 +244,7 @@ func (k *app) initContainer() error { if err = ctn.Fill(name, &pf); err != nil { return nil, err } + rootCmd.PersistentFlags().AddFlagSet(pf) break Tags } @@ -236,8 +260,8 @@ func (k *app) initContainer() error { Name: TagCliCommand, }}, Build: func(ctn di.Container) (_ interface{}, err error) { - var p Registry - if err = ctn.Fill(DefRegistry, &p); err != nil { + var ctx context.Context + if err = ctn.Fill(DefContext, &ctx); err != nil { return nil, err } @@ -247,39 +271,46 @@ func (k *app) initContainer() error { SilenceUsage: true, SilenceErrors: true, Run: func(cmd *cobra.Command, args []string) { - fmt.Println( - p.Get("app.version"), - ) + if v, ok := ctx.Value("app.version").(string); ok { + fmt.Println(v) + } }, }, nil }, }, + di.Def{ + Name: DefContext, + Build: func(_ di.Container) (interface{}, error) { + return a.ctx, nil + }, + Unshared: true, + }, di.Def{ Name: DefRegistry, Build: func(_ di.Container) (interface{}, error) { - return k.registry, nil + return a.registry, nil }, }, ) } // registerBundles resolve bundles dependencies and register them. -func (k *app) registerBundles() (err error) { +func (a *app) registerBundles() (err error) { // resolve dependencies var ( - resolved = make([]string, 0, len(k.bundles)) - unresolved = make([]string, 0, len(k.bundles)) + resolved = make([]string, 0, len(a.bundles)) + unresolved = make([]string, 0, len(a.bundles)) ) - for _, bundle := range k.bundles { - if err = k.resolveDependencies(bundle, &resolved, &unresolved); err != nil { + for _, bundle := range a.bundles { + if err = a.resolveDependencies(bundle, &resolved, &unresolved); err != nil { return err } } // register for _, name := range resolved { - if err = k.bundles[name].Build(k.container); err != nil { + if err = a.bundles[name].Build(a.builder); err != nil { return err } } @@ -288,12 +319,13 @@ func (k *app) registerBundles() (err error) { } // dependencies generate dependencies graph. -func (k *app) resolveDependencies(bundle Bundle, resolved *[]string, unresolved *[]string) (err error) { +func (a *app) resolveDependencies(bundle Bundle, resolved *[]string, unresolved *[]string) (err error) { for _, name := range *unresolved { if bundle.Name() == name { return fmt.Errorf(`"%s" has circular dependency of itself`, bundle.Name()) } } + *unresolved = append(*unresolved, bundle.Name()) if v, ok := bundle.(BundleDependsOn); ok { @@ -302,11 +334,11 @@ func (k *app) resolveDependencies(bundle Bundle, resolved *[]string, unresolved return fmt.Errorf(`"%s" can not depends on itself`, v.Name()) } - if _, ok := k.bundles[name]; !ok { + if _, ok := a.bundles[name]; !ok { return fmt.Errorf(`"%s" has unresoled dependency "%s"`, v.Name(), name) } - if err = k.resolveDependencies(k.bundles[name], resolved, unresolved); err != nil { + if err = a.resolveDependencies(a.bundles[name], resolved, unresolved); err != nil { return err } } @@ -319,44 +351,25 @@ func (k *app) resolveDependencies(bundle Bundle, resolved *[]string, unresolved return nil } } + *resolved = append(*resolved, bundle.Name()) return nil } -// apply implements Option. -func (f optionFunc) apply(kernel *app) error { - return f(kernel) +// withCancel append cancellation to current context. Method is non thread safe. +func (a *app) withCancel() (fn context.CancelFunc) { + a.ctx, fn = context.WithCancel(a.ctx) + return fn } -// Get implements Registry. -func (p *registry) Get(name string) interface{} { - p.mux.RLock() - defer p.mux.RUnlock() - - return p.values[name] +// withValue append any value to current context. Method is non thread safe. +func (a *app) withValue(key, value interface{}) context.Context { + a.ctx = context.WithValue(a.ctx, key, value) + return a.ctx } -// Set implements Registry. -func (p *registry) Set(name string, value interface{}) { - p.mux.Lock() - defer p.mux.Unlock() - - p.values[name] = value -} - -// Fill implements Registry. -func (p *registry) Fill(name string, target interface{}) (err error) { - var src interface{} - defer func() { - if r := recover(); r != nil { - t := reflect.TypeOf(target) - s := reflect.TypeOf(src) - err = fmt.Errorf("target is `%s` but should be a pointer to the source type `%s`", t, s) - } - }() - - src = p.Get(name) - reflect.ValueOf(target).Elem().Set(reflect.ValueOf(src)) - return +// apply implements Option. +func (f optionFunc) apply(kernel *app) error { + return f(kernel) } diff --git a/glue_test.go b/glue_test.go index 0f86506..53d922e 100644 --- a/glue_test.go +++ b/glue_test.go @@ -2,6 +2,7 @@ package glue_test import ( "bytes" + "context" "errors" "io" "os" @@ -116,6 +117,22 @@ func TestBundles(t *testing.T) { }) } +func TestContext(t *testing.T) { + t.Run("PositiveCase1", func(t *testing.T) { + var _, err = glue.NewApp( + glue.Context(context.Background()), + ) + assert.Nil(t, err) + }) + + t.Run("NegativeCase1", func(t *testing.T) { + var _, err = glue.NewApp( + glue.Context(nil), + ) + assert.Error(t, err) + }) +} + func TestScopes(t *testing.T) { t.Run("PositiveCase1", func(t *testing.T) { var _, err = glue.NewApp( diff --git a/registry.go b/registry.go new file mode 100644 index 0000000..d9210e2 --- /dev/null +++ b/registry.go @@ -0,0 +1,62 @@ +package glue + +import ( + "fmt" + "reflect" + "sync" +) + +type ( + // Registry interface. + // Deprecated: use Context instead of Registry. Will be removed in 3.0. + Registry interface { + Get(name string) interface{} + Set(name string, value interface{}) + Fill(name string, value interface{}) error + } + + // registry is implementation of Registry. + registry struct { + mux sync.RWMutex + values map[string]interface{} + } +) + +// newRegistry is Registry constructor. +func newRegistry() Registry { + return ®istry{ + values: make(map[string]interface{}), + } +} + +// Get implements Registry. +func (p *registry) Get(name string) interface{} { + p.mux.RLock() + defer p.mux.RUnlock() + + return p.values[name] +} + +// Set implements Registry. +func (p *registry) Set(name string, value interface{}) { + p.mux.Lock() + defer p.mux.Unlock() + + p.values[name] = value +} + +// Fill implements Registry. +func (p *registry) Fill(name string, target interface{}) (err error) { + var src interface{} + defer func() { + if r := recover(); r != nil { + t := reflect.TypeOf(target) + s := reflect.TypeOf(src) + err = fmt.Errorf("target is `%s` but should be a pointer to the source type `%s`", t, s) + } + }() + + src = p.Get(name) + reflect.ValueOf(target).Elem().Set(reflect.ValueOf(src)) + return +}