├── .gitignore ├── LICENSE ├── README.md ├── circle.yml ├── com.go ├── config ├── api.go ├── config.go ├── config_test.go ├── doc.go └── viper │ ├── doc.go │ ├── viper.go │ └── viper_test.go ├── objects ├── objects.go └── objects_test.go └── plugins ├── doc.go ├── plugins.go └── plugins_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /local 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Glider Labs. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Glider Labs nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gliderlabs/com 2 | 3 | A component-oriented approach to building Golang applications 4 | 5 | [![GoDoc](https://godoc.org/github.com/gliderlabs/com?status.svg)](https://godoc.org/github.com/gliderlabs/com) 6 | [![CircleCI](https://img.shields.io/circleci/project/github/gliderlabs/com.svg)](https://circleci.com/gh/gliderlabs/com) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/gliderlabs/com)](https://goreportcard.com/report/github.com/gliderlabs/com) 8 | [![Slack](http://slack.gliderlabs.com/badge.svg)](http://slack.gliderlabs.com) 9 | [![Email Updates](https://img.shields.io/badge/updates-subscribe-yellow.svg)](https://app.convertkit.com/landing_pages/289455) 10 | 11 | ## Concept 12 | 13 | We want to see a world with great "building blocks" where you can quickly build 14 | whatever you want. Traditional "composability" is not enough, they need to 15 | integrate and hook into each other. 16 | 17 | This library provides the core mechanisms needed to build out a modular and 18 | extensible component architecture for your application, which also extend into 19 | an ecosystem of "drop-in" reusable components. 20 | 21 | There are two parts to this package that are designed to work with each other: 22 | 23 | * An object registry for interface-based extension points and dependency injection 24 | * A configuration API for settings, disabling objects, and picking interface backends 25 | 26 | ## Example application 27 | 28 | See the [example wiki app repo](https://github.com/gl-prototypes/wiki). 29 | 30 | After building out [reusable components](https://github.com/gliderlabs/stdcom), 31 | a simple wiki with GitHub authentication could be put together in ~200 lines of 32 | Go as a single component. 33 | 34 | ## Using com 35 | 36 | For now, see [GoDocs](https://godoc.org/github.com/gliderlabs/com), the 37 | example application, and the components in [stdcom](https://github.com/gliderlabs/stdcom). 38 | 39 | ## Dependencies 40 | 41 | Good libraries should have minimal dependencies. Here are the ones com uses and 42 | for what: 43 | 44 | * github.com/spf13/afero (plugins, config tests) 45 | * github.com/spf13/viper (config, config/viper) 46 | 47 | ## License 48 | 49 | BSD 50 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | jobs: 3 | build: 4 | docker: 5 | - image: golang:1.9 6 | working_directory: /go/src/github.com/gliderlabs/com 7 | steps: 8 | - checkout 9 | - run: go get ./... 10 | - run: go test -v -race ./... 11 | -------------------------------------------------------------------------------- /com.go: -------------------------------------------------------------------------------- 1 | // Package com is a user-facing interface to the object registry. Since 2 | // the only part of the API you need to use is Register, the rest of the API 3 | // for interacting with an object registry is in its own subpackage. This is used 4 | // by other tooling built around com, for example the config subpackage. 5 | // 6 | // When you register an object, it will populate fields based on the com struct 7 | // tags used on them. The object will then also be used to populate fields 8 | // of other objects in the registry where the type or interface matches. 9 | // 10 | // type Component struct { 11 | // Log log.Logger `com:"singleton"` 12 | // Handlers []api.Handlers `com:"extpoint"` 13 | // DB api.Store `com:"config"` 14 | // } 15 | // 16 | // In the above example component, it has fields with all three possible struct 17 | // tags: 18 | // 19 | // Singleton will pick the first object in the registry that implements that 20 | // interface. You can also use pointers to concrete types, for example to other 21 | // component types. 22 | // 23 | // Extpoint is going to be a slice of all objects in the registry that implement 24 | // that interface. 25 | // 26 | // Config is not populated, but is allowed to be populated via the registry API. 27 | // If you're using the config package, it will do this for you and populate it 28 | // based on configuration. In this case, the key would be "DB" and the value 29 | // could be the name of any registered component that implements api.Store. 30 | package com 31 | 32 | import "github.com/gliderlabs/com/objects" 33 | 34 | var ( 35 | // DefaultRegistry is often used as the single top level registry for an app. 36 | DefaultRegistry = &objects.Registry{} 37 | ) 38 | 39 | // Register will add an object with optional name to the default registry. 40 | func Register(obj interface{}, name string) error { 41 | return DefaultRegistry.Register(objects.New(obj, name)) 42 | } 43 | -------------------------------------------------------------------------------- /config/api.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Settings is an interface representing a collection of key-values from a 4 | // configuration or subset of a configuration. 90% of the time you'll just 5 | // use Unmarshal into a struct, but sometimes you'll want to grab a specific 6 | // key. To encourage using structs, there are no typed getters. 7 | type Settings interface { 8 | // Get returns the value associated with the key as an empty interface. 9 | Get(key string) interface{} 10 | 11 | // IsSet checks to see if the key has been set in configuration. 12 | IsSet(key string) bool 13 | 14 | // Unmarshal unmarshals the config into a Struct. Make sure that the tags on the fields of the structure are properly set. 15 | Unmarshal(rawVal interface{}) error 16 | 17 | // UnmarshalKey takes a single key and unmarshals it into a Struct. 18 | // BUG: Currently with Viper, UnmarshalKey will ignore environment values. 19 | UnmarshalKey(key string, rawVal interface{}) error 20 | 21 | // SetDefault sets the default value for this key. 22 | SetDefault(key string, value interface{}) 23 | 24 | // Sub returns new Settings instance representing a sub tree of this instance. 25 | Sub(key string) Settings 26 | } 27 | 28 | // Provider is an interface for configuration providers, which includes the 29 | // the interface to loaded configuration from that provider. 30 | type Provider interface { 31 | // Load returns Settings for named configuration loaded from provided paths. 32 | // Leave the file extension off of name as supported format extensions will 33 | // automatically be added. 34 | Load(name string, paths []string) (Settings, error) 35 | 36 | // New returns an empty Settings instance. 37 | New() Settings 38 | 39 | Settings 40 | } 41 | 42 | // Initializer is an extension point interface with a hook allowing objects to 43 | // handle their configuration when configuration is loaded by the provider. 44 | type Initializer interface { 45 | // InitializeConfig is called on a registered object with Settings for that 46 | // object when configuration has been loaded. 47 | InitializeConfig(config Settings) error 48 | } 49 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/gliderlabs/com/objects" 9 | ) 10 | 11 | const ( 12 | envFormatter = "%s_CONFIG" 13 | disabledKey = "disabled" 14 | ) 15 | 16 | // Load uses a Provider to read in configuration from various files and the 17 | // environment for the objects in a particular Registry. It passes Settings for 18 | // each object to any object that implements the Initializer interface. Then it 19 | // will lookup any struct fields with `com:"config"` and use the settings for 20 | // that object to get the name of an object from the registry to assign to that 21 | // field. It also disables any objects in the Registry referenced in the top-level 22 | // config section called "disabled". 23 | func Load(registry *objects.Registry, provider Provider, name string, paths []string) error { 24 | // add extra paths from environment 25 | envConfig := os.Getenv(fmt.Sprintf(envFormatter, strings.ToUpper(name))) 26 | paths = append(paths, strings.Split(envConfig, ":")...) 27 | 28 | // tell provider to load config 29 | cfg, err := provider.Load(name, paths) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | // get all top level keys in config 35 | var keys map[string]interface{} 36 | if err := cfg.Unmarshal(&keys); err != nil { 37 | return err 38 | } 39 | 40 | // iterate over all objects in registry 41 | for _, obj := range registry.Objects() { 42 | s := provider.New() 43 | 44 | // check if any top level key matches object 45 | for key := range keys { 46 | o, _ := registry.Lookup(key) 47 | if o == obj { 48 | s = cfg.Sub(key) 49 | break 50 | } 51 | } 52 | 53 | // if object is config.Initializer, initialize it 54 | if init, ok := obj.Value.(Initializer); ok { 55 | if err := init.InitializeConfig(s); err != nil { 56 | return err 57 | } 58 | } 59 | 60 | // use config to lookup and set config fields 61 | for name, field := range obj.Fields { 62 | if field.Config && s.IsSet(name) { 63 | objName, ok := s.Get(name).(string) 64 | if !ok { 65 | continue 66 | } 67 | o, err := registry.Lookup(objName) 68 | if err != nil { 69 | return err 70 | } 71 | obj.Assign(name, o) 72 | } 73 | } 74 | } 75 | 76 | // disable any objects found under disabled key 77 | if cfg.IsSet(disabledKey) { 78 | var disabled map[string]bool 79 | cfg.UnmarshalKey(disabledKey, &disabled) 80 | for name, d := range disabled { 81 | o, err := registry.Lookup(name) 82 | if err != nil { 83 | continue 84 | } 85 | registry.SetEnabled(o.FQN(), !d) 86 | } 87 | } 88 | 89 | // reload registry 90 | return registry.Reload() 91 | } 92 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/gliderlabs/com/config" 10 | "github.com/gliderlabs/com/config/viper" 11 | "github.com/gliderlabs/com/objects" 12 | "github.com/spf13/afero" 13 | viperlib "github.com/spf13/viper" 14 | ) 15 | 16 | var ( 17 | initErr = errors.New("initerr is true") 18 | ) 19 | 20 | type TestComponent struct { 21 | Foo string 22 | } 23 | 24 | func (c *TestComponent) InitializeConfig(cfg config.Settings) error { 25 | if getBool(cfg.Get("initerr")) { 26 | return initErr 27 | } 28 | var keys map[string]interface{} 29 | cfg.Unmarshal(&keys) 30 | return cfg.Unmarshal(c) 31 | } 32 | 33 | func getBool(v interface{}) bool { 34 | vv, _ := v.(bool) 35 | return vv 36 | } 37 | 38 | func newTestProvider(t *testing.T, path, config string) config.Provider { 39 | fs := afero.NewMemMapFs() 40 | err := afero.WriteFile(fs, path, []byte(config), 0644) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | v := viperlib.New() 45 | v.SetFs(fs) 46 | return &viper.Provider{v} 47 | } 48 | 49 | func fatal(t *testing.T, err error) { 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | } 54 | 55 | func TestLoadToml(t *testing.T) { 56 | reg := &objects.Registry{} 57 | obj := &TestComponent{} 58 | reg.Register(&objects.Object{Value: obj}) 59 | provider := newTestProvider(t, "/etc/test.toml", ` 60 | [TestComponent] 61 | foo = "foobar" 62 | `) 63 | err := config.Load(reg, provider, "test", []string{"/etc"}) 64 | fatal(t, err) 65 | if obj.Foo != "foobar" { 66 | t.Fatalf("got %#v; want %#v", obj.Foo, "foobar") 67 | } 68 | } 69 | 70 | func TestLoadYaml(t *testing.T) { 71 | reg := &objects.Registry{} 72 | obj := &TestComponent{} 73 | reg.Register(&objects.Object{Value: obj}) 74 | provider := newTestProvider(t, "/etc/test.yaml", ` 75 | TestComponent: 76 | foo: "foobar" 77 | `) 78 | err := config.Load(reg, provider, "test", []string{"/etc"}) 79 | fatal(t, err) 80 | if obj.Foo != "foobar" { 81 | t.Fatalf("got %#v; want %#v", obj.Foo, "foobar") 82 | } 83 | } 84 | 85 | func TestLoadJson(t *testing.T) { 86 | reg := &objects.Registry{} 87 | obj := &TestComponent{} 88 | reg.Register(&objects.Object{Value: obj}) 89 | provider := newTestProvider(t, "/etc/test.json", ` 90 | {"TestComponent": 91 | {"foo": "foobar"} 92 | }`) 93 | err := config.Load(reg, provider, "test", []string{"/etc"}) 94 | fatal(t, err) 95 | if obj.Foo != "foobar" { 96 | t.Fatalf("got %#v; want %#v", obj.Foo, "foobar") 97 | } 98 | } 99 | 100 | func TestLoadFromWorkingDir(t *testing.T) { 101 | reg := &objects.Registry{} 102 | wd, err := os.Getwd() 103 | fatal(t, err) 104 | // virtual FS will use real cwd since no way fake cwd resolution later 105 | provider := newTestProvider(t, fmt.Sprintf("%s/test.toml", wd), ` 106 | [TestComponent] 107 | foo = "foobar" 108 | `) 109 | err = config.Load(reg, provider, "test", []string{"."}) 110 | fatal(t, err) 111 | } 112 | 113 | func TestLoadInvalidFile(t *testing.T) { 114 | reg := &objects.Registry{} 115 | provider := newTestProvider(t, "/etc/test.toml", ` 116 | #!/usr/bin/python 117 | print "Hello world" 118 | `) 119 | err := config.Load(reg, provider, "test", []string{"/etc"}) 120 | if err == nil { 121 | t.Fatal("expected error") 122 | } 123 | } 124 | 125 | func TestInitializerError(t *testing.T) { 126 | reg := &objects.Registry{} 127 | obj := &TestComponent{} 128 | reg.Register(&objects.Object{Value: obj}) 129 | provider := newTestProvider(t, "/etc/test.toml", ` 130 | [TestComponent] 131 | initerr = true 132 | `) 133 | err := config.Load(reg, provider, "test", []string{"/etc"}) 134 | if err != initErr { 135 | t.Fatalf("got %#v; want %#v", err, initErr) 136 | } 137 | } 138 | 139 | type stringer struct { 140 | s string 141 | } 142 | 143 | func (s stringer) String() string { 144 | return s.s 145 | } 146 | 147 | func TestConfigField(t *testing.T) { 148 | var c struct { 149 | Stringer fmt.Stringer `com:"config"` 150 | } 151 | reg := &objects.Registry{} 152 | reg.Register(&objects.Object{Value: &c, Name: "Component"}) 153 | reg.Register(&objects.Object{Value: &stringer{"Foo"}, Name: "Fooer"}) 154 | provider := newTestProvider(t, "/etc/test.toml", ` 155 | [Component] 156 | Stringer = "Fooer" 157 | `) 158 | err := config.Load(reg, provider, "test", []string{"/etc"}) 159 | fatal(t, err) 160 | if got := c.Stringer.String(); got != "Foo" { 161 | t.Fatalf("got %#v; want %#v", got, "Foo") 162 | } 163 | } 164 | 165 | func TestConfigFieldNoObject(t *testing.T) { 166 | var c struct { 167 | Stringer fmt.Stringer `com:"config"` 168 | } 169 | reg := &objects.Registry{} 170 | reg.Register(&objects.Object{Value: &c, Name: "Component"}) 171 | provider := newTestProvider(t, "/etc/test.toml", ` 172 | [Component] 173 | Stringer = "Fooer" 174 | `) 175 | err := config.Load(reg, provider, "test", []string{"/etc"}) 176 | if err == nil { 177 | t.Fatal("expected error") 178 | } 179 | } 180 | 181 | func TestDisabled(t *testing.T) { 182 | reg := &objects.Registry{} 183 | obj := &objects.Object{Value: &TestComponent{}} 184 | reg.Register(obj) 185 | provider := newTestProvider(t, "/etc/test.toml", ` 186 | [TestComponent] 187 | foo = "foobar" 188 | 189 | [disabled] 190 | TestComponent = true 191 | IgnoreMe = true 192 | `) 193 | err := config.Load(reg, provider, "test", []string{"/etc"}) 194 | fatal(t, err) 195 | if got := obj.Enabled; got != false { 196 | t.Fatalf("got %#v; want %#v", got, false) 197 | } 198 | } 199 | 200 | func TestEnvOverride(t *testing.T) { 201 | reg := &objects.Registry{} 202 | obj := &TestComponent{} 203 | reg.Register(&objects.Object{Value: obj}) 204 | provider := newTestProvider(t, "/etc/test.toml", ` 205 | [TestComponent] 206 | foo = "foobar" 207 | `) 208 | os.Setenv("TESTCOMPONENT_FOO", "bazqux") 209 | err := config.Load(reg, provider, "test", []string{"/etc"}) 210 | fatal(t, err) 211 | var keys map[string]interface{} 212 | provider.Unmarshal(&keys) 213 | if obj.Foo != "bazqux" { 214 | t.Fatalf("got %#v; want %#v", obj.Foo, "bazqux") 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /config/doc.go: -------------------------------------------------------------------------------- 1 | // Package config does 3 things: 2 | // 1. provide an interface for component objects to get and process their own config 3 | // 2. provide an interface for config providers to hook into the load mechanism 4 | // 3. provide a load mechanism with sensible defaults and config lifecycle 5 | // 6 | // The load mechanism defines the basic semantics and lifecycle of configuration: 7 | // 8 | // 1. configuration is loaded from one or more files via Load 9 | // 2. the config format(s) are up to the config provider 10 | // 3. top level keys map to registered object names matched via Lookup 11 | // 4. a special "disabled" top level key is used to disable registered objects 12 | // 5. files are loaded from paths that the app specifies in call to Load 13 | // 6. more filepaths can be specified via user environment variable 14 | // 7. config can be set or overridden by user environment variables 15 | // 8. resulting config for each object is passed via extension point 16 | // 9. objects use this to specify defaults, process, and store values 17 | // 10. "config" fields of an object are assigned by lookup using the key by that field name 18 | // 11. registry is reloaded 19 | // 20 | // The default, preferred, and builtin configuration provider is Viper. Viper 21 | // can be used directly for more control, or replaced with a custom provider. 22 | // 23 | // The Settings and Initializer interfaces are the only parts needed for object 24 | // compatibility in the component ecosystem. Apps can define their own config 25 | // Provider, or ignore the Load mechanism entirely. 26 | package config 27 | -------------------------------------------------------------------------------- /config/viper/doc.go: -------------------------------------------------------------------------------- 1 | // Package viper provides a config.Provider based on the Viper configuration 2 | // library. Since the config.Provider interface was borrowed from Viper, this is 3 | // a very light package implementation. 4 | // 5 | // The Load implementation not only loads config files from multiple paths, it 6 | // uses Viper's AutomaticEnv to load config from environment. It also uses 7 | // SetEnvKeyReplacer to use underscores in place of periods when identifying 8 | // sub keys via environment. 9 | package viper 10 | -------------------------------------------------------------------------------- /config/viper/viper.go: -------------------------------------------------------------------------------- 1 | package viper 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gliderlabs/com/config" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | // New returns an initialized Viper provider instance. 11 | func New() config.Provider { 12 | v := viper.New() 13 | return &Provider{v} 14 | } 15 | 16 | // Provider is a config.Provider for Viper. 17 | type Provider struct { 18 | *viper.Viper 19 | } 20 | 21 | // Sub returns new Settings instance representing a sub tree of this instance. 22 | func (p *Provider) Sub(key string) config.Settings { 23 | sub := p.Viper.Sub(key) 24 | // Sub somehow removes values set/overridden by environment, so we do this 25 | // just to make sure they are set properly in this new Viper instance. 26 | // UnmarshalKey could be used here except it has the same problem as Sub. 27 | var keys map[string]map[string]interface{} 28 | p.Unmarshal(&keys) 29 | for k, v := range keys[key] { 30 | sub.Set(k, v) 31 | } 32 | return &Provider{sub} 33 | } 34 | 35 | // New returns an empty Settings instance. 36 | func (p *Provider) New() config.Settings { 37 | return New() 38 | } 39 | 40 | // Load returns Settings for named configuration loaded from provided paths. 41 | // Leave the file extension off of name as supported format extensions will 42 | // automatically be added by Viper. 43 | func (p *Provider) Load(name string, paths []string) (config.Settings, error) { 44 | // read in config files 45 | if len(paths) > 0 && name != "" { 46 | p.SetConfigName(name) 47 | for _, path := range paths { 48 | p.AddConfigPath(path) 49 | } 50 | if err := p.ReadInConfig(); err != nil { 51 | switch err.(type) { 52 | case viper.ConfigFileNotFoundError: 53 | // it's fine! 54 | break 55 | default: 56 | return nil, err 57 | } 58 | } 59 | } 60 | 61 | // read config from environment 62 | p.AutomaticEnv() 63 | p.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 64 | 65 | return p, nil 66 | } 67 | -------------------------------------------------------------------------------- /config/viper/viper_test.go: -------------------------------------------------------------------------------- 1 | package viper 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/spf13/afero" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func newTestProvider(t *testing.T, path, config string) *Provider { 11 | fs := afero.NewMemMapFs() 12 | err := afero.WriteFile(fs, path, []byte(config), 0644) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | v := viper.New() 17 | v.SetFs(fs) 18 | return &Provider{v} 19 | } 20 | 21 | func getString(v interface{}) string { 22 | vv, _ := v.(string) 23 | return vv 24 | } 25 | 26 | func fatal(t *testing.T, err error) { 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | } 31 | 32 | func TestLoadToml(t *testing.T) { 33 | provider := newTestProvider(t, "/etc/test.toml", ` 34 | [Test] 35 | foo = "foobar" 36 | `) 37 | settings, err := provider.Load("test", []string{"/etc"}) 38 | fatal(t, err) 39 | if got := getString(settings.Get("Test.foo")); got != "foobar" { 40 | t.Fatalf("got %#v; want %#v", got, "foobar") 41 | } 42 | } 43 | 44 | func TestLoadYaml(t *testing.T) { 45 | provider := newTestProvider(t, "/etc/test.yaml", ` 46 | Test: 47 | foo: "foobar" 48 | `) 49 | settings, err := provider.Load("test", []string{"/etc"}) 50 | fatal(t, err) 51 | if got := getString(settings.Get("Test.foo")); got != "foobar" { 52 | t.Fatalf("got %#v; want %#v", got, "foobar") 53 | } 54 | } 55 | 56 | func TestLoadJson(t *testing.T) { 57 | provider := newTestProvider(t, "/etc/test.json", ` 58 | {"Test": 59 | {"foo": "foobar"} 60 | }`) 61 | settings, err := provider.Load("test", []string{"/etc"}) 62 | fatal(t, err) 63 | if got := getString(settings.Get("Test.foo")); got != "foobar" { 64 | t.Fatalf("got %#v; want %#v", got, "foobar") 65 | } 66 | } 67 | 68 | // func TestLoadNotFound(t *testing.T) { 69 | // provider := newTestProvider(t, "/var/test.toml", ` 70 | // [Test] 71 | // foo = "foobar" 72 | // `) 73 | // _, err := provider.Load("test", []string{"/etc", "/tmp"}) 74 | // if err == nil { 75 | // t.Fatal("expected error") 76 | // } 77 | // } 78 | 79 | func TestNew(t *testing.T) { 80 | t.Parallel() 81 | provider := New() 82 | var m map[string]interface{} 83 | if err := provider.Unmarshal(&m); err != nil { 84 | t.Fatal(err) 85 | } 86 | if len(m) > 0 { 87 | t.Fatal("new config provider is not empty") 88 | } 89 | } 90 | 91 | func TestEmpty(t *testing.T) { 92 | t.Parallel() 93 | settings := New().New() 94 | var m map[string]interface{} 95 | if err := settings.Unmarshal(&m); err != nil { 96 | t.Fatal(err) 97 | } 98 | if len(m) > 0 { 99 | t.Fatal("empty config settings is not empty") 100 | } 101 | } 102 | 103 | func TestSub(t *testing.T) { 104 | t.Parallel() 105 | v := viper.New() 106 | v.Set("sub", map[string]interface{}{ 107 | "key": "value", 108 | }) 109 | provider := &Provider{v} 110 | sub := provider.Sub("sub") 111 | val := getString(sub.Get("key")) 112 | if val != "value" { 113 | t.Fatalf("expected 'value', got '%#v'", val) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /objects/objects.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | const ( 12 | TagSingleton = "singleton" 13 | TagExtpoint = "extpoint" 14 | TagConfig = "config" 15 | ) 16 | 17 | // Object represents an object and its metadata in a registry 18 | type Object struct { 19 | Value interface{} 20 | Name string 21 | Fields map[string]*Field 22 | Enabled bool 23 | PkgPath string 24 | 25 | reflectType reflect.Type 26 | reflectValue reflect.Value 27 | } 28 | 29 | // New creates a new Object by value and name 30 | func New(v interface{}, name string) *Object { 31 | return &Object{Value: v, Name: name} 32 | } 33 | 34 | // FQN returns a fully qualified name for a unique object in a registry 35 | func (o *Object) FQN() string { 36 | return strings.ToLower(fmt.Sprintf("%s#%s", o.PkgPath, o.Name)) 37 | } 38 | 39 | // Assign will set a named field of the object value if it has not already 40 | // been assigned. It will not assign to fields marked as extension points. 41 | // It will return true if the assignment is successful. 42 | func (o *Object) Assign(field string, obj *Object) bool { 43 | f, ok := o.Fields[field] 44 | if !ok { 45 | return false 46 | } 47 | // don't assign if already assigned 48 | if !isNilOrZero(f.reflectValue, f.reflectValue.Type()) { 49 | return false 50 | } 51 | // don't assign to extpoints because as slices they are handled differently 52 | if !f.Extpoint && obj.reflectType.AssignableTo(f.reflectValue.Type()) { 53 | f.reflectValue.Set(reflect.ValueOf(obj.Value)) 54 | return true 55 | } 56 | return false 57 | } 58 | 59 | // Field represents metadata of a field in an Object value's struct. 60 | type Field struct { 61 | Object *Object 62 | Name string 63 | Config bool 64 | Extpoint bool 65 | Tag string 66 | 67 | reflectValue reflect.Value 68 | } 69 | 70 | // Registry is a container for objects. 71 | type Registry struct { 72 | sync.Mutex 73 | objects []*Object 74 | disabled map[string]bool 75 | } 76 | 77 | // Register adds objects to the registry. 78 | func (r *Registry) Register(objects ...*Object) error { 79 | r.Lock() 80 | defer r.Unlock() 81 | r.initDisabled() 82 | for _, o := range objects { 83 | // set up type and value reflection 84 | o.reflectType = reflect.TypeOf(o.Value) 85 | o.reflectValue = reflect.ValueOf(o.Value) 86 | 87 | // if not a struct, ignore 88 | if !isStructPtr(o.reflectType) { 89 | continue 90 | } 91 | 92 | // collect tagged fields 93 | o.Fields = make(map[string]*Field) 94 | for i := 0; i < o.reflectValue.Elem().NumField(); i++ { 95 | field := o.reflectValue.Elem().Field(i) 96 | fieldName := o.reflectType.Elem().Field(i).Name 97 | fieldTag, ok := o.reflectType.Elem().Field(i).Tag.Lookup("com") 98 | if ok && field.CanSet() { 99 | o.Fields[fieldName] = &Field{ 100 | Name: fieldName, 101 | Config: fieldTag == TagConfig, 102 | Extpoint: fieldTag == TagExtpoint, 103 | Tag: fieldTag, 104 | reflectValue: field, 105 | } 106 | } 107 | } 108 | 109 | // set normalized package path. if the package is "com" we assume it 110 | // contains the component for its parent, so we strip it off. 111 | o.PkgPath = strings.TrimSuffix(o.reflectType.Elem().PkgPath(), "/com") 112 | 113 | // the default name is set by the name of the struct type 114 | if o.Name == "" { 115 | o.Name = o.reflectType.Elem().Name() 116 | } 117 | 118 | // error if the object has no package path 119 | if o.Name == "" && o.reflectType.Elem().PkgPath() == "" { 120 | return errors.New("unable to register object without name when it has no package path") 121 | } 122 | 123 | // enable unless already marked as disabled 124 | _, disabled := r.disabled[o.FQN()] 125 | o.Enabled = !disabled 126 | 127 | // append object to registry list of objects 128 | r.objects = append(r.objects, o) 129 | } 130 | 131 | // re-populate registered objects 132 | return r.reload() 133 | } 134 | 135 | // Lookup will attempt to find an object in the registry... 136 | // 1. if it matches the object FQN exactly 137 | // 2. if it matches a single object Name 138 | // 3. if it matches a single object by package path suffix 139 | func (r *Registry) Lookup(name string) (*Object, error) { 140 | // TODO: allow to choose to ignore disabled 141 | // TODO: match suffix for full FQN? (pkgpath+name) 142 | 143 | // all matching is done case insensitive 144 | name = strings.ToLower(name) 145 | var matches []*Object 146 | for _, obj := range r.Objects() { 147 | // first match any exact FQN 148 | if obj.FQN() == name { 149 | return obj, nil 150 | } 151 | // name matches added to slice 152 | if strings.ToLower(obj.Name) == name { 153 | matches = append(matches, obj) 154 | } 155 | } 156 | // if only one matched name, return 157 | if len(matches) == 1 { 158 | return matches[0], nil 159 | } 160 | // if more than one, error 161 | if len(matches) > 1 { 162 | return nil, errors.New("ambiguous name for lookup") 163 | } 164 | // now attempt suffix matches 165 | matches = matches[:0] 166 | for _, obj := range r.Objects() { 167 | if strings.HasSuffix(strings.ToLower(obj.PkgPath), name) { 168 | matches = append(matches, obj) 169 | } 170 | } 171 | if len(matches) == 1 { 172 | return matches[0], nil 173 | } 174 | if len(matches) > 1 { 175 | return nil, errors.New("ambiguous name for lookup") 176 | } 177 | return nil, errors.New("object not found") 178 | } 179 | 180 | // SetEnabled will set whether an object is enabled. 181 | func (r *Registry) SetEnabled(fqn string, enabled bool) { 182 | r.Lock() 183 | defer r.Unlock() 184 | r.initDisabled() 185 | r.disabled[fqn] = !enabled 186 | for _, o := range r.objects { 187 | if o.FQN() == fqn { 188 | o.Enabled = enabled 189 | break 190 | } 191 | } 192 | } 193 | 194 | func (r *Registry) initDisabled() { 195 | if r.disabled == nil { 196 | r.disabled = make(map[string]bool) 197 | } 198 | } 199 | 200 | // Enabled returns all enabled objects. 201 | func (r *Registry) Enabled() []*Object { 202 | r.Lock() 203 | defer r.Unlock() 204 | var objects []*Object 205 | for _, o := range r.objects { 206 | if o.Enabled { 207 | objects = append(objects, o) 208 | } 209 | } 210 | return objects 211 | } 212 | 213 | // Objects returns all registered objects. 214 | func (r *Registry) Objects() []*Object { 215 | r.Lock() 216 | defer r.Unlock() 217 | var objects []*Object 218 | for _, o := range r.objects { 219 | objects = append(objects, o) 220 | } 221 | return objects 222 | } 223 | 224 | func (r *Registry) ValueTo(rv reflect.Value) { 225 | for _, obj := range r.Objects() { 226 | robj := reflect.ValueOf(obj.Value) 227 | if rv.Elem().Type().Kind() == reflect.Struct { 228 | if robj.Elem().Type().AssignableTo(rv.Elem().Type()) { 229 | rv.Elem().Set(robj.Elem()) 230 | break 231 | } 232 | } else { 233 | if robj.Type().Implements(rv.Elem().Type()) { 234 | rv.Elem().Set(robj) 235 | break 236 | } 237 | } 238 | } 239 | 240 | } 241 | 242 | // Reload will go over all objects in the registry and attempt to populate 243 | // fields with com struct tags with other objects in the registry. 244 | func (r *Registry) Reload() error { 245 | r.Lock() 246 | defer r.Unlock() 247 | return r.reload() 248 | } 249 | 250 | func (r *Registry) reload() error { 251 | for _, o := range r.objects { 252 | if err := r.populateSingletons(o); err != nil { 253 | return err 254 | } 255 | if err := r.populateExtpoints(o); err != nil { 256 | return err 257 | } 258 | } 259 | return nil 260 | } 261 | 262 | func (r *Registry) populateSingletons(o *Object) error { 263 | for k, f := range o.Fields { 264 | if f.Config || f.Extpoint { 265 | continue 266 | } 267 | for _, existing := range r.objects { 268 | if existing.Enabled && existing.reflectType.AssignableTo(f.reflectValue.Type()) { 269 | o.Assign(k, existing) 270 | break 271 | } 272 | } 273 | } 274 | return nil 275 | } 276 | 277 | func (r *Registry) populateExtpoints(o *Object) error { 278 | for _, f := range o.Fields { 279 | if !f.Extpoint { 280 | continue 281 | } 282 | var objects []reflect.Value 283 | for _, existing := range r.objects { 284 | if existing.Enabled && existing.reflectType.AssignableTo(f.reflectValue.Type().Elem()) { 285 | objects = append(objects, existing.reflectValue) 286 | } 287 | } 288 | f.reflectValue.Set(reflect.MakeSlice(f.reflectValue.Type(), 0, len(objects))) 289 | for _, obj := range objects { 290 | f.reflectValue.Set(reflect.Append(f.reflectValue, obj)) 291 | } 292 | } 293 | return nil 294 | } 295 | 296 | func isStructPtr(t reflect.Type) bool { 297 | return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct 298 | } 299 | 300 | func isNilOrZero(v reflect.Value, t reflect.Type) bool { 301 | switch v.Kind() { 302 | default: 303 | return reflect.DeepEqual(v.Interface(), reflect.Zero(t).Interface()) 304 | case reflect.Interface, reflect.Ptr: 305 | return v.IsNil() 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /objects/objects_test.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type Stringer interface { 8 | String() string 9 | } 10 | 11 | type Foo struct { 12 | msg string 13 | } 14 | 15 | func (f *Foo) String() string { 16 | return f.msg 17 | } 18 | 19 | func TestRegister(t *testing.T) { 20 | r := &Registry{} 21 | v := &Foo{t.Name()} 22 | if err := r.Register(&Object{Value: v}); err != nil { 23 | t.Fatal(err) 24 | } 25 | found := false 26 | for _, obj := range r.Objects() { 27 | if obj.Value == v { 28 | found = true 29 | } 30 | } 31 | if !found { 32 | t.Fatal("registered object not found") 33 | } 34 | } 35 | 36 | func TestLookup(t *testing.T) { 37 | r := &Registry{} 38 | v := &Foo{t.Name()} 39 | if err := r.Register(&Object{Name: t.Name(), Value: v}); err != nil { 40 | t.Fatal(err) 41 | } 42 | obj, _ := r.Lookup(t.Name()) 43 | if obj == nil || obj.Value != v { 44 | t.Fatal("lookup return nil or wrong object") 45 | } 46 | } 47 | 48 | func TestStructSingleton(t *testing.T) { 49 | r := &Registry{} 50 | v1 := &Foo{t.Name()} 51 | var v2 struct { 52 | A *Foo `com:"singleton"` 53 | } 54 | if err := r.Register(&Object{Value: v1}, &Object{Value: &v2, Name: "v2"}); err != nil { 55 | t.Fatal(err) 56 | } 57 | if v2.A != v1 { 58 | t.Fatal("field not set to registered singleton after register") 59 | } 60 | } 61 | 62 | func TestInterfaceSingleton(t *testing.T) { 63 | r := &Registry{} 64 | v1 := &Foo{t.Name()} 65 | var v2 struct { 66 | A Stringer `com:"singleton"` 67 | } 68 | if err := r.Register(&Object{Value: v1}, &Object{Value: &v2, Name: "v2"}); err != nil { 69 | t.Fatal(err) 70 | } 71 | if v2.A != v1 { 72 | t.Fatal("field not set to registered singleton after register") 73 | } 74 | } 75 | 76 | func TestExtpoints(t *testing.T) { 77 | r := &Registry{} 78 | ext1 := &Foo{"ext1"} 79 | ext2 := &Foo{"ext2"} 80 | var v struct { 81 | A []Stringer `com:"extpoint"` 82 | } 83 | if err := r.Register(&Object{Value: &v, Name: "v"}, &Object{Value: ext1}, &Object{Value: ext2}); err != nil { 84 | t.Fatal(err) 85 | } 86 | if len(v.A) != 2 { 87 | t.Fatal("field not set to registered extensions after register") 88 | } 89 | } 90 | 91 | func TestSkipAssignedSingletons(t *testing.T) { 92 | r := &Registry{} 93 | v1 := &Foo{"registered"} 94 | var v2 struct { 95 | A *Foo `com:"singleton"` 96 | } 97 | v2.A = &Foo{"notregistered"} 98 | if err := r.Register(&Object{Value: v1}, &Object{Value: &v2, Name: "v2"}); err != nil { 99 | t.Fatal(err) 100 | } 101 | if v2.A.String() != "notregistered" { 102 | t.Fatal("field was assigned even though it was already set") 103 | } 104 | } 105 | 106 | func TestAssigningNonexistantFieldsNoop(t *testing.T) { 107 | var v struct { 108 | A *Foo `com:"singleton"` 109 | } 110 | obj := &Object{Value: &v} 111 | if obj.Assign("B", &Object{Value: &Foo{}}) != false { 112 | t.Fatal("assign allowed for non-existent field") 113 | } 114 | } 115 | 116 | func TestAssigningExtpointFieldsNoop(t *testing.T) { 117 | var v struct { 118 | A []Stringer `com:"extpoint"` 119 | } 120 | obj := &Object{Value: &v} 121 | if obj.Assign("A", &Object{Value: &Foo{}}) != false { 122 | t.Fatal("assign allowed for extpoint field") 123 | } 124 | } 125 | 126 | func TestConfigFieldLeftUnassigned(t *testing.T) { 127 | r := &Registry{} 128 | v1 := &Foo{t.Name()} 129 | var v2 struct { 130 | A Stringer `com:"config"` 131 | } 132 | if err := r.Register(&Object{Value: v1}, &Object{Value: &v2, Name: "v2"}); err != nil { 133 | t.Fatal(err) 134 | } 135 | if v2.A != nil { 136 | t.Fatal("config field not left unassigned") 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /plugins/doc.go: -------------------------------------------------------------------------------- 1 | // Package plugins allows dynamic Go plugins to provide objects to a registry. 2 | // 3 | // It is currently experimental. 4 | package plugins 5 | -------------------------------------------------------------------------------- /plugins/plugins.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "plugin" 7 | "strings" 8 | 9 | "github.com/gliderlabs/com/objects" 10 | "github.com/spf13/afero" 11 | ) 12 | 13 | const ( 14 | // TODO: should be public to self document 15 | envFormatter = "%s_PLUGINS" 16 | 17 | // TODO: should be public to self document 18 | fnSymbol = "Registerable" 19 | ) 20 | 21 | var ( 22 | // used for testing. TODO: component? 23 | pluginLoader = loadPlugin 24 | fs = afero.NewOsFs() 25 | ) 26 | 27 | // TODO: define and use a Registerable type to self document the function signature 28 | 29 | // Load will open Go shared object plugins, call the symbol Registerable, and 30 | // register objects returned. 31 | func Load(registry *objects.Registry, name string, paths []string) error { 32 | // get paths from environment 33 | envPaths := os.Getenv(fmt.Sprintf(envFormatter, strings.ToUpper(name))) 34 | paths = append(paths, strings.Split(envPaths, ":")...) 35 | 36 | // look for .so files in each path and try to load them 37 | for _, path := range paths { 38 | matches, err := afero.Glob(fs, fmt.Sprintf("%s/*.so", path)) 39 | if err != nil { 40 | continue 41 | } 42 | for _, filepath := range matches { 43 | if err := pluginLoader(registry, filepath); err != nil { 44 | return err 45 | } 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func loadPlugin(reg *objects.Registry, filepath string) error { 53 | // open the shared object binary 54 | p, err := plugin.Open(filepath) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | // lookup function that returns objects to register 60 | symbol, err := p.Lookup(fnSymbol) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | // call function and register returned objects 66 | for _, obj := range symbol.(func() []interface{})() { 67 | reg.Register(&objects.Object{Value: obj}) 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /plugins/plugins_test.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gliderlabs/com/objects" 7 | "github.com/spf13/afero" 8 | ) 9 | 10 | func mockLoadPlugin(reg *objects.Registry, filepath string) error { 11 | return nil 12 | } 13 | 14 | func setupMocks() { 15 | pluginLoader = mockLoadPlugin 16 | fs = afero.NewMemMapFs() 17 | } 18 | 19 | func reset() { 20 | pluginLoader = loadPlugin 21 | fs = afero.NewOsFs() 22 | } 23 | 24 | func TestLoadTODO(t *testing.T) { 25 | setupMocks() 26 | defer reset() 27 | reg := &objects.Registry{} 28 | err := Load(reg, "test", []string{}) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | } 33 | --------------------------------------------------------------------------------