diff --git a/.gitignore b/.gitignore index 9c0f1110..0da3ff62 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ core.iml .idea/* /.idea/ .env +config/testdata/module_test_partial.json diff --git a/config/config.go b/config/config.go index 4aaf3fcb..90e11029 100644 --- a/config/config.go +++ b/config/config.go @@ -16,6 +16,7 @@ import ( // KoanfAdapter is a implementation of contract.Config based on Koanf (https://github.com/knadh/koanf). type KoanfAdapter struct { layers []ProviderSet + validators []Validator watcher contract.ConfigWatcher dispatcher contract.Dispatcher delimiter string @@ -62,6 +63,13 @@ func WithDispatcher(dispatcher contract.Dispatcher) Option { } } +// WithValidators changes the validators of Koanf. +func WithValidators(validators ...Validator) Option { + return func(option *KoanfAdapter) { + option.validators = validators + } +} + // NewConfig creates a new *KoanfAdapter. func NewConfig(options ...Option) (*KoanfAdapter, error) { adapter := KoanfAdapter{delimiter: "."} @@ -83,19 +91,29 @@ func NewConfig(options ...Option) (*KoanfAdapter, error) { // an error occurred, Reload will return early and abort the rest of the // reloading. func (k *KoanfAdapter) Reload() error { - if k.dispatcher != nil { - defer k.dispatcher.Dispatch(context.Background(), events.Of(events.OnReload{NewConf: k})) - } - - k.rwlock.Lock() - defer k.rwlock.Unlock() + var tmp = koanf.New(".") for i := len(k.layers) - 1; i >= 0; i-- { - err := k.K.Load(k.layers[i].Provider, k.layers[i].Parser) + err := tmp.Load(k.layers[i].Provider, k.layers[i].Parser) if err != nil { return fmt.Errorf("unable to load config %w", err) } } + + for _, f := range k.validators { + if err := f(tmp.Raw()); err != nil { + return fmt.Errorf("validation failed: %w", err) + } + } + + k.rwlock.Lock() + k.K = tmp + k.rwlock.Unlock() + + if k.dispatcher != nil { + k.dispatcher.Dispatch(context.Background(), events.Of(events.OnReload{NewConf: k})) + } + return nil } diff --git a/config/config_test.go b/config/config_test.go index 600d723d..e18b3e5f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,6 +2,7 @@ package config import ( "context" + "errors" "fmt" "io/ioutil" "os" @@ -219,6 +220,17 @@ func TestMapAdapter_Unmarshal(t *gotesting.T) { }, target) } +func TestKoanfAdapter_Reload(t *gotesting.T) { + t.Parallel() + conf, err := NewConfig( + WithValidators(func(data map[string]interface{}) error { + return errors.New("bad config") + }), + ) + assert.Error(t, err) + assert.Nil(t, conf) +} + func prepareJSONTestSubject(t *gotesting.T) *KoanfAdapter { k := koanf.New(".") if err := k.Load(file.Provider("testdata/mock.json"), json.Parser()); err != nil { diff --git a/config/exported_config.go b/config/exported_config.go index 153e34a2..1e9ddd96 100644 --- a/config/exported_config.go +++ b/config/exported_config.go @@ -3,7 +3,12 @@ package config // ExportedConfig is a struct that outlines a set of configuration. // Each module is supposed to emit ExportedConfig into DI, and Package config should collect them. type ExportedConfig struct { - Owner string - Data map[string]interface{} - Comment string + Owner string + Data map[string]interface{} + Comment string + Validate Validator } + +// Validator is a method to verify if config is valid. If it is not valid, the +// returned error should contain a human readable description of why. +type Validator func(data map[string]interface{}) error diff --git a/config/integration_test.go b/config/integration_test.go new file mode 100644 index 00000000..39f0b7d3 --- /dev/null +++ b/config/integration_test.go @@ -0,0 +1,40 @@ +package config_test + +import ( + "errors" + "testing" + + "github.com/DoNewsCode/core" + "github.com/DoNewsCode/core/config" + "github.com/DoNewsCode/core/di" +) + +func provideConfig() configOut { + return configOut{ + Config: []config.ExportedConfig{{ + Validate: func(data map[string]interface{}) error { + return errors.New("bad config") + }, + }}, + } +} + +type configOut struct { + di.Out + + Config []config.ExportedConfig `group:"config,flatten"` +} + +func Test_integration(t *testing.T) { + defer func() { + if r := recover(); r != nil { + return + } + t.Errorf("test should panic. the config is not valid.") + }() + c := core.Default() + c.Provide(di.Deps{ + provideConfig, + }) + c.AddModuleFunc(config.New) +} diff --git a/config/module.go b/config/module.go index 4770f635..ac07f10b 100644 --- a/config/module.go +++ b/config/module.go @@ -44,6 +44,11 @@ func New(p ConfigIn) (Module, error) { if adapter, ok = p.Conf.(*KoanfAdapter); !ok { return Module{}, fmt.Errorf("expects a *config.KoanfAdapter instance, but %T given", p.Conf) } + + if err := loadValidators(adapter, p.ExportedConfigs); err != nil { + return Module{}, err + } + return Module{ dispatcher: p.Dispatcher, conf: adapter, @@ -65,8 +70,8 @@ func (m Module) ProvideRunGroup(group *run.Group) { // ProvideCommand provides the config related command. func (m Module) ProvideCommand(command *cobra.Command) { var ( - outputFile string - style string + targetFilePath string + style string ) initCmd := &cobra.Command{ Use: "init [module]", @@ -99,8 +104,8 @@ func (m Module) ProvideCommand(command *cobra.Command) { } exportedConfigs = copy } - os.MkdirAll(filepath.Dir(outputFile), os.ModePerm) - targetFile, err = os.OpenFile(outputFile, + os.MkdirAll(filepath.Dir(targetFilePath), os.ModePerm) + targetFile, err = os.OpenFile(targetFilePath, handler.flags(), os.ModePerm) if err != nil { return errors.Wrap(err, "failed to open config file") @@ -121,29 +126,111 @@ func (m Module) ProvideCommand(command *cobra.Command) { return nil }, } - initCmd.Flags().StringVarP( - &outputFile, + + verifyCmd := &cobra.Command{ + Use: "verify [module]", + Short: "verify the config file is correct.", + Long: "verify the config file is correct based on the methods exported by modules.", + RunE: func(cmd *cobra.Command, args []string) error { + var ( + handler handler + targetFile *os.File + exportedConfigs []ExportedConfig + confMap map[string]interface{} + err error + ) + handler, err = getHandler(style) + if err != nil { + return err + } + if len(args) == 0 { + exportedConfigs = m.exportedConfigs + } + if len(args) >= 1 { + var copy = make([]ExportedConfig, 0) + for i := range m.exportedConfigs { + for j := 0; j < len(args); j++ { + if args[j] == m.exportedConfigs[i].Owner { + copy = append(copy, m.exportedConfigs[i]) + break + } + } + } + exportedConfigs = copy + } + os.MkdirAll(filepath.Dir(targetFilePath), os.ModePerm) + targetFile, err = os.OpenFile(targetFilePath, + handler.flags(), os.ModePerm) + if err != nil { + return errors.Wrap(err, "failed to open config file") + } + defer targetFile.Close() + bytes, err := ioutil.ReadAll(targetFile) + if err != nil { + return errors.Wrap(err, "failed to read config file") + } + err = handler.unmarshal(bytes, &confMap) + if err != nil { + return errors.Wrap(err, "failed to unmarshal config file") + } + for _, config := range exportedConfigs { + if config.Validate == nil { + continue + } + if err := config.Validate(confMap); err != nil { + return errors.Wrap(err, "invalid config") + } + } + return nil + }, + } + + configCmd := &cobra.Command{ + Use: "config", + Short: "manage configuration", + Long: "manage configuration, such as export a copy of default config.", + } + configCmd.PersistentFlags().StringVarP( + &targetFilePath, "outputFile", "o", "./config/config.yaml", - "The output file of exported config", + "The output file of exported config (alias of targetFile)", ) - initCmd.Flags().StringVarP( + configCmd.PersistentFlags().StringVarP( + &targetFilePath, + "targetFile", + "t", + "./config/config.yaml", + "The targeted config file", + ) + configCmd.PersistentFlags().StringVarP( &style, "style", "s", "yaml", "The output file style", ) - configCmd := &cobra.Command{ - Use: "config", - Short: "manage configuration", - Long: "manage configuration, such as export a copy of default config.", - } configCmd.AddCommand(initCmd) + configCmd.AddCommand(verifyCmd) command.AddCommand(configCmd) } +func loadValidators(k *KoanfAdapter, exportedConfigs []ExportedConfig) error { + for _, config := range exportedConfigs { + if config.Validate == nil { + continue + } + k.validators = append(k.validators, config.Validate) + } + for _, f := range k.validators { + if err := f(k.K.Raw()); err != nil { + return fmt.Errorf("invalid config: %w", err) + } + } + return nil +} + func getHandler(style string) (handler, error) { switch style { case "json": @@ -221,10 +308,10 @@ func (y jsonHandler) write(file *os.File, configs []ExportedConfig, confMap map[ confMap = make(map[string]interface{}) } for _, exportedConfig := range configs { - if _, ok := confMap[exportedConfig.Owner]; ok { - continue - } for k := range exportedConfig.Data { + if _, ok := confMap[k]; ok { + continue + } confMap[k] = exportedConfig.Data[k] } } diff --git a/config/module_test.go b/config/module_test.go index 56fd3452..dac3d407 100644 --- a/config/module_test.go +++ b/config/module_test.go @@ -12,6 +12,7 @@ import ( "github.com/DoNewsCode/core/events" "github.com/knadh/koanf/providers/confmap" "github.com/oklog/run" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) @@ -20,22 +21,33 @@ func setup() *cobra.Command { os.Remove("./testdata/module_test.yaml") os.Remove("./testdata/module_test.json") var config, _ = NewConfig() - var mod = Module{conf: config, exportedConfigs: []ExportedConfig{ - { - "foo", - map[string]interface{}{ - "foo": "bar", + var mod = Module{ + conf: config, + exportedConfigs: []ExportedConfig{ + { + "foo", + map[string]interface{}{ + "foo": "bar", + }, + "A mock config", + func(data map[string]interface{}) error { + if _, ok := data["foo"]; !ok { + return errors.New("bad config") + } + return nil + }, }, - "A mock config", - }, - { - "baz", - map[string]interface{}{ - "baz": "qux", + { + "baz", + map[string]interface{}{ + "baz": "qux", + }, + "Other mock config", + nil, }, - "Other mock config", }, - }} + dispatcher: nil, + } rootCmd := &cobra.Command{ Use: "root", } @@ -43,7 +55,7 @@ func setup() *cobra.Command { return rootCmd } -func TestModule_ProvideCommand(t *testing.T) { +func TestModule_ProvideCommand_initCmd(t *testing.T) { rootCmd := setup() cases := []struct { name string @@ -87,6 +99,12 @@ func TestModule_ProvideCommand(t *testing.T) { []string{"config", "init", "--outputFile", "./testdata/module_test.json", "--style", "json"}, "./testdata/module_test_expected.json", }, + { + "partial json", + "./testdata/module_test_partial.json", + []string{"config", "init", "--outputFile", "./testdata/module_test_partial.json", "--style", "json"}, + "./testdata/module_test_partial_expected.json", + }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -103,6 +121,52 @@ func TestModule_ProvideCommand(t *testing.T) { } } +func TestModule_ProvideCommand_verifyCmd(t *testing.T) { + rootCmd := setup() + cases := []struct { + name string + args []string + isErr bool + }{ + { + "bad config", + []string{"config", "verify", "--targetFile", "./testdata/module_test_empty.yaml"}, + true, + }, + { + "bad config with module", + []string{"config", "verify", "foo", "--targetFile", "./testdata/module_test_empty.yaml"}, + true, + }, + { + "bad config with good module selected", + []string{"config", "verify", "baz", "--targetFile", "./testdata/module_test_empty.yaml"}, + false, + }, + { + "good config", + []string{"config", "verify", "--targetFile", "./testdata/module_test_gold.yaml"}, + false, + }, + { + "good config with module", + []string{"config", "verify", "foo", "--targetFile", "./testdata/module_test_gold.yaml"}, + false, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + rootCmd.SetArgs(c.args) + err := rootCmd.Execute() + if c.isErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + func TestModule_Watch(t *testing.T) { t.Run("test without module", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) diff --git a/config/testdata/module_test_empty.yaml b/config/testdata/module_test_empty.yaml new file mode 100644 index 00000000..139597f9 --- /dev/null +++ b/config/testdata/module_test_empty.yaml @@ -0,0 +1,2 @@ + + diff --git a/config/testdata/module_test_gold.yaml b/config/testdata/module_test_gold.yaml new file mode 100644 index 00000000..3c994ed6 --- /dev/null +++ b/config/testdata/module_test_gold.yaml @@ -0,0 +1,2 @@ +# A mock config +foo: bar diff --git a/config/testdata/module_test_partial.json b/config/testdata/module_test_partial.json new file mode 100644 index 00000000..c8c4105e --- /dev/null +++ b/config/testdata/module_test_partial.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} diff --git a/config/testdata/module_test_partial_expected.json b/config/testdata/module_test_partial_expected.json new file mode 100644 index 00000000..54bce183 --- /dev/null +++ b/config/testdata/module_test_partial_expected.json @@ -0,0 +1,4 @@ +{ + "baz": "qux", + "foo": "bar" +} diff --git a/default_config.go b/default_config.go index 2aaae1d4..fc6d30c9 100644 --- a/default_config.go +++ b/default_config.go @@ -1,7 +1,9 @@ package core import ( + "fmt" stdlog "log" + "net" "github.com/DoNewsCode/core/config" "github.com/DoNewsCode/core/contract" @@ -16,7 +18,6 @@ import ( const defaultConfig = ` name: app -version: 0.1.0 env: local http: addr: :8080 @@ -117,13 +118,13 @@ func provideDefaultConfig() []config.ExportedConfig { "name": "app", }, Comment: "The name of the application", - }, - { - Owner: "core", - Data: map[string]interface{}{ - "version": "0.1.0", + Validate: func(data map[string]interface{}) error { + _, err := getString(data, "name") + if err != nil { + return fmt.Errorf("the name field is not valid: %w", err) + } + return nil }, - Comment: "The version of the application", }, { Owner: "core", @@ -131,6 +132,17 @@ func provideDefaultConfig() []config.ExportedConfig { "env": "local", }, Comment: "The environment of the application, one of production, development, staging, testing or local", + Validate: func(data map[string]interface{}) error { + str, err := getString(data, "env") + if err != nil { + return fmt.Errorf("the env field is not valid: %w", err) + } + if config.NewEnv(str) != config.EnvUnknown { + return nil + } + return fmt.Errorf( + "the env field must be one of \"production\", \"development\", \"staging\", \"testing\" or \"local\", got %s", str) + }, }, { Owner: "core", @@ -141,6 +153,23 @@ func provideDefaultConfig() []config.ExportedConfig { }, }, Comment: "The http address", + Validate: func(data map[string]interface{}) error { + disable, err := getBool(data, "http", "disable") + if err != nil { + return fmt.Errorf("the http.disable field is not valid: %w", err) + } + if disable { + return nil + } + str, err := getString(data, "http", "addr") + if err != nil { + return fmt.Errorf("the http.addr field is not valid: %w", err) + } + if _, err := net.ResolveTCPAddr("tcp", str); err != nil { + return fmt.Errorf("the http.addr field must be an valid address like :8080, got %s", str) + } + return nil + }, }, { Owner: "core", @@ -151,6 +180,23 @@ func provideDefaultConfig() []config.ExportedConfig { }, }, Comment: "The gRPC address", + Validate: func(data map[string]interface{}) error { + disable, err := getBool(data, "grpc", "disable") + if err != nil { + return fmt.Errorf("the grpc.disable field is not valid: %w", err) + } + if disable { + return nil + } + str, err := getString(data, "grpc", "addr") + if err != nil { + return fmt.Errorf("the grpc.addr field is not valid: %w", err) + } + if _, err := net.ResolveTCPAddr("tcp", str); err != nil { + return fmt.Errorf("the grpc.addr field must be an valid address like :9090, got %s", str) + } + return nil + }, }, { Owner: "core", @@ -160,6 +206,13 @@ func provideDefaultConfig() []config.ExportedConfig { }, }, Comment: "The cron job runner", + Validate: func(data map[string]interface{}) error { + _, err := getBool(data, "cron", "disable") + if err != nil { + return fmt.Errorf("the cron.disable field is not valid: %w", err) + } + return nil + }, }, { Owner: "core", @@ -167,6 +220,23 @@ func provideDefaultConfig() []config.ExportedConfig { "log": map[string]interface{}{"level": "debug", "format": "logfmt"}, }, Comment: "The global logging level and format", + Validate: func(data map[string]interface{}) error { + lvl, err := getString(data, "log", "level") + if err != nil { + return fmt.Errorf("the log.level field is not valid: %w", err) + } + if !isValidLevel(lvl) { + return fmt.Errorf("allowed levels are \"debug\", \"info\", \"warn\", \"error\", or \"none\", got \"%s\"", lvl) + } + format, err := getString(data, "log", "format") + if err != nil { + return fmt.Errorf("the log.format field is not valid: %w", err) + } + if !isValidFormat(format) { + return fmt.Errorf("the log format is not supported") + } + return nil + }, }, } } diff --git a/default_config_test.go b/default_config_test.go index 464470d9..d83896de 100644 --- a/default_config_test.go +++ b/default_config_test.go @@ -1,7 +1,6 @@ package core import ( - "encoding/json" "testing" "github.com/stretchr/testify/assert" @@ -9,6 +8,184 @@ import ( func TestDefaultConfig(t *testing.T) { conf := provideDefaultConfig() - _, err := json.Marshal(conf) - assert.NoError(t, err) + for _, c := range conf { + if c.Validate != nil { + err := c.Validate(c.Data) + assert.NoError(t, err) + } + } +} + +func TestDefaultConfig_invalid(t *testing.T) { + conf := provideDefaultConfig() + + t.Run("empty", func(t *testing.T) { + for _, c := range conf { + if c.Validate != nil { + err := c.Validate(map[string]interface{}{}) + assert.Error(t, err) + } + } + }) + + t.Run("invalid http addr", func(t *testing.T) { + conf := provideDefaultConfig() + for _, c := range conf { + if c.Validate != nil { + err := c.Validate(map[string]interface{}{ + "http": map[string]interface{}{ + "addr": "aaa", + "disable": false, + }, + }) + assert.Error(t, err) + } + } + }) + + t.Run("invalid grpc addr", func(t *testing.T) { + conf := provideDefaultConfig() + for _, c := range conf { + if c.Validate != nil { + err := c.Validate(map[string]interface{}{ + "grpc": map[string]interface{}{ + "addr": "aaa", + "disable": false, + }, + }) + assert.Error(t, err) + } + } + }) + + t.Run("disabled transport http", func(t *testing.T) { + conf := provideDefaultConfig() + for _, c := range conf { + if c.Validate != nil { + err := c.Validate(map[string]interface{}{ + "http": map[string]interface{}{ + "addr": "aaa", + "disable": true, + }, + }) + if err == nil { + return + } + } + } + t.Error("disabled transport should not have addr requirement") + }) + + t.Run("disabled transport grpc", func(t *testing.T) { + conf := provideDefaultConfig() + for _, c := range conf { + if c.Validate != nil { + err := c.Validate(map[string]interface{}{ + "grpc": map[string]interface{}{ + "addr": "aaa", + "disable": true, + }, + }) + if err == nil { + return + } + } + } + t.Error("disabled transport should not have addr requirement") + }) + + t.Run("transport not map", func(t *testing.T) { + conf := provideDefaultConfig() + for _, c := range conf { + if c.Validate != nil { + err := c.Validate(map[string]interface{}{ + "grpc": ":8080", + }) + assert.Error(t, err) + } + } + }) + + t.Run("wrong type", func(t *testing.T) { + conf := provideDefaultConfig() + for _, c := range conf { + if c.Validate != nil { + err := c.Validate(map[string]interface{}{ + "grpc": map[string]interface{}{ + "disable": "", + }, + }) + assert.Error(t, err) + } + } + }) + + t.Run("wrong env", func(t *testing.T) { + conf := provideDefaultConfig() + for _, c := range conf { + if c.Validate != nil { + err := c.Validate(map[string]interface{}{ + "env": "bar", + }) + assert.Error(t, err) + } + } + }) + + t.Run("wrong app", func(t *testing.T) { + conf := provideDefaultConfig() + for _, c := range conf { + if c.Validate != nil { + err := c.Validate(map[string]interface{}{ + "app": 1, + }) + assert.Error(t, err) + } + } + }) + + t.Run("wrong log level", func(t *testing.T) { + conf := provideDefaultConfig() + for _, c := range conf { + if c.Validate != nil { + err := c.Validate(map[string]interface{}{ + "level": map[string]interface{}{ + "format": "json", + "level": "all", + }, + }) + assert.Error(t, err) + } + } + }) + + t.Run("wrong log format", func(t *testing.T) { + conf := provideDefaultConfig() + for _, c := range conf { + if c.Validate != nil { + err := c.Validate(map[string]interface{}{ + "level": map[string]interface{}{ + "format": "foo", + "level": "debug", + }, + }) + assert.Error(t, err) + } + } + }) +} + +func TestDefaultConfig_network(t *testing.T) { + conf := provideDefaultConfig() + for _, c := range conf { + if c.Validate != nil { + err := c.Validate(map[string]interface{}{ + "http": map[string]interface{}{ + "addr": "aaa", + "disable": false, + }, + }) + assert.Error(t, err) + } + } } diff --git a/dtx/sagas/dependency_test.go b/dtx/sagas/dependency_test.go index 45ab39e4..e325d914 100644 --- a/dtx/sagas/dependency_test.go +++ b/dtx/sagas/dependency_test.go @@ -9,7 +9,6 @@ import ( "github.com/DoNewsCode/core/di" "github.com/oklog/run" "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" ) type sagas struct { @@ -49,8 +48,7 @@ func TestNew(t *testing.T) { func Test_provideConfig(t *testing.T) { conf := provideConfig() - _, err := yaml.Marshal(conf.Config) - assert.NoError(t, err) + assert.NotNil(t, conf) } func timeout(duration time.Duration, g *run.Group) { diff --git a/dtx/sagas/mysqlstore/dependency_test.go b/dtx/sagas/mysqlstore/dependency_test.go index ad62018d..08dd2eac 100644 --- a/dtx/sagas/mysqlstore/dependency_test.go +++ b/dtx/sagas/mysqlstore/dependency_test.go @@ -7,7 +7,6 @@ import ( "github.com/DoNewsCode/core/dtx/sagas" "github.com/DoNewsCode/core/otgorm" "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" "gorm.io/gorm" ) @@ -39,6 +38,5 @@ func TestProviders(t *testing.T) { func Test_provideConfig(t *testing.T) { conf := provideConfig() - _, err := yaml.Marshal(conf.Config) - assert.NoError(t, err) + assert.NotNil(t, conf) } diff --git a/helper.go b/helper.go new file mode 100644 index 00000000..e017a2fb --- /dev/null +++ b/helper.go @@ -0,0 +1,79 @@ +package core + +import ( + "errors" + "fmt" + "strings" +) + +func getString(data map[string]interface{}, key ...string) (string, error) { + if len(key) <= 0 { + panic("key must be provided at least once") + } + for i := 0; i < len(key)-1; i++ { + value, ok := data[key[i]] + if !ok { + return "", fmt.Errorf("%s doesn't exist", strings.Join(key[0:i+1], ".")) + } + data, ok = value.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("%s is not a map", strings.Join(key[0:i+1], ".")) + } + } + value, ok := data[key[len(key)-1]] + if !ok { + return "", fmt.Errorf("%s doesn't exist", strings.Join(key, ".")) + } + str, ok := value.(string) + if !ok { + return str, errors.New("must be a string") + } + return str, nil +} + +func getBool(data map[string]interface{}, key ...string) (bool, error) { + if len(key) <= 0 { + panic("key must be provided at least once") + } + for i := 0; i < len(key)-1; i++ { + value, ok := data[key[i]] + if !ok { + return false, fmt.Errorf("%s doesn't exist", strings.Join(key[0:i+1], ".")) + } + data, ok = value.(map[string]interface{}) + if !ok { + return false, fmt.Errorf("%s is not a map", strings.Join(key[0:i+1], ".")) + } + } + value, ok := data[key[len(key)-1]] + if !ok { + return false, fmt.Errorf("%s doesn't exist", strings.Join(key, ".")) + } + b, ok := value.(bool) + if !ok { + return b, errors.New("must be a bool") + } + return b, nil +} + +// isValidLevel tests if the given input is valid level config. +func isValidLevel(levelCfg string) bool { + validLevel := []string{"debug", "info", "warn", "error", "none"} + for i := range validLevel { + if validLevel[i] == levelCfg { + return true + } + } + return false +} + +// isValidLevel tests if the given input is valid format config. +func isValidFormat(format string) bool { + validFormat := []string{"json", "logfmt"} + for i := range validFormat { + if validFormat[i] == format { + return true + } + } + return false +} diff --git a/leader/dependency_test.go b/leader/dependency_test.go index d81e6dcf..62ae72b2 100644 --- a/leader/dependency_test.go +++ b/leader/dependency_test.go @@ -7,7 +7,6 @@ import ( leaderetcd2 "github.com/DoNewsCode/core/leader/leaderetcd" "github.com/stretchr/testify/assert" "go.etcd.io/etcd/client/v3" - "gopkg.in/yaml.v3" ) type mockMaker struct { @@ -82,6 +81,5 @@ func TestDetermineDriver(t *testing.T) { func Test_provideConfig(t *testing.T) { conf := provideConfig() - _, err := yaml.Marshal(conf.Config) - assert.NoError(t, err) + assert.NotNil(t, conf) } diff --git a/otetcd/dependency_test.go b/otetcd/dependency_test.go index 7aa43ef9..5ed69d6d 100644 --- a/otetcd/dependency_test.go +++ b/otetcd/dependency_test.go @@ -11,7 +11,6 @@ import ( "github.com/go-kit/kit/log" "github.com/stretchr/testify/assert" "go.etcd.io/etcd/client/v3" - "gopkg.in/yaml.v3" ) func TestMain(m *testing.M) { @@ -68,6 +67,5 @@ func TestProvideFactory(t *testing.T) { func Test_provideConfig(t *testing.T) { conf := provideConfig() - _, err := yaml.Marshal(conf.Config) - assert.NoError(t, err) + assert.NotNil(t, conf) }