Browse Source

Merge pull request 'add definition config' (#3) from def into master

Reviewed-on: https://gitoa.ru/go-4devs/config/pulls/3
pull/4/head
andrey 3 months ago
parent
commit
e90b154cd0
  1. 4
      .drone.yml
  2. 63
      client.go
  3. 62
      client_example_test.go
  4. 29
      definition/defenition.go
  5. 70
      definition/generate/generator.go
  6. 16
      definition/generate/helpers.go
  7. 87
      definition/generate/imports.go
  8. 38
      definition/generate/run.go
  9. 41
      definition/generate/template.go
  10. 60
      definition/generate/view.go
  11. 27
      definition/group/group.go
  12. 85
      definition/group/view.go
  13. 27
      definition/option.go
  14. 100
      definition/option/option.go
  15. 33
      definition/option/tpl/option.tmpl
  16. 3
      definition/option/tpl/parse.tmpl
  17. 8
      definition/option/tpl/unmarshal_json.tmpl
  18. 8
      definition/option/tpl/unmarshal_text.tmpl
  19. 226
      definition/option/view.go
  20. 6
      definition/option/view_params.go
  21. 27
      definition/proto/proto.go
  22. 76
      definition/proto/view.go
  23. 4
      docker-compose.yml
  24. 8
      error.go
  25. 41
      go.mod
  26. 5
      go.sum
  27. 17
      provider.go
  28. 45
      provider/arg/provider.go
  29. 14
      provider/arg/provider_test.go
  30. 35
      provider/env/provider.go
  31. 6
      provider/env/provider_test.go
  32. 56
      provider/etcd/provider.go
  33. 50
      provider/etcd/provider_test.go
  34. 42
      provider/ini/provider.go
  35. 14
      provider/ini/provider_test.go
  36. 39
      provider/json/provider.go
  37. 8
      provider/json/provider_test.go
  38. 32
      provider/toml/provider.go
  39. 12
      provider/toml/provider_test.go
  40. 89
      provider/vault/secret.go
  41. 6
      provider/vault/secret_test.go
  42. 14
      provider/watcher/provider.go
  43. 12
      provider/watcher/provider_test.go
  44. 41
      provider/yaml/provider.go
  45. 6
      provider/yaml/provider_test.go
  46. 18
      provider/yaml/watch.go
  47. 28
      test/provider_suite.go
  48. 1
      value.go
  49. 4
      value/decode.go
  50. 4
      value/jbytes.go
  51. 4
      value/jstring.go
  52. 4
      value/value.go

4
.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

63
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
}

62
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)

29
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
}

70
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)
}

16
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)
}

87
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
}

38
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
}

41
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)
}
`

60
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
}

27
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
}

85
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}
}
`

27
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
}

100
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...)
}

33
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
}

3
definition/option/tpl/parse.tmpl

@ -0,0 +1,3 @@
{{block "Parse" .}}
return {{.ValName}}.Parse{{ .FuncType}}()
{{end}}

8
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}}

8
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}}

226
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
}

6
definition/option/view_params.go

@ -0,0 +1,6 @@
package option
const (
ViewParamFunctName = "view.funcName"
ViewParamDescription = "view.description"
)

27
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
}

76
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
}
`

4
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:

8
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")
)

41
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
)

5
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=

17
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)

45
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)
}

14
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()

35
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)
}

6
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)
}

56
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)
}

50
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() {

42
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
}

14
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)

39
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)
}

8
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)

32
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
}

12
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)

89
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
}

6
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)
}

14
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) {

12
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()

41
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
}

6
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"),
}

18
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...)
}

28
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)
}
}

1
value.go

@ -8,6 +8,7 @@ type Value interface {
ReadValue
ParseValue
UnmarshalValue
IsEquals(Value) bool
}
type UnmarshalValue interface {

4
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()
}

4
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()
}

4
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()
}

4
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()
}

Loading…
Cancel
Save