diff --git a/.drone.yml b/.drone.yml index 6182098..0261f19 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3,12 +3,12 @@ name: default services: - name: vault - image: vault:1.7.1 + image: vault:1.13.3 environment: VAULT_DEV_ROOT_TOKEN_ID: dev VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200 - name: etcd - image: bitnami/etcd:3 + image: bitnami/etcd:3.5.11 environment: ALLOW_NONE_AUTHENTICATION: yes diff --git a/client.go b/client.go index 508115e..15739e0 100644 --- a/client.go +++ b/client.go @@ -8,8 +8,8 @@ import ( "sync/atomic" ) -func Must(namespace, appName string, providers ...interface{}) *Client { - client, err := New(namespace, appName, providers...) +func Must(providers ...interface{}) *Client { + client, err := New(providers...) if err != nil { panic(err) } @@ -17,10 +17,8 @@ func Must(namespace, appName string, providers ...interface{}) *Client { return client } -func New(namespace, appName string, providers ...interface{}) (*Client, error) { +func New(providers ...interface{}) (*Client, error) { client := &Client{ - namespace: namespace, - appName: appName, providers: make([]Provider, len(providers)), } @@ -66,7 +64,7 @@ func (p *provider) init(ctx context.Context) error { return nil } -func (p *provider) Watch(ctx context.Context, key Key, callback WatchCallback) error { +func (p *provider) Watch(ctx context.Context, callback WatchCallback, path ...string) error { if err := p.init(ctx); err != nil { return fmt.Errorf("init read:%w", err) } @@ -76,21 +74,21 @@ func (p *provider) Watch(ctx context.Context, key Key, callback WatchCallback) e return nil } - if err := watch.Watch(ctx, key, callback); err != nil { + if err := watch.Watch(ctx, callback, path...); err != nil { return fmt.Errorf("factory provider: %w", err) } return nil } -func (p *provider) Read(ctx context.Context, key Key) (Variable, error) { +func (p *provider) Value(ctx context.Context, path ...string) (Value, error) { if err := p.init(ctx); err != nil { - return Variable{}, fmt.Errorf("init read:%w", err) + return nil, fmt.Errorf("init read:%w", err) } - variable, err := p.provider.Read(ctx, key) + variable, err := p.provider.Value(ctx, path...) if err != nil { - return Variable{}, fmt.Errorf("factory provider: %w", err) + return nil, fmt.Errorf("factory provider: %w", err) } return variable, nil @@ -98,53 +96,34 @@ func (p *provider) Read(ctx context.Context, key Key) (Variable, error) { type Client struct { providers []Provider - appName string - namespace string } -func (c *Client) key(name string) Key { - return Key{ - Name: name, - AppName: c.appName, - Namespace: c.namespace, - } +func (c *Client) Name() string { + return "client" } // Value get value by name. -// nolint: ireturn -func (c *Client) Value(ctx context.Context, name string) (Value, error) { - variable, err := c.Variable(ctx, name) - if err != nil { - return nil, fmt.Errorf("variable:%w", err) - } - - return variable.Value, nil -} - -func (c *Client) Variable(ctx context.Context, name string) (Variable, error) { +func (c *Client) Value(ctx context.Context, path ...string) (Value, error) { var ( - variable Variable - err error + value Value + err error ) - key := c.key(name) - for _, provider := range c.providers { - variable, err = provider.Read(ctx, key) - if err == nil || !(errors.Is(err, ErrVariableNotFound) || errors.Is(err, ErrInitFactory)) { + value, err = provider.Value(ctx, path...) + if err == nil || !(errors.Is(err, ErrValueNotFound) || errors.Is(err, ErrInitFactory)) { break } } if err != nil { - return variable, fmt.Errorf("client failed get variable: %w", err) + return value, fmt.Errorf("client failed get value: %w", err) } - return variable, nil + return value, nil } -func (c *Client) Watch(ctx context.Context, name string, callback WatchCallback) error { - key := c.key(name) +func (c *Client) Watch(ctx context.Context, callback WatchCallback, path ...string) error { for idx, prov := range c.providers { provider, ok := prov.(WatchProvider) @@ -152,9 +131,9 @@ func (c *Client) Watch(ctx context.Context, name string, callback WatchCallback) continue } - err := provider.Watch(ctx, key, callback) + err := provider.Watch(ctx, callback, path...) if err != nil { - if errors.Is(err, ErrVariableNotFound) || errors.Is(err, ErrInitFactory) { + if errors.Is(err, ErrValueNotFound) || errors.Is(err, ErrInitFactory) { continue } diff --git a/client_example_test.go b/client_example_test.go index 7550fb5..56de1aa 100644 --- a/client_example_test.go +++ b/client_example_test.go @@ -20,6 +20,11 @@ import ( ) func ExampleClient_Value() { + const ( + namespace = "fdevs" + appName = "config" + ) + ctx := context.Background() _ = os.Setenv("FDEVS_CONFIG_LISTEN", "8080") _ = os.Setenv("FDEVS_CONFIG_HOST", "localhost") @@ -51,11 +56,11 @@ func ExampleClient_Value() { // read json config jsonConfig := test.ReadFile("config.json") - config, err := config.New(test.Namespace, test.AppName, + config, err := config.New( arg.New(), - env.New(), - etcd.NewProvider(etcdClient), - vault.NewSecretKV2(vaultClient), + env.New(test.Namespace, test.AppName), + etcd.NewProvider(namespace, appName, etcdClient), + vault.NewSecretKV2(namespace, appName, vaultClient), json.New(jsonConfig), ) if err != nil { @@ -64,30 +69,30 @@ func ExampleClient_Value() { return } - dsn, err := config.Value(ctx, "example:dsn") + dsn, err := config.Value(ctx, "example", "dsn") if err != nil { - log.Print(err) + log.Print("example:dsn", err) return } port, err := config.Value(ctx, "listen") if err != nil { - log.Print(err) + log.Print("listen", err) return } enabled, err := config.Value(ctx, "maintain") if err != nil { - log.Print(err) + log.Print("maintain", err) return } title, err := config.Value(ctx, "app.name.title") if err != nil { - log.Print(err) + log.Print("app.name.title", err) return } @@ -125,6 +130,11 @@ func ExampleClient_Value() { } func ExampleClient_Watch() { + const ( + namespace = "fdevs" + appName = "config" + ) + ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -155,10 +165,10 @@ func ExampleClient_Watch() { } }() - watcher, err := config.New(test.Namespace, test.AppName, - watcher.New(time.Microsecond, env.New()), + watcher, err := config.New( + watcher.New(time.Microsecond, env.New(test.Namespace, test.AppName)), watcher.New(time.Microsecond, yaml.NewWatch("test/fixture/config.yaml")), - etcd.NewProvider(etcdClient), + etcd.NewProvider(namespace, appName, etcdClient), ) if err != nil { log.Print(err) @@ -169,10 +179,10 @@ func ExampleClient_Watch() { wg := sync.WaitGroup{} wg.Add(2) - err = watcher.Watch(ctx, "example_enable", func(ctx context.Context, oldVar, newVar config.Variable) { - fmt.Println("update ", oldVar.Provider, " variable:", oldVar.Name, ", old: ", oldVar.Value.Bool(), " new:", newVar.Value.Bool()) + err = watcher.Watch(ctx, func(ctx context.Context, oldVar, newVar config.Value) { + fmt.Println("update example_enable old: ", oldVar.Bool(), " new:", newVar.Bool()) wg.Done() - }) + }, "example_enable") if err != nil { log.Print(err) @@ -181,10 +191,10 @@ func ExampleClient_Watch() { _ = os.Setenv("FDEVS_CONFIG_EXAMPLE_ENABLE", "false") - err = watcher.Watch(ctx, "example_db_dsn", func(ctx context.Context, oldVar, newVar config.Variable) { - fmt.Println("update ", oldVar.Provider, " variable:", oldVar.Name, ", old: ", oldVar.Value.String(), " new:", newVar.Value.String()) + err = watcher.Watch(ctx, func(ctx context.Context, oldVar, newVar config.Value) { + fmt.Println("update example_db_dsn old: ", oldVar.String(), " new:", newVar.String()) wg.Done() - }) + }, "example_db_dsn") if err != nil { log.Print(err) @@ -202,8 +212,8 @@ func ExampleClient_Watch() { wg.Wait() // Output: - // update env variable: FDEVS_CONFIG_EXAMPLE_ENABLE , old: true new: false - // update etcd variable: fdevs/config/example_db_dsn , old: pgsql://user@pass:127.0.0.1:5432 new: mysql://localhost:5432 + // update example_enable old: true new: false + // update example_db_dsn old: pgsql://user@pass:127.0.0.1:5432 new: mysql://localhost:5432 } func ExampleClient_Value_factory() { @@ -219,10 +229,10 @@ func ExampleClient_Value_factory() { os.Args = []string{"main.go", "--config-json=config.json", "--config-yaml=test/fixture/config.yaml"} - config, err := config.New(test.Namespace, test.AppName, + config, err := config.New( arg.New(), - env.New(), - config.Factory(func(ctx context.Context, cfg config.ReadConfig) (config.Provider, error) { + env.New(test.Namespace, test.AppName), + config.Factory(func(ctx context.Context, cfg config.Provider) (config.Provider, error) { val, err := cfg.Value(ctx, "config-json") if err != nil { return nil, fmt.Errorf("failed read config file:%w", err) @@ -231,7 +241,7 @@ func ExampleClient_Value_factory() { return json.New(jsonConfig), nil }), - config.Factory(func(ctx context.Context, cfg config.ReadConfig) (config.Provider, error) { + config.Factory(func(ctx context.Context, cfg config.Provider) (config.Provider, error) { val, err := cfg.Value(ctx, "config-yaml") if err != nil { return nil, fmt.Errorf("failed read config file:%w", err) @@ -258,14 +268,14 @@ func ExampleClient_Value_factory() { return } - title, err := config.Value(ctx, "app.name.title") + title, err := config.Value(ctx, "app", "name", "title") if err != nil { log.Print(err) return } - yamlTitle, err := config.Value(ctx, "app/title") + yamlTitle, err := config.Value(ctx, "app", "title") if err != nil { log.Print(err) diff --git a/definition/defenition.go b/definition/defenition.go new file mode 100755 index 0000000..64385e1 --- /dev/null +++ b/definition/defenition.go @@ -0,0 +1,29 @@ +package definition + +import ( + "fmt" +) + +func New() Definition { + return Definition{} +} + +type Definition struct { + options Options +} + +func (d *Definition) Add(opts ...Option) *Definition { + d.options = append(d.options, opts...) + + return d +} + +func (d *Definition) View(handle func(Option) error) error { + for idx, opt := range d.options { + if err := handle(opt); err != nil { + return fmt.Errorf("%s[%d]:%w", opt.Kind(), idx, err) + } + } + + return nil +} diff --git a/definition/generate/generator.go b/definition/generate/generator.go new file mode 100644 index 0000000..fdb9e93 --- /dev/null +++ b/definition/generate/generator.go @@ -0,0 +1,70 @@ +package generate + +import ( + "fmt" + "io" + + "gitoa.ru/go-4devs/config/definition" +) + +type Generator struct { + pkg string + ViewOption + Imp Imports + errs []error + defaultErrors []string +} + +func (g Generator) Pkg() string { + return g.pkg +} + +func (g Generator) Imports() []Import { + return g.Imp.Imports() +} + +func (g Generator) Handle(w io.Writer, data Handler, opt definition.Option) error { + handle := get(opt.Kind()) + + return handle(w, data, opt) +} + +func (g Generator) StructName() string { + return FuncName(g.Prefix + "_" + g.Struct + "_" + g.Suffix) +} + +func (g Generator) Options() ViewOption { + return g.ViewOption +} + +func (g Generator) Keys() []string { + return nil +} + +func (g Generator) DefaultErrors() []string { + if len(g.defaultErrors) > 0 { + return g.defaultErrors + } + + if len(g.ViewOption.Errors.Default) > 0 { + g.Imp.Adds("errors") + } + + g.defaultErrors = make([]string, len(g.ViewOption.Errors.Default)) + for idx, name := range g.ViewOption.Errors.Default { + short, err := g.AddType(name) + if err != nil { + g.errs = append(g.errs, fmt.Errorf("add default error[%d]:%w", idx, err)) + + return nil + } + + g.defaultErrors[idx] = short + } + + return g.defaultErrors +} + +func (g *Generator) AddType(pkg string) (string, error) { + return g.Imp.AddType(pkg) +} diff --git a/definition/generate/helpers.go b/definition/generate/helpers.go new file mode 100644 index 0000000..b3cdf0c --- /dev/null +++ b/definition/generate/helpers.go @@ -0,0 +1,16 @@ +package generate + +import ( + "errors" + + "github.com/iancoleman/strcase" +) + +var ( + ErrNotFound = errors.New("not found") + ErrAlreadyExist = errors.New("already exist") +) + +func FuncName(in string) string { + return strcase.ToCamel(in) +} diff --git a/definition/generate/imports.go b/definition/generate/imports.go new file mode 100644 index 0000000..fc40a59 --- /dev/null +++ b/definition/generate/imports.go @@ -0,0 +1,87 @@ +package generate + +import ( + "fmt" + "strconv" + "strings" +) + +func NewImports() Imports { + return Imports{ + data: make(map[string]string), + } +} + +type Imports struct { + data map[string]string +} + +func (i Imports) Imports() []Import { + imports := make([]Import, 0, len(i.data)) + for name, alias := range i.data { + imports = append(imports, Import{ + Package: name, + Alias: alias, + }) + } + + return imports +} + +func (i *Imports) Short(fullType string) (string, error) { + idx := strings.LastIndexByte(fullType, '.') + if idx == -1 { + return "", fmt.Errorf("unexpected") + } + if alias, ok := i.data[fullType[:idx]]; ok { + return alias + fullType[idx:], nil + } + + return "", fmt.Errorf("%w alias for pkg %v", ErrNotFound, fullType[:idx]) +} + +func (i *Imports) AddType(fullType string) (string, error) { + idx := strings.LastIndexByte(fullType, '.') + if idx == -1 { + return "", fmt.Errorf("unexpected") + } + + imp := i.Add(fullType[:idx]) + + return imp.Alias + fullType[idx:], nil + +} + +func (i *Imports) Adds(pkgs ...string) { + for _, pkg := range pkgs { + i.Add(pkg) + } +} + +func (i *Imports) Add(pkg string) Import { + alias := pkg + idx := strings.LastIndexByte(pkg, '/') + if idx != -1 { + alias = pkg[idx+1:] + } + if al, ok := i.data[pkg]; ok { + return Import{Package: pkg, Alias: al} + } + + for _, al := range i.data { + if al == alias { + alias += strconv.Itoa(len(i.data)) + } + } + i.data[pkg] = alias + + return Import{ + Alias: alias, + Package: pkg, + } +} + +type Import struct { + Alias string + Package string +} diff --git a/definition/generate/run.go b/definition/generate/run.go new file mode 100644 index 0000000..a0a0029 --- /dev/null +++ b/definition/generate/run.go @@ -0,0 +1,38 @@ +package generate + +import ( + "bytes" + "fmt" + "io" + + "gitoa.ru/go-4devs/config/definition" +) + +func Run(w io.Writer, pkgName string, defs definition.Definition, viewOpt ViewOption) error { + gen := Generator{ + pkg: pkgName, + ViewOption: viewOpt, + Imp: NewImports(), + } + + gen.Imp.Adds("gitoa.ru/go-4devs/config", "fmt", "context") + + var view bytes.Buffer + err := defs.View(func(o definition.Option) error { + return gen.Handle(&view, &gen, o) + }) + if err != nil { + return fmt.Errorf("render options:%w", err) + } + + if err := tpl.Execute(w, gen); err != nil { + return fmt.Errorf("render base:%w", err) + } + + _, cerr := io.Copy(w, &view) + if cerr != nil { + return fmt.Errorf("copy error:%w", cerr) + } + + return nil +} diff --git a/definition/generate/template.go b/definition/generate/template.go new file mode 100644 index 0000000..34ed076 --- /dev/null +++ b/definition/generate/template.go @@ -0,0 +1,41 @@ +package generate + +import "text/template" + +var tpl = template.Must(template.New("tpls").Parse(baseTemplate)) + +var baseTemplate = `// Code generated gitoa.ru/go-4devs/config DO NOT EDIT. +package {{.Pkg}} + +import ( + {{range .Imports}} + {{- .Alias }}"{{ .Package }}" + {{end}} +) + +func With{{.StructName}}Log(log func(context.Context, string, ...any)) func(*{{.StructName}}) { + return func(ci *{{.StructName}}) { + ci.log = log + } +} + +func New{{.StructName}}(prov config.Provider, opts ...func(*{{.StructName}})) {{.StructName}} { + i := {{.StructName}}{ + Provider: prov, + log: func(_ context.Context, format string, args ...any) { + fmt.Printf(format, args...) + }, + } + + for _, opt := range opts { + opt(&i) + } + + return i +} + +type {{.StructName}} struct { + config.Provider + log func(context.Context, string, ...any) +} +` diff --git a/definition/generate/view.go b/definition/generate/view.go new file mode 100644 index 0000000..31f5a68 --- /dev/null +++ b/definition/generate/view.go @@ -0,0 +1,60 @@ +package generate + +import ( + "fmt" + "io" + "sync" + + "gitoa.ru/go-4devs/config/definition" +) + +var handlers = sync.Map{} + +func Add(kind string, h Handle) error { + _, ok := handlers.Load(kind) + if ok { + return fmt.Errorf("kind %v: %w", kind, ErrAlreadyExist) + } + + handlers.Store(kind, h) + return nil +} + +func get(kind string) Handle { + h, ok := handlers.Load(kind) + if !ok { + return func(w io.Writer, h Handler, o definition.Option) error { + return fmt.Errorf("handler by %v:%w", kind, ErrNotFound) + } + } + + return h.(Handle) +} + +func MustAdd(kind string, h Handle) { + if err := Add(kind, h); err != nil { + panic(err) + } +} + +type Handle func(io.Writer, Handler, definition.Option) error + +type Handler interface { + StructName() string + Handle(io.Writer, Handler, definition.Option) error + Options() ViewOption + Keys() []string + AddType(fullName string) (string, error) + DefaultErrors() []string +} + +type ViewOption struct { + Prefix, Suffix string + Context bool + Struct string + Errors ViewErrors +} + +type ViewErrors struct { + Default []string +} diff --git a/definition/group/group.go b/definition/group/group.go new file mode 100755 index 0000000..32328c3 --- /dev/null +++ b/definition/group/group.go @@ -0,0 +1,27 @@ +package group + +import ( + "gitoa.ru/go-4devs/config/definition" +) + +const Kind = "group" + +var _ definition.Option = Group{} + +func New(name, desc string, opts ...definition.Option) Group { + return Group{ + Name: name, + Description: desc, + Options: opts, + } +} + +type Group struct { + Options definition.Options + Name string + Description string +} + +func (o Group) Kind() string { + return Kind +} diff --git a/definition/group/view.go b/definition/group/view.go new file mode 100644 index 0000000..250ad11 --- /dev/null +++ b/definition/group/view.go @@ -0,0 +1,85 @@ +package group + +import ( + "fmt" + "io" + "text/template" + + "gitoa.ru/go-4devs/config/definition" + "gitoa.ru/go-4devs/config/definition/generate" +) + +func init() { + generate.MustAdd(Kind, handle) +} + +func handle(w io.Writer, data generate.Handler, option definition.Option) error { + group, ok := option.(Group) + if !ok { + return fmt.Errorf("uexepected type:%T", option) + } + viewData := View{ + Group: group, + ParentName: data.StructName(), + ViewOption: data.Options(), + } + + err := tpl.Execute(w, viewData) + + if err != nil { + return fmt.Errorf("render group:%w", err) + } + + childData := ChildData{ + Handler: data, + structName: viewData.StructName(), + keys: append(data.Keys(), group.Name), + } + for idx, child := range group.Options { + if cerr := data.Handle(w, childData, child); cerr != nil { + return fmt.Errorf("render group child[%d]:%w", idx, cerr) + } + } + + return nil +} + +type ChildData struct { + generate.Handler + structName string + keys []string +} + +func (c ChildData) StructName() string { + return c.structName +} + +func (v ChildData) Keys() []string { + return v.keys +} + +type View struct { + Group + ParentName string + generate.ViewOption +} + +func (v View) FuncName() string { + return generate.FuncName(v.Name) +} + +func (v View) StructName() string { + return generate.FuncName(v.Prefix + v.Name + v.Suffix) +} + +var tpl = template.Must(template.New("tpls").Parse(tplw)) + +var tplw = `type {{.StructName}} struct { + {{.ParentName}} +} + +// {{.FuncName}} {{.Description}}. +func (i {{.ParentName}}) {{.FuncName}}() {{.StructName}} { + return {{.StructName}}{i} +} +` diff --git a/definition/option.go b/definition/option.go new file mode 100755 index 0000000..9a3bd57 --- /dev/null +++ b/definition/option.go @@ -0,0 +1,27 @@ +package definition + +type Option interface { + Kind() string +} + +type Options []Option + +func (s Options) Len() int { return len(s) } +func (s Options) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +type Params []Param + +func (p Params) Get(name string) (any, bool) { + for _, param := range p { + if param.Name == name { + return param.Value, true + } + } + + return nil, false +} + +type Param struct { + Name string + Value any +} diff --git a/definition/option/option.go b/definition/option/option.go new file mode 100755 index 0000000..1936f2f --- /dev/null +++ b/definition/option/option.go @@ -0,0 +1,100 @@ +package option + +import ( + "gitoa.ru/go-4devs/config/definition" +) + +var _ definition.Option = Option{} + +const ( + Kind = "option" +) + +const ( + TypeString = "string" + TypeInt = "int" + TypeInt64 = "int64" + TypeUint = "uint" + TypeUint64 = "uint64" + TypeFloat64 = "float64" + TypeBool = "bool" + TypeTime = "time.Time" + TypeDuration = "time.Duration" +) + +func Default(v any) func(*Option) { + return func(o *Option) { + o.Default = v + } +} + +func New(name, desc string, vtype any, opts ...func(*Option)) Option { + option := Option{ + Name: name, + Description: desc, + Type: vtype, + } + + for _, opt := range opts { + opt(&option) + } + + return option +} + +type Option struct { + Name string + Description string + Type any + Default any + Params definition.Params +} + +func (o Option) WithParams(params ...definition.Param) Option { + return Option{ + Name: o.Name, + Description: o.Description, + Type: o.Type, + Params: append(params, o.Params...), + } +} + +func (o Option) Kind() string { + return Kind +} + +func Time(name, desc string, opts ...func(*Option)) Option { + return New(name, desc, TypeTime, opts...) +} + +func Duration(name, desc string, opts ...func(*Option)) Option { + return New(name, desc, TypeDuration, opts...) +} + +func String(name, desc string, opts ...func(*Option)) Option { + return New(name, desc, TypeString, opts...) +} + +func Int(name, desc string, opts ...func(*Option)) Option { + return New(name, desc, TypeInt, opts...) +} + +func Int64(name, desc string, opts ...func(*Option)) Option { + return New(name, desc, TypeInt64, opts...) +} + +func Uint(name, desc string, opts ...func(*Option)) Option { + return New(name, desc, TypeUint, opts...) +} + +func Uint64(name, desc string, opts ...func(*Option)) Option { + return New(name, desc, TypeUint64, opts...) +} + +func Float64(name, desc string, opts ...func(*Option)) Option { + return New(name, desc, TypeFloat64, opts...) +} + +func Bool(name, desc string, opts ...func(*Option)) Option { + return New(name, desc, TypeBool, opts...) +} diff --git a/definition/option/tpl/option.tmpl b/definition/option/tpl/option.tmpl new file mode 100644 index 0000000..73f5532 --- /dev/null +++ b/definition/option/tpl/option.tmpl @@ -0,0 +1,33 @@ +// read{{.FuncName}} {{.Description}}. +func (i {{.StructName}}) read{{.FuncName}}(ctx context.Context) (v {{.Type}},e error) { + val, err := i.Value(ctx, {{ .ParentKeys }}"{{ .Name }}") + if err != nil { + {{if .HasDefault}} + {{$default := .Default}} + {{range .DefaultErrors}} + if errors.Is(err,{{.}}){ + return {{$default}} + } + {{end}} + {{end}} + return v, fmt.Errorf("read {{.Keys}}:%w",err) + } + + {{.Parse "val" "v" .Keys }} +} + +// Read{{.FuncName}} {{.Description}}. +func (i {{.StructName}}) Read{{.FuncName}}(ctx context.Context) ({{.Type}}, error) { + return i.read{{.FuncName}}(ctx) +} + +// {{.FuncName}} {{.Description}}. +func (i {{.StructName}}) {{.FuncName}}({{if .Context}} ctx context.Context {{end}}) {{.Type}} { + {{if not .Context}} ctx := context.Background() {{end}} + val, err := i.read{{.FuncName}}(ctx) + if err != nil { + i.log(ctx, "get {{.Keys}}: %v", err) + } + + return val +} diff --git a/definition/option/tpl/parse.tmpl b/definition/option/tpl/parse.tmpl new file mode 100644 index 0000000..0cbc58b --- /dev/null +++ b/definition/option/tpl/parse.tmpl @@ -0,0 +1,3 @@ +{{block "Parse" .}} +return {{.ValName}}.Parse{{ .FuncType}}() +{{end}} \ No newline at end of file diff --git a/definition/option/tpl/unmarshal_json.tmpl b/definition/option/tpl/unmarshal_json.tmpl new file mode 100644 index 0000000..b54d7fb --- /dev/null +++ b/definition/option/tpl/unmarshal_json.tmpl @@ -0,0 +1,8 @@ +{{block "UnmarshalJSON" . }} + pval, perr := {{.ValName}}.ParseString() + if perr != nil { + return {{.Value}}, fmt.Errorf("read {{.Keys}}:%w", perr) + } + + return {{.Value}}, {{.Value}}.UnmarshalJSON([]byte(pval)) +{{end}} \ No newline at end of file diff --git a/definition/option/tpl/unmarshal_text.tmpl b/definition/option/tpl/unmarshal_text.tmpl new file mode 100644 index 0000000..fbb8105 --- /dev/null +++ b/definition/option/tpl/unmarshal_text.tmpl @@ -0,0 +1,8 @@ +{{block "UnmarshalText" . }} + pval, perr := {{.ValName}}.ParseString() + if perr != nil { + return {{.Value}}, fmt.Errorf("read {{.Keys}}:%w", perr) + } + + return {{.Value}}, {{.Value}}.UnmarshalText([]byte(pval)) +{{end}} \ No newline at end of file diff --git a/definition/option/view.go b/definition/option/view.go new file mode 100644 index 0000000..777d4e4 --- /dev/null +++ b/definition/option/view.go @@ -0,0 +1,226 @@ +package option + +import ( + "bytes" + "embed" + "encoding" + "encoding/json" + "fmt" + "io" + "reflect" + "strings" + "text/template" + "time" + + "gitoa.ru/go-4devs/config/definition" + "gitoa.ru/go-4devs/config/definition/generate" +) + +//go:embed tpl/* +var tpls embed.FS + +var tpl = template.Must(template.New("tpls").ParseFS(tpls, "tpl/*.tmpl")) + +func init() { + generate.MustAdd(Kind, Handle(tpl.Lookup("option.tmpl"))) +} + +func Handle(tpl *template.Template) generate.Handle { + return func(w io.Writer, h generate.Handler, o definition.Option) error { + opt, _ := o.(Option) + if err := tpl.Execute(w, View{Option: opt, Handler: h}); err != nil { + return fmt.Errorf("option tpl:%w", err) + } + + return nil + } +} + +type View struct { + Option + generate.Handler +} + +func (v View) Context() bool { + return v.Options().Context +} + +func (v View) FuncName() string { + if funcName, ok := v.Option.Params.Get(ViewParamFunctName); ok { + return funcName.(string) + } + + return generate.FuncName(v.Name) +} + +func (v View) Description() string { + if desc, ok := v.Option.Params.Get(ViewParamDescription); ok { + return desc.(string) + } + + return v.Option.Description +} + +func (v View) Default() string { + switch data := v.Option.Default.(type) { + case time.Time: + return fmt.Sprintf("time.Parse(%q,time.RFC3339Nano)", data.Format(time.RFC3339Nano)) + case time.Duration: + return fmt.Sprintf("time.ParseDuration(%q)", data) + default: + return fmt.Sprintf("%#v, nil", data) + } +} + +func (v View) HasDefault() bool { + return v.Option.Default != nil +} + +func (v View) ParentKeys() string { + if len(v.Handler.Keys()) > 0 { + return `"` + strings.Join(v.Handler.Keys(), `","`) + `",` + } + + return "" +} + +func (v View) Type() string { + slice := "" + if vtype, ok := v.Option.Type.(string); ok { + if strings.Contains(vtype, ".") { + if name, err := v.AddType(vtype); err == nil { + return slice + name + } + } + return vtype + } + + rtype := reflect.TypeOf(v.Option.Type) + + if rtype.PkgPath() == "" { + return rtype.String() + } + + if rtype.Kind() == reflect.Slice { + slice = "[]" + } + + short, err := v.AddType(rtype.PkgPath() + "." + rtype.Name()) + if err != nil { + return err.Error() + } + + return slice + short +} + +func (v View) FuncType() string { + return generate.FuncName(v.Type()) +} + +func (v View) Parse(valName string, value string, keys []string) string { + + h := parser(v.Option.Type) + + data, err := h(ParseData{ + Value: value, + ValName: valName, + Keys: keys, + View: v, + }) + if err != nil { + return err.Error() + } + + return data +} + +var parses = map[string]func(data ParseData) (string, error){ + typesIntreface[0].Name(): func(data ParseData) (string, error) { + var b bytes.Buffer + + err := tpl.ExecuteTemplate(&b, "unmarshal_text.tmpl", data) + if err != nil { + return "", err + } + + return b.String(), nil + }, + typesIntreface[1].Name(): func(data ParseData) (string, error) { + var b bytes.Buffer + + err := tpl.ExecuteTemplate(&b, "unmarshal_json.tmpl", data) + if err != nil { + return "", err + } + + return b.String(), nil + }, + TypeInt: internal, + TypeInt64: internal, + TypeBool: internal, + TypeString: internal, + TypeFloat64: internal, + TypeUint: internal, + TypeUint64: internal, + "time.Duration": func(data ParseData) (string, error) { + return fmt.Sprintf("return %s.ParseDuration()", data.ValName), nil + }, + "time.Time": func(data ParseData) (string, error) { + return fmt.Sprintf("return %s.ParseTime()", data.ValName), nil + }, + "any": func(data ParseData) (string, error) { + return fmt.Sprintf("return %[2]s, %[1]s.Unmarshal(&%[2]s)", data.ValName, data.Value), nil + }, +} + +func internal(data ParseData) (string, error) { + var b bytes.Buffer + err := tpl.ExecuteTemplate(&b, "parse.tmpl", data) + if err != nil { + return "", err + } + + return b.String(), nil +} + +var ( + typesIntreface = [...]reflect.Type{ + reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem(), + reflect.TypeOf((*json.Unmarshaler)(nil)).Elem(), + } +) + +func parser(data any) func(ParseData) (string, error) { + vtype := reflect.TypeOf(data) + name := vtype.Name() + if v, ok := data.(string); ok { + name = v + } + + if vtype.Kind() == reflect.Slice { + return parses["any"] + } + + if h, ok := parses[name]; ok { + return h + } + + for _, extypes := range typesIntreface { + if vtype.Implements(extypes) { + return parses[extypes.Name()] + } + + if vtype.Kind() != reflect.Ptr && reflect.PointerTo(vtype).Implements(extypes) { + return parses[extypes.Name()] + } + } + + return parses["any"] +} + +type ParseData struct { + Value string + ValName string + Keys []string + View +} diff --git a/definition/option/view_params.go b/definition/option/view_params.go new file mode 100644 index 0000000..36c4ee1 --- /dev/null +++ b/definition/option/view_params.go @@ -0,0 +1,6 @@ +package option + +const ( + ViewParamFunctName = "view.funcName" + ViewParamDescription = "view.description" +) diff --git a/definition/proto/proto.go b/definition/proto/proto.go new file mode 100644 index 0000000..439487a --- /dev/null +++ b/definition/proto/proto.go @@ -0,0 +1,27 @@ +package proto + +import ( + "gitoa.ru/go-4devs/config/definition" +) + +const Kind = "proto" + +func New(name, desc string, opt definition.Option, opts ...func(*Proto)) Proto { + pr := Proto{ + Name: name, + Description: desc, + Option: opt, + } + + return pr +} + +type Proto struct { + Name string + Description string + Option definition.Option +} + +func (p Proto) Kind() string { + return Kind +} diff --git a/definition/proto/view.go b/definition/proto/view.go new file mode 100644 index 0000000..b046cac --- /dev/null +++ b/definition/proto/view.go @@ -0,0 +1,76 @@ +package proto + +import ( + "fmt" + "io" + "strings" + "text/template" + + "gitoa.ru/go-4devs/config/definition" + "gitoa.ru/go-4devs/config/definition/generate" + "gitoa.ru/go-4devs/config/definition/option" +) + +func init() { + generate.MustAdd(Kind, handle) +} + +func handle(w io.Writer, data generate.Handler, opt definition.Option) error { + proto, ok := opt.(Proto) + if !ok { + return fmt.Errorf("uexepected type:%T", opt) + } + if viewOpt, ok := proto.Option.(option.Option); ok { + viewOpt = viewOpt.WithParams( + definition.Param{ + Name: option.ViewParamFunctName, + Value: generate.FuncName(proto.Name) + generate.FuncName(viewOpt.Name), + }, + definition.Param{ + Name: option.ViewParamDescription, + Value: proto.Description + " " + viewOpt.Description, + }, + ) + + return option.Handle(tpl)(w, data, viewOpt) + } + + return fmt.Errorf("not support option type") +} + +var tpl = template.Must(template.New("tpls").Funcs(template.FuncMap{"join": strings.Join}).Parse(templateOption)) + +var templateOption = `// read{{.FuncName}} {{.Description}}. +func (i {{.StructName}}) read{{.FuncName}}(ctx context.Context, key string) (v {{.Type}},e error) { + val, err := i.Value(ctx, {{ .ParentKeys }} key, "{{.Name}}") + if err != nil { + {{if .HasDefault}} + {{$default := .Default}} + {{range .DefaultErrors}} + if errors.Is(err,{{.}}){ + return {{$default}} + } + {{end}} + {{end}} + return v, fmt.Errorf("read {{.Keys}}:%w",err) + } + + {{.Parse "val" "v" .Keys }} +} + +// Read{{.FuncName}} {{.Description}}. +func (i {{.StructName}}) Read{{.FuncName}}(ctx context.Context, key string) ({{.Type}}, error) { + return i.read{{.FuncName}}(ctx, key) +} + +// {{.FuncName}} {{.Description}}. +func (i {{.StructName}}) {{.FuncName}}({{if .Context}} ctx context.Context, {{end}} key string) {{.Type}} { + {{if not .Context}} ctx := context.Background() {{end}} + val, err := i.read{{.FuncName}}(ctx, key) + if err != nil { + i.log(ctx, "get {{.Keys}}: %v", err) + } + + return val +} +` diff --git a/docker-compose.yml b/docker-compose.yml index a41d67b..92a0b4c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: vault: - image: vault:latest + image: vault:1.13.3 cap_add: - IPC_LOCK ports: @@ -10,7 +10,7 @@ services: environment: VAULT_DEV_ROOT_TOKEN_ID: "dev" etcd: - image: bitnami/etcd + image: bitnami/etcd:3.5.11 environment: ALLOW_NONE_AUTHENTICATION: "yes" ports: diff --git a/error.go b/error.go index ddd9d74..93c30be 100644 --- a/error.go +++ b/error.go @@ -3,8 +3,8 @@ package config import "errors" var ( - ErrVariableNotFound = errors.New("variable not found") - ErrInvalidValue = errors.New("invalid value") - ErrUnknowType = errors.New("unknow type") - ErrInitFactory = errors.New("init factory") + ErrValueNotFound = errors.New("value not found") + ErrInvalidValue = errors.New("invalid value") + ErrUnknowType = errors.New("unknow type") + ErrInitFactory = errors.New("init factory") ) diff --git a/go.mod b/go.mod index 824d50a..18dcf20 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ module gitoa.ru/go-4devs/config -go 1.16 +go 1.18 require ( github.com/hashicorp/vault/api v1.1.0 + github.com/iancoleman/strcase v0.3.0 github.com/pelletier/go-toml v1.9.0 - github.com/smartystreets/goconvey v1.6.4 // indirect github.com/stretchr/testify v1.7.0 github.com/tidwall/gjson v1.7.5 go.etcd.io/etcd/api/v3 v3.5.0-alpha.0 @@ -13,3 +13,40 @@ require ( gopkg.in/ini.v1 v1.62.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) + +require ( + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gogo/protobuf v1.3.1 // indirect + github.com/golang/protobuf v1.3.5 // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.1 // indirect + github.com/hashicorp/go-multierror v1.1.0 // indirect + github.com/hashicorp/go-retryablehttp v0.6.6 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.3.2 // indirect + github.com/pierrec/lz4 v2.0.5+incompatible // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/smartystreets/goconvey v1.6.4 // indirect + github.com/tidwall/match v1.0.3 // indirect + github.com/tidwall/pretty v1.1.0 // indirect + go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0 // indirect + go.uber.org/atomic v1.6.0 // indirect + go.uber.org/multierr v1.5.0 // indirect + go.uber.org/zap v1.16.0 // indirect + golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 // indirect + golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + golang.org/x/text v0.3.0 // indirect + golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect + google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884 // indirect + google.golang.org/grpc v1.32.0 // indirect + gopkg.in/square/go-jose.v2 v2.5.1 // indirect +) diff --git a/go.sum b/go.sum index 21c49ad..2cead0e 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,8 @@ github.com/hashicorp/vault/api v1.1.0/go.mod h1:R3Umvhlxi2TN7Ex2hzOowyeNb+SfbVWI github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267 h1:e1ok06zGrWJW91rzRroyl5nRNqraaBe4d5hiKcVZuHM= github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -257,8 +259,9 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 h1:bNEHhJCnrwMKNMmOx3yAynp5vs5/gRy+XWFtZFu7NBM= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= diff --git a/provider.go b/provider.go index 9b1ffff..d7fa9c0 100644 --- a/provider.go +++ b/provider.go @@ -3,17 +3,18 @@ package config import "context" type Provider interface { - Read(ctx context.Context, key Key) (Variable, error) + Value(ctx context.Context, path ...string) (Value, error) } -type WatchCallback func(ctx context.Context, oldVar, newVar Variable) - -type WatchProvider interface { - Watch(ctx context.Context, key Key, callback WatchCallback) error +type NamedProvider interface { + Name() string + Provider } -type ReadConfig interface { - Value(ctx context.Context, name string) (Value, error) +type WatchCallback func(ctx context.Context, oldVar, newVar Value) + +type WatchProvider interface { + Watch(ctx context.Context, callback WatchCallback, path ...string) error } -type Factory func(ctx context.Context, cfg ReadConfig) (Provider, error) +type Factory func(ctx context.Context, cfg Provider) (Provider, error) diff --git a/provider/arg/provider.go b/provider/arg/provider.go index e106f05..620ea8d 100644 --- a/provider/arg/provider.go +++ b/provider/arg/provider.go @@ -7,23 +7,27 @@ import ( "strings" "gitoa.ru/go-4devs/config" - "gitoa.ru/go-4devs/config/key" "gitoa.ru/go-4devs/config/value" "gopkg.in/yaml.v3" ) +const Name = "arg" + var _ config.Provider = (*Provider)(nil) type Option func(*Provider) -func WithKeyFactory(factory config.KeyFactory) Option { +func WithKeyFactory(factory func(s ...string) string) Option { return func(p *Provider) { p.key = factory } } func New(opts ...Option) *Provider { prov := Provider{ - key: key.Name, + key: func(s ...string) string { + return strings.Join(s, "-") + }, args: make(map[string][]string, len(os.Args[1:])), + name: Name, } for _, opt := range opts { @@ -35,7 +39,8 @@ func New(opts ...Option) *Provider { type Provider struct { args map[string][]string - key config.KeyFactory + key func(...string) string + name string } // nolint: cyclop @@ -98,45 +103,29 @@ func (p *Provider) parse() error { } func (p *Provider) Name() string { - return "arg" -} - -func (p *Provider) IsSupport(ctx context.Context, key config.Key) bool { - return p.key(ctx, key) != "" + return p.name } -func (p *Provider) Read(ctx context.Context, key config.Key) (config.Variable, error) { +func (p *Provider) Value(ctx context.Context, path ...string) (config.Value, error) { if err := p.parse(); err != nil { - return config.Variable{ - Name: "", - Value: nil, - Provider: p.Name(), - }, err + return nil, err } - name := p.key(ctx, key) + name := p.key(path...) if val, ok := p.args[name]; ok { switch { case len(val) == 1: - return config.Variable{ - Name: name, - Provider: p.Name(), - Value: value.JString(val[0]), - }, nil + return value.JString(val[0]), nil default: var yNode yaml.Node if err := yaml.Unmarshal([]byte("["+strings.Join(val, ",")+"]"), &yNode); err != nil { - return config.Variable{}, fmt.Errorf("arg: failed unmarshal yaml:%w", err) + return nil, fmt.Errorf("arg: failed unmarshal yaml:%w", err) } - return config.Variable{ - Name: name, - Provider: p.Name(), - Value: value.Decode(yNode.Decode), - }, nil + return value.Decode(yNode.Decode), nil } } - return config.Variable{}, fmt.Errorf("%w: %s", config.ErrVariableNotFound, name) + return nil, fmt.Errorf("%s:%w", p.Name(), config.ErrValueNotFound) } diff --git a/provider/arg/provider_test.go b/provider/arg/provider_test.go index 44216a5..a27cf2e 100644 --- a/provider/arg/provider_test.go +++ b/provider/arg/provider_test.go @@ -31,15 +31,15 @@ func TestProvider(t *testing.T) { "--end-after=2008-01-02T15:04:05+03:00", } read := []test.Read{ - test.NewRead("listen", 8080), - test.NewRead("config", "config.hcl"), - test.NewRead("start-at", test.Time("2010-01-02T15:04:05Z")), - test.NewReadUnmarshal("url", &[]string{"http://4devs.io", "https://4devs.io"}, &[]string{}), - test.NewReadUnmarshal("timeout", &[]time.Duration{time.Minute, time.Hour}, &[]time.Duration{}), - test.NewReadUnmarshal("end-after", &[]time.Time{ + test.NewRead(8080, "listen"), + test.NewRead("config.hcl", "config"), + test.NewRead(test.Time("2010-01-02T15:04:05Z"), "start-at"), + test.NewReadUnmarshal(&[]string{"http://4devs.io", "https://4devs.io"}, &[]string{}, "url"), + test.NewReadUnmarshal(&[]time.Duration{time.Minute, time.Hour}, &[]time.Duration{}, "timeout"), + test.NewReadUnmarshal(&[]time.Time{ test.Time("2009-01-02T15:04:05Z"), test.Time("2008-01-02T15:04:05+03:00"), - }, &[]time.Time{}), + }, &[]time.Time{}, "end-after"), } prov := arg.New() diff --git a/provider/env/provider.go b/provider/env/provider.go index e55520a..b287b34 100644 --- a/provider/env/provider.go +++ b/provider/env/provider.go @@ -2,27 +2,30 @@ package env import ( "context" + "fmt" "os" "strings" "gitoa.ru/go-4devs/config" - "gitoa.ru/go-4devs/config/key" "gitoa.ru/go-4devs/config/value" ) +const Name = "env" + var _ config.Provider = (*Provider)(nil) type Option func(*Provider) -func WithKeyFactory(factory config.KeyFactory) Option { +func WithKeyFactory(factory func(...string) string) Option { return func(p *Provider) { p.key = factory } } -func New(opts ...Option) *Provider { +func New(namespace, appName string, opts ...Option) *Provider { provider := Provider{ - key: func(ctx context.Context, k config.Key) string { - return strings.ToUpper(key.NsAppName("_")(ctx, k)) + key: func(path ...string) string { + return strings.ToUpper(strings.Join(path, "_")) }, + prefix: strings.ToUpper(namespace + "_" + appName + "_"), } for _, opt := range opts { @@ -33,26 +36,20 @@ func New(opts ...Option) *Provider { } type Provider struct { - key config.KeyFactory + key func(...string) string + name string + prefix string } func (p *Provider) Name() string { - return "env" -} - -func (p *Provider) IsSupport(ctx context.Context, key config.Key) bool { - return p.key(ctx, key) != "" + return p.name } -func (p *Provider) Read(ctx context.Context, key config.Key) (config.Variable, error) { - name := p.key(ctx, key) +func (p *Provider) Value(ctx context.Context, path ...string) (config.Value, error) { + name := p.prefix + p.key(path...) if val, ok := os.LookupEnv(name); ok { - return config.Variable{ - Name: name, - Provider: p.Name(), - Value: value.JString(val), - }, nil + return value.JString(val), nil } - return config.Variable{}, config.ErrVariableNotFound + return nil, fmt.Errorf("%v:%w", p.Name(), config.ErrValueNotFound) } diff --git a/provider/env/provider_test.go b/provider/env/provider_test.go index 4096718..501f86a 100644 --- a/provider/env/provider_test.go +++ b/provider/env/provider_test.go @@ -14,11 +14,11 @@ func TestProvider(t *testing.T) { os.Setenv("FDEVS_CONFIG_DSN", test.DSN) os.Setenv("FDEVS_CONFIG_PORT", "8080") - provider := env.New() + provider := env.New("fdevs", "config") read := []test.Read{ - test.NewRead("dsn", test.DSN), - test.NewRead("port", 8080), + test.NewRead(test.DSN, "dsn"), + test.NewRead(8080, "port"), } test.Run(t, provider, read) } diff --git a/provider/etcd/provider.go b/provider/etcd/provider.go index 91d187d..5e2a848 100644 --- a/provider/etcd/provider.go +++ b/provider/etcd/provider.go @@ -3,14 +3,19 @@ package etcd import ( "context" "fmt" + "strings" "gitoa.ru/go-4devs/config" - "gitoa.ru/go-4devs/config/key" "gitoa.ru/go-4devs/config/value" pb "go.etcd.io/etcd/api/v3/mvccpb" client "go.etcd.io/etcd/client/v3" ) +const ( + Name = "etcd" + Separator = "/" +) + var ( _ config.Provider = (*Provider)(nil) _ config.WatchProvider = (*Provider)(nil) @@ -21,10 +26,14 @@ type Client interface { client.Watcher } -func NewProvider(client Client) *Provider { +func NewProvider(namespace, appName string, client Client) *Provider { p := Provider{ client: client, - key: key.NsAppName("/"), + key: func(s ...string) string { + return strings.Join(s, Separator) + }, + name: Name, + prefix: namespace + Separator + appName, } return &p @@ -32,34 +41,35 @@ func NewProvider(client Client) *Provider { type Provider struct { client Client - key config.KeyFactory -} - -func (p *Provider) IsSupport(ctx context.Context, key config.Key) bool { - return p.key(ctx, key) != "" + key func(...string) string + name string + prefix string } func (p *Provider) Name() string { - return "etcd" + return p.name +} +func (p *Provider) Key(s []string) string { + return p.prefix + Separator + p.key(s...) } -func (p *Provider) Read(ctx context.Context, key config.Key) (config.Variable, error) { - name := p.key(ctx, key) +func (p *Provider) Value(ctx context.Context, path ...string) (config.Value, error) { + name := p.Key(path) resp, err := p.client.Get(ctx, name, client.WithPrefix()) if err != nil { - return config.Variable{}, fmt.Errorf("%w: key:%s, prov:%s", err, name, p.Name()) + return nil, fmt.Errorf("%w: key:%s, prov:%s", err, name, p.Name()) } val, err := p.resolve(name, resp.Kvs) if err != nil { - return config.Variable{}, fmt.Errorf("%w: key:%s, prov:%s", err, name, p.Name()) + return nil, fmt.Errorf("%w: key:%s, prov:%s", err, name, p.Name()) } return val, nil } -func (p *Provider) Watch(ctx context.Context, key config.Key, callback config.WatchCallback) error { +func (p *Provider) Watch(ctx context.Context, callback config.WatchCallback, path ...string) error { go func(ctx context.Context, key string, callback config.WatchCallback) { watch := p.client.Watch(ctx, key, client.WithPrevKV(), client.WithPrefix()) for w := range watch { @@ -70,7 +80,7 @@ func (p *Provider) Watch(ctx context.Context, key config.Key, callback config.Wa callback(ctx, oldVar, newVar) } } - }(ctx, p.key(ctx, key), callback) + }(ctx, p.Key(path), callback) return nil } @@ -87,23 +97,15 @@ func (p *Provider) getEventKvs(events []*client.Event) ([]*pb.KeyValue, []*pb.Ke return kvs, old } -func (p *Provider) resolve(key string, kvs []*pb.KeyValue) (config.Variable, error) { +func (p *Provider) resolve(key string, kvs []*pb.KeyValue) (config.Value, error) { for _, kv := range kvs { switch { case kv == nil: - return config.Variable{ - Name: key, - Provider: p.Name(), - Value: nil, - }, nil + return nil, nil case string(kv.Key) == key: - return config.Variable{ - Value: value.JBytes(kv.Value), - Name: key, - Provider: p.Name(), - }, nil + return value.JBytes(kv.Value), nil } } - return config.Variable{}, fmt.Errorf("%w: name %s", config.ErrVariableNotFound, key) + return nil, fmt.Errorf("%w: name %s", config.ErrValueNotFound, key) } diff --git a/provider/etcd/provider_test.go b/provider/etcd/provider_test.go index c547c71..82021e9 100644 --- a/provider/etcd/provider_test.go +++ b/provider/etcd/provider_test.go @@ -23,17 +23,17 @@ func TestProvider(t *testing.T) { et, err := test.NewEtcd(ctx) require.NoError(t, err) - provider := etcd.NewProvider(et) + provider := etcd.NewProvider("fdevs", "config", et) read := []test.Read{ - test.NewRead("db_dsn", test.DSN), - test.NewRead("duration", 12*time.Minute), - test.NewRead("port", 8080), - test.NewRead("maintain", true), - test.NewRead("start_at", test.Time("2020-01-02T15:04:05Z")), - test.NewRead("percent", .064), - test.NewRead("count", uint(2020)), - test.NewRead("int64", int64(2021)), - test.NewRead("uint64", int64(2022)), + test.NewRead(test.DSN, "db_dsn"), + test.NewRead(12*time.Minute, "duration"), + test.NewRead(8080, "port"), + test.NewRead(true, "maintain"), + test.NewRead(test.Time("2020-01-02T15:04:05Z"), "start_at"), + test.NewRead(.064, "percent"), + test.NewRead(uint(2020), "count"), + test.NewRead(int64(2021), "int64"), + test.NewRead(int64(2022), "uint64"), test.NewReadConfig("config"), } test.Run(t, provider, read) @@ -49,11 +49,7 @@ func TestWatcher(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - key := config.Key{ - AppName: "config", - Namespace: "fdevs", - Name: "test_watch", - } + key := "test_watch" et, err := test.NewEtcd(ctx) require.NoError(t, err) @@ -65,24 +61,24 @@ func TestWatcher(t *testing.T) { var cnt, cnt2 int32 - prov := etcd.NewProvider(et) + prov := etcd.NewProvider("fdevs", "config", et) wg := sync.WaitGroup{} wg.Add(6) - watch := func(cnt *int32) func(ctx context.Context, oldVar, newVar config.Variable) { - return func(ctx context.Context, oldVar, newVar config.Variable) { + watch := func(cnt *int32) func(ctx context.Context, oldVar, newVar config.Value) { + return func(ctx context.Context, oldVar, newVar config.Value) { switch *cnt { case 0: - assert.Equal(t, value(*cnt), newVar.Value.String()) - assert.Nil(t, oldVar.Value) + assert.Equal(t, value(*cnt), newVar.String()) + assert.Nil(t, oldVar) case 1: - assert.Equal(t, value(*cnt), newVar.Value.String()) - assert.Equal(t, value(*cnt-1), oldVar.Value.String()) + assert.Equal(t, value(*cnt), newVar.String()) + assert.Equal(t, value(*cnt-1), oldVar.String()) case 2: - _, perr := newVar.Value.ParseString() + _, perr := newVar.ParseString() assert.NoError(t, perr) - assert.Equal(t, "", newVar.Value.String()) - assert.Equal(t, value(*cnt-1), oldVar.Value.String()) + assert.Equal(t, "", newVar.String()) + assert.Equal(t, value(*cnt-1), oldVar.String()) default: assert.Fail(t, "unexpected watch") } @@ -92,8 +88,8 @@ func TestWatcher(t *testing.T) { } } - err = prov.Watch(ctx, key, watch(&cnt)) - err = prov.Watch(ctx, key, watch(&cnt2)) + err = prov.Watch(ctx, watch(&cnt), key) + err = prov.Watch(ctx, watch(&cnt2), key) require.NoError(t, err) time.AfterFunc(time.Second, func() { diff --git a/provider/ini/provider.go b/provider/ini/provider.go index e06c15b..b7b048c 100644 --- a/provider/ini/provider.go +++ b/provider/ini/provider.go @@ -10,55 +10,49 @@ import ( "gopkg.in/ini.v1" ) +const ( + Name = "ini" + Separator = "." +) + var _ config.Provider = (*Provider)(nil) func New(data *ini.File) *Provider { - const nameParts = 2 - return &Provider{ data: data, - resolve: func(ctx context.Context, key config.Key) (string, string) { - keys := strings.SplitN(key.Name, "/", nameParts) - if len(keys) == 1 { - return "", keys[0] + resolve: func(path []string) (string, string) { + if len(path) == 1 { + return "", path[0] } - return keys[0], keys[1] + return strings.Join(path[:len(path)-1], Separator), strings.ToUpper(path[len(path)-1]) }, + name: Name, } } type Provider struct { data *ini.File - resolve func(ctx context.Context, key config.Key) (string, string) -} - -func (p *Provider) IsSupport(ctx context.Context, key config.Key) bool { - section, name := p.resolve(ctx, key) - - return section != "" && name != "" + resolve func(path []string) (string, string) + name string } func (p *Provider) Name() string { - return "ini" + return p.name } -func (p *Provider) Read(ctx context.Context, key config.Key) (config.Variable, error) { - section, name := p.resolve(ctx, key) +func (p *Provider) Value(ctx context.Context, path ...string) (config.Value, error) { + section, name := p.resolve(path) iniSection, err := p.data.GetSection(section) if err != nil { - return config.Variable{}, fmt.Errorf("%w: %s: %w", config.ErrVariableNotFound, p.Name(), err) + return nil, fmt.Errorf("%w: %s: %w", config.ErrValueNotFound, p.Name(), err) } iniKey, err := iniSection.GetKey(name) if err != nil { - return config.Variable{}, fmt.Errorf("%w: %s: %w", config.ErrVariableNotFound, p.Name(), err) + return nil, fmt.Errorf("%w: %s: %w", config.ErrValueNotFound, p.Name(), err) } - return config.Variable{ - Name: section + ":" + name, - Provider: p.Name(), - Value: value.JString(iniKey.String()), - }, nil + return value.JString(iniKey.String()), nil } diff --git a/provider/ini/provider_test.go b/provider/ini/provider_test.go index 3d1572d..2a74152 100644 --- a/provider/ini/provider_test.go +++ b/provider/ini/provider_test.go @@ -14,13 +14,13 @@ func TestProvider(t *testing.T) { file := test.NewINI() read := []test.Read{ - test.NewRead("project/PROJECT_BOARD_BASIC_KANBAN_TYPE", "To Do, In Progress, Done"), - test.NewRead("repository.editor/PREVIEWABLE_FILE_MODES", "markdown"), - test.NewRead("server/LOCAL_ROOT_URL", "http://0.0.0.0:3000/"), - test.NewRead("server/LFS_HTTP_AUTH_EXPIRY", 20*time.Minute), - test.NewRead("repository.pull-request/DEFAULT_MERGE_MESSAGE_SIZE", 5120), - test.NewRead("ui/SHOW_USER_EMAIL", true), - test.NewRead("cors/ENABLED", false), + test.NewRead("To Do, In Progress, Done", "project", "PROJECT_BOARD_BASIC_KANBAN_TYPE"), + test.NewRead("markdown", "repository.editor", "PREVIEWABLE_FILE_MODES"), + test.NewRead("http://0.0.0.0:3000/", "server", "LOCAL_ROOT_URL"), + test.NewRead(20*time.Minute, "server", "LFS_HTTP_AUTH_EXPIRY"), + test.NewRead(5120, "repository.pull-request", "DEFAULT_MERGE_MESSAGE_SIZE"), + test.NewRead(true, "ui", "SHOW_USER_EMAIL"), + test.NewRead(false, "cors", "enabled"), } prov := ini.New(file) diff --git a/provider/json/provider.go b/provider/json/provider.go index 13d1647..69eae81 100644 --- a/provider/json/provider.go +++ b/provider/json/provider.go @@ -3,20 +3,27 @@ package json import ( "context" "fmt" - "io/ioutil" + "os" "path/filepath" + "strings" "github.com/tidwall/gjson" "gitoa.ru/go-4devs/config" - "gitoa.ru/go-4devs/config/key" "gitoa.ru/go-4devs/config/value" ) +const ( + Name = "json" + Separator = "." +) + var _ config.Provider = (*Provider)(nil) func New(json []byte, opts ...Option) *Provider { provider := Provider{ - key: key.Name, + key: func(s ...string) string { + return strings.Join(s, Separator) + }, data: json, } @@ -28,7 +35,7 @@ func New(json []byte, opts ...Option) *Provider { } func NewFile(path string, opts ...Option) (*Provider, error) { - file, err := ioutil.ReadFile(filepath.Clean(path)) + file, err := os.ReadFile(filepath.Clean(path)) if err != nil { return nil, fmt.Errorf("%w: unable to read config file %#q: file not found or unreadable", err, path) } @@ -40,27 +47,19 @@ type Option func(*Provider) type Provider struct { data []byte - key config.KeyFactory -} - -func (p *Provider) IsSupport(ctx context.Context, key config.Key) bool { - return p.key(ctx, key) != "" + key func(...string) string + name string } func (p *Provider) Name() string { - return "json" + return p.name } -func (p *Provider) Read(ctx context.Context, key config.Key) (config.Variable, error) { - path := p.key(ctx, key) - - if val := gjson.GetBytes(p.data, path); val.Exists() { - return config.Variable{ - Name: path, - Provider: p.Name(), - Value: value.JString(val.String()), - }, nil +func (p *Provider) Value(ctx context.Context, path ...string) (config.Value, error) { + key := p.key(path...) + if val := gjson.GetBytes(p.data, key); val.Exists() { + return value.JString(val.String()), nil } - return config.Variable{}, config.ErrVariableNotFound + return nil, fmt.Errorf("%v:%w", p.Name(), config.ErrValueNotFound) } diff --git a/provider/json/provider_test.go b/provider/json/provider_test.go index c5fa00c..68457c2 100644 --- a/provider/json/provider_test.go +++ b/provider/json/provider_test.go @@ -16,11 +16,11 @@ func TestProvider(t *testing.T) { prov := provider.New(js) sl := []string{} read := []test.Read{ - test.NewRead("app.name.title", "config title"), - test.NewRead("app.name.timeout", time.Minute), - test.NewReadUnmarshal("app.name.var", &[]string{"name"}, &sl), + test.NewRead("config title", "app.name.title"), + test.NewRead(time.Minute, "app.name.timeout"), + test.NewReadUnmarshal(&[]string{"name"}, &sl, "app.name.var"), test.NewReadConfig("cfg"), - test.NewRead("app.name.success", true), + test.NewRead(true, "app", "name", "success"), } test.Run(t, prov, read) diff --git a/provider/toml/provider.go b/provider/toml/provider.go index 6129413..073aa8b 100644 --- a/provider/toml/provider.go +++ b/provider/toml/provider.go @@ -3,13 +3,18 @@ package toml import ( "context" "fmt" + "strings" "github.com/pelletier/go-toml" "gitoa.ru/go-4devs/config" - "gitoa.ru/go-4devs/config/key" "gitoa.ru/go-4devs/config/value" ) +const ( + Name = "toml" + Separator = "." +) + var _ config.Provider = (*Provider)(nil) func NewFile(file string, opts ...Option) (*Provider, error) { @@ -26,7 +31,9 @@ type Option func(*Provider) func configure(tree *toml.Tree, opts ...Option) *Provider { prov := &Provider{ tree: tree, - key: key.Name, + key: func(s []string) string { + return strings.Join(s, Separator) + }, } for _, opt := range opts { @@ -47,25 +54,18 @@ func New(data []byte, opts ...Option) (*Provider, error) { type Provider struct { tree *toml.Tree - key config.KeyFactory -} - -func (p *Provider) IsSupport(ctx context.Context, key config.Key) bool { - return p.key(ctx, key) != "" + key func([]string) string + name string } func (p *Provider) Name() string { - return "toml" + return p.name } -func (p *Provider) Read(ctx context.Context, key config.Key) (config.Variable, error) { - if k := p.key(ctx, key); p.tree.Has(k) { - return config.Variable{ - Name: k, - Provider: p.Name(), - Value: Value{Value: value.Value{Val: p.tree.Get(k)}}, - }, nil +func (p *Provider) Value(ctx context.Context, path ...string) (config.Value, error) { + if k := p.key(path); p.tree.Has(k) { + return Value{Value: value.Value{Val: p.tree.Get(k)}}, nil } - return config.Variable{}, config.ErrVariableNotFound + return nil, config.ErrValueNotFound } diff --git a/provider/toml/provider_test.go b/provider/toml/provider_test.go index 98cfe02..89c3abe 100644 --- a/provider/toml/provider_test.go +++ b/provider/toml/provider_test.go @@ -17,12 +17,12 @@ func TestProvider(t *testing.T) { m := []int{} read := []test.Read{ - test.NewRead("database.server", "192.168.1.1"), - test.NewRead("title", "TOML Example"), - test.NewRead("servers.alpha.ip", "10.0.0.1"), - test.NewRead("database.enabled", true), - test.NewRead("database.connection_max", 5000), - test.NewReadUnmarshal("database.ports", &[]int{8001, 8001, 8002}, &m), + test.NewRead("192.168.1.1", "database.server"), + test.NewRead("TOML Example", "title"), + test.NewRead("10.0.0.1", "servers.alpha.ip"), + test.NewRead(true, "database.enabled"), + test.NewRead(5000, "database.connection_max"), + test.NewReadUnmarshal(&[]int{8001, 8001, 8002}, &m, "database", "ports"), } test.Run(t, prov, read) diff --git a/provider/vault/secret.go b/provider/vault/secret.go index 0e56ce4..1d1a650 100644 --- a/provider/vault/secret.go +++ b/provider/vault/secret.go @@ -4,25 +4,42 @@ import ( "context" "encoding/json" "fmt" + "log" + "strings" "github.com/hashicorp/vault/api" "gitoa.ru/go-4devs/config" - "gitoa.ru/go-4devs/config/key" "gitoa.ru/go-4devs/config/value" ) +const ( + Name = "vault" + Separator = "/" + Prefix = "secret/data/" + ValueName = "value" +) + var _ config.Provider = (*SecretKV2)(nil) type SecretOption func(*SecretKV2) -func WithSecretResolve(f func(context.Context, config.Key) (string, string)) SecretOption { +func WithSecretResolve(f func(key []string) (string, string)) SecretOption { return func(s *SecretKV2) { s.resolve = f } } -func NewSecretKV2(client *api.Client, opts ...SecretOption) *SecretKV2 { +func NewSecretKV2(namespace, appName string, client *api.Client, opts ...SecretOption) *SecretKV2 { prov := SecretKV2{ - client: client, - resolve: key.LastIndexField(":", "value", key.PrefixName("secret/data/", key.NsAppName("/"))), + client: client, + resolve: func(key []string) (string, string) { + keysLen := len(key) + if keysLen == 1 { + return "", key[0] + } + + return strings.Join(key[:keysLen-1], Separator), key[keysLen-1] + }, + name: Name, + prefix: Prefix + namespace + Separator + appName, } for _, opt := range opts { @@ -34,57 +51,69 @@ func NewSecretKV2(client *api.Client, opts ...SecretOption) *SecretKV2 { type SecretKV2 struct { client *api.Client - resolve func(ctx context.Context, key config.Key) (string, string) + resolve func(key []string) (string, string) + name string + prefix string } -func (p *SecretKV2) IsSupport(ctx context.Context, key config.Key) bool { - path, _ := p.resolve(ctx, key) +func (p *SecretKV2) Name() string { + return p.name +} +func (p *SecretKV2) Key(in []string) (string, string) { + path, val := p.resolve(in) + if path == "" { + return p.prefix, val + } - return path != "" + return p.prefix + Separator + path, val } +func (p *SecretKV2) read(path, key string) (*api.Secret, error) { + secret, err := p.client.Logical().Read(path) + if err != nil { + return nil, err + } + if secret == nil && key != ValueName { + return p.read(path+Separator+key, ValueName) + } -func (p *SecretKV2) Name() string { - return "vault" + return secret, err } -func (p *SecretKV2) Read(ctx context.Context, key config.Key) (config.Variable, error) { - path, field := p.resolve(ctx, key) +func (p *SecretKV2) Value(ctx context.Context, key ...string) (config.Value, error) { + path, field := p.Key(key) - secret, err := p.client.Logical().Read(path) + secret, err := p.read(path, field) if err != nil { - return config.Variable{}, fmt.Errorf("%w: path:%s, field:%s, provider:%s", err, path, field, p.Name()) + return nil, fmt.Errorf("%w: path:%s, field:%s, provider:%s", err, path, field, p.Name()) } if secret == nil || len(secret.Data) == 0 { - return config.Variable{}, fmt.Errorf("%w: path:%s, field:%s, provider:%s", config.ErrVariableNotFound, path, field, p.Name()) + log.Println(secret == nil) + return nil, fmt.Errorf("%w: path:%s, field:%s, provider:%s", config.ErrValueNotFound, path, field, p.Name()) } if len(secret.Warnings) > 0 { - return config.Variable{}, - fmt.Errorf("%w: warn: %s, path:%s, field:%s, provider:%s", config.ErrVariableNotFound, secret.Warnings, path, field, p.Name()) + return nil, + fmt.Errorf("%w: warn: %s, path:%s, field:%s, provider:%s", config.ErrValueNotFound, secret.Warnings, path, field, p.Name()) } data, ok := secret.Data["data"].(map[string]interface{}) if !ok { - return config.Variable{}, fmt.Errorf("%w: path:%s, field:%s, provider:%s", config.ErrVariableNotFound, path, field, p.Name()) + return nil, fmt.Errorf("%w: path:%s, field:%s, provider:%s", config.ErrValueNotFound, path, field, p.Name()) } if val, ok := data[field]; ok { - return config.Variable{ - Name: path + field, - Provider: p.Name(), - Value: value.JString(fmt.Sprint(val)), - }, nil + return value.JString(fmt.Sprint(val)), nil + } + + if val, ok := data[ValueName]; ok { + return value.JString(fmt.Sprint(val)), nil } md, err := json.Marshal(data) if err != nil { - return config.Variable{}, fmt.Errorf("%w: %w", config.ErrInvalidValue, err) + return nil, fmt.Errorf("%w: %w", config.ErrInvalidValue, err) } - return config.Variable{ - Name: path + field, - Provider: p.Name(), - Value: value.JBytes(md), - }, nil + return value.JBytes(md), nil } diff --git a/provider/vault/secret_test.go b/provider/vault/secret_test.go index 330286f..0501904 100644 --- a/provider/vault/secret_test.go +++ b/provider/vault/secret_test.go @@ -15,12 +15,12 @@ func TestProvider(t *testing.T) { cl, err := test.NewVault() require.NoError(t, err) - provider := vault.NewSecretKV2(cl) + provider := vault.NewSecretKV2("fdevs", "config", cl) read := []test.Read{ test.NewReadConfig("database"), - test.NewRead("db:dsn", test.DSN), - test.NewRead("db:timeout", time.Minute), + test.NewRead(test.DSN, "db", "dsn"), + test.NewRead(time.Minute, "db", "timeout"), } test.Run(t, provider, read) } diff --git a/provider/watcher/provider.go b/provider/watcher/provider.go index fece95c..b821c3d 100644 --- a/provider/watcher/provider.go +++ b/provider/watcher/provider.go @@ -14,10 +14,10 @@ var ( _ config.WatchProvider = (*Provider)(nil) ) -func New(duration time.Duration, provider config.Provider, opts ...Option) *Provider { +func New(duration time.Duration, provider config.NamedProvider, opts ...Option) *Provider { prov := &Provider{ - Provider: provider, - ticker: time.NewTicker(duration), + NamedProvider: provider, + ticker: time.NewTicker(duration), logger: func(_ context.Context, msg string) { log.Print(msg) }, @@ -39,13 +39,13 @@ func WithLogger(l func(context.Context, string)) Option { type Option func(*Provider) type Provider struct { - config.Provider + config.NamedProvider ticker *time.Ticker logger func(context.Context, string) } -func (p *Provider) Watch(ctx context.Context, key config.Key, callback config.WatchCallback) error { - oldVar, err := p.Provider.Read(ctx, key) +func (p *Provider) Watch(ctx context.Context, callback config.WatchCallback, key ...string) error { + oldVar, err := p.NamedProvider.Value(ctx, key...) if err != nil { return fmt.Errorf("failed watch variable: %w", err) } @@ -54,7 +54,7 @@ func (p *Provider) Watch(ctx context.Context, key config.Key, callback config.Wa for { select { case <-p.ticker.C: - newVar, err := p.Provider.Read(ctx, key) + newVar, err := p.NamedProvider.Value(ctx, key...) if err != nil { p.logger(ctx, err.Error()) } else if !newVar.IsEquals(oldVar) { diff --git a/provider/watcher/provider_test.go b/provider/watcher/provider_test.go index 47611c9..d4d089f 100644 --- a/provider/watcher/provider_test.go +++ b/provider/watcher/provider_test.go @@ -22,14 +22,10 @@ func (p *provider) Name() string { return "test" } -func (p *provider) Read(context.Context, config.Key) (config.Variable, error) { +func (p *provider) Value(context.Context, ...string) (config.Value, error) { p.cnt++ - return config.Variable{ - Name: "tmpname", - Provider: p.Name(), - Value: value.JString(fmt.Sprint(p.cnt)), - }, nil + return value.JString(fmt.Sprint(p.cnt)), nil } func TestWatcher(t *testing.T) { @@ -46,11 +42,11 @@ func TestWatcher(t *testing.T) { err := w.Watch( ctx, - config.Key{Name: "tmpname"}, - func(ctx context.Context, oldVar, newVar config.Variable) { + func(ctx context.Context, oldVar, newVar config.Value) { atomic.AddInt32(&cnt, 1) wg.Done() }, + "tmpname", ) require.NoError(t, err) wg.Wait() diff --git a/provider/yaml/provider.go b/provider/yaml/provider.go index 7e173a1..eefc5f0 100644 --- a/provider/yaml/provider.go +++ b/provider/yaml/provider.go @@ -4,22 +4,21 @@ import ( "context" "errors" "fmt" - "io/ioutil" - "strings" + "os" "gitoa.ru/go-4devs/config" "gitoa.ru/go-4devs/config/value" "gopkg.in/yaml.v3" ) -var _ config.Provider = (*Provider)(nil) +const ( + Name = "yaml" +) -func keyFactory(_ context.Context, key config.Key) []string { - return strings.Split(key.Name, "/") -} +var _ config.Provider = (*Provider)(nil) func NewFile(name string, opts ...Option) (*Provider, error) { - in, err := ioutil.ReadFile(name) + in, err := os.ReadFile(name) if err != nil { return nil, fmt.Errorf("yaml_file: read error: %w", err) } @@ -38,7 +37,7 @@ func New(yml []byte, opts ...Option) (*Provider, error) { func create(opts ...Option) *Provider { prov := Provider{ - key: keyFactory, + name: Name, } for _, opt := range opts { @@ -52,22 +51,20 @@ type Option func(*Provider) type Provider struct { data node - key func(context.Context, config.Key) []string + name string } func (p *Provider) Name() string { - return "yaml" + return p.name } -func (p *Provider) Read(ctx context.Context, key config.Key) (config.Variable, error) { - k := p.key(ctx, key) +func (p *Provider) Value(_ context.Context, path ...string) (config.Value, error) { - return p.data.read(p.Name(), k) + return p.data.read(p.Name(), path) } func (p *Provider) With(data *yaml.Node) *Provider { return &Provider{ - key: p.key, data: node{Node: data}, } } @@ -76,21 +73,17 @@ type node struct { *yaml.Node } -func (n *node) read(name string, keys []string) (config.Variable, error) { +func (n *node) read(name string, keys []string) (config.Value, error) { val, err := getData(n.Node.Content[0].Content, keys) if err != nil { - if errors.Is(err, config.ErrVariableNotFound) { - return config.Variable{}, fmt.Errorf("%w: %s", config.ErrVariableNotFound, name) + if errors.Is(err, config.ErrValueNotFound) { + return nil, fmt.Errorf("%w: %s", config.ErrValueNotFound, name) } - return config.Variable{}, fmt.Errorf("%w: %s", err, name) + return nil, fmt.Errorf("%w: %s", err, name) } - return config.Variable{ - Name: strings.Join(keys, "."), - Provider: name, - Value: value.Decode(val), - }, nil + return value.Decode(val), nil } func getData(node []*yaml.Node, keys []string) (func(interface{}) error, error) { @@ -104,5 +97,5 @@ func getData(node []*yaml.Node, keys []string) (func(interface{}) error, error) } } - return nil, config.ErrVariableNotFound + return nil, config.ErrValueNotFound } diff --git a/provider/yaml/provider_test.go b/provider/yaml/provider_test.go index c4e970c..e5105db 100644 --- a/provider/yaml/provider_test.go +++ b/provider/yaml/provider_test.go @@ -16,9 +16,9 @@ func TestProvider(t *testing.T) { require.Nil(t, err) read := []test.Read{ - test.NewRead("duration_var", 21*time.Minute), - test.NewRead("app/name/bool_var", true), - test.NewRead("time_var", test.Time("2020-01-02T15:04:05Z")), + test.NewRead(21*time.Minute, "duration_var"), + test.NewRead(true, "app", "name", "bool_var"), + test.NewRead(test.Time("2020-01-02T15:04:05Z"), "time_var"), test.NewReadConfig("cfg"), } diff --git a/provider/yaml/watch.go b/provider/yaml/watch.go index 2da862a..a88fc67 100644 --- a/provider/yaml/watch.go +++ b/provider/yaml/watch.go @@ -3,16 +3,19 @@ package yaml import ( "context" "fmt" - "io/ioutil" + "os" "gitoa.ru/go-4devs/config" "gopkg.in/yaml.v3" ) +const NameWatch = "yaml_watch" + func NewWatch(name string, opts ...Option) *Watch { f := Watch{ file: name, prov: create(opts...), + name: NameWatch, } return &f @@ -21,22 +24,23 @@ func NewWatch(name string, opts ...Option) *Watch { type Watch struct { file string prov *Provider + name string } func (p *Watch) Name() string { - return "yaml_watch" + return p.name } -func (p *Watch) Read(ctx context.Context, key config.Key) (config.Variable, error) { - in, err := ioutil.ReadFile(p.file) +func (p *Watch) Value(ctx context.Context, path ...string) (config.Value, error) { + in, err := os.ReadFile(p.file) if err != nil { - return config.Variable{}, fmt.Errorf("yaml_file: read error: %w", err) + return nil, fmt.Errorf("yaml_file: read error: %w", err) } var yNode yaml.Node if err = yaml.Unmarshal(in, &yNode); err != nil { - return config.Variable{}, fmt.Errorf("yaml_file: unmarshal error: %w", err) + return nil, fmt.Errorf("yaml_file: unmarshal error: %w", err) } - return p.prov.With(&yNode).Read(ctx, key) + return p.prov.With(&yNode).Value(ctx, path...) } diff --git a/test/provider_suite.go b/test/provider_suite.go index 7635c79..c71ae92 100644 --- a/test/provider_suite.go +++ b/test/provider_suite.go @@ -35,7 +35,7 @@ type ProviderSuite struct { } type Read struct { - Key config.Key + Key []string Assert func(t *testing.T, v config.Value) } @@ -46,22 +46,18 @@ type Config struct { Enabled bool } -func NewReadConfig(key string) Read { +func NewReadConfig(key ...string) Read { ex := &Config{ Duration: 21 * time.Minute, Enabled: true, } - return NewReadUnmarshal(key, ex, &Config{}) + return NewReadUnmarshal(ex, &Config{}, key...) } -func NewReadUnmarshal(key string, expected, target interface{}) Read { +func NewReadUnmarshal(expected, target interface{}, key ...string) Read { return Read{ - Key: config.Key{ - Namespace: "fdevs", - AppName: "config", - Name: key, - }, + Key: key, Assert: func(t *testing.T, v config.Value) { t.Helper() require.NoErrorf(t, v.Unmarshal(target), "unmarshal") @@ -77,13 +73,9 @@ func Time(value string) time.Time { } // nolint: cyclop -func NewRead(key string, expected interface{}) Read { +func NewRead(expected interface{}, key ...string) Read { return Read{ - Key: config.Key{ - Namespace: "fdevs", - AppName: "config", - Name: key, - }, + Key: key, Assert: func(t *testing.T, v config.Value) { t.Helper() var ( @@ -134,9 +126,9 @@ func (ps *ProviderSuite) TestReadKeys() { ctx := context.Background() for _, read := range ps.read { - val, err := ps.provider.Read(ctx, read.Key) - require.NoError(ps.T(), err, read.Key.String()) - read.Assert(ps.T(), val.Value) + val, err := ps.provider.Value(ctx, read.Key...) + require.NoError(ps.T(), err, read.Key) + read.Assert(ps.T(), val) } } diff --git a/value.go b/value.go index 8d9e86e..26cd231 100644 --- a/value.go +++ b/value.go @@ -8,6 +8,7 @@ type Value interface { ReadValue ParseValue UnmarshalValue + IsEquals(Value) bool } type UnmarshalValue interface { diff --git a/value/decode.go b/value/decode.go index 8258fda..eaf8a61 100644 --- a/value/decode.go +++ b/value/decode.go @@ -108,3 +108,7 @@ func (s Decode) Time() time.Time { return in } + +func (s Decode) IsEquals(in config.Value) bool { + return s.String() == in.String() +} diff --git a/value/jbytes.go b/value/jbytes.go index d500db3..051d637 100644 --- a/value/jbytes.go +++ b/value/jbytes.go @@ -110,3 +110,7 @@ func (s JBytes) Time() time.Time { return in } + +func (s JBytes) IsEquals(in config.Value) bool { + return s.String() == in.String() +} diff --git a/value/jstring.go b/value/jstring.go index fa80cb7..e9d752c 100644 --- a/value/jstring.go +++ b/value/jstring.go @@ -110,3 +110,7 @@ func (s JString) Time() time.Time { return in } + +func (s JString) IsEquals(in config.Value) bool { + return s.String() == in.String() +} diff --git a/value/value.go b/value/value.go index 727fd86..f2d5f79 100644 --- a/value/value.go +++ b/value/value.go @@ -156,3 +156,7 @@ func (s Value) ParseTime() (time.Time, error) { return time.Time{}, config.ErrInvalidValue } + +func (s Value) IsEquals(in config.Value) bool { + return s.String() == in.String() +}