add definition config
This commit is contained in:
@@ -3,12 +3,12 @@ name: default
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
- name: vault
|
- name: vault
|
||||||
image: vault:1.7.1
|
image: vault:1.13.3
|
||||||
environment:
|
environment:
|
||||||
VAULT_DEV_ROOT_TOKEN_ID: dev
|
VAULT_DEV_ROOT_TOKEN_ID: dev
|
||||||
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
|
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
|
||||||
- name: etcd
|
- name: etcd
|
||||||
image: bitnami/etcd:3
|
image: bitnami/etcd:3.5.11
|
||||||
environment:
|
environment:
|
||||||
ALLOW_NONE_AUTHENTICATION: yes
|
ALLOW_NONE_AUTHENTICATION: yes
|
||||||
|
|
||||||
|
|||||||
61
client.go
61
client.go
@@ -8,8 +8,8 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Must(namespace, appName string, providers ...interface{}) *Client {
|
func Must(providers ...interface{}) *Client {
|
||||||
client, err := New(namespace, appName, providers...)
|
client, err := New(providers...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -17,10 +17,8 @@ func Must(namespace, appName string, providers ...interface{}) *Client {
|
|||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(namespace, appName string, providers ...interface{}) (*Client, error) {
|
func New(providers ...interface{}) (*Client, error) {
|
||||||
client := &Client{
|
client := &Client{
|
||||||
namespace: namespace,
|
|
||||||
appName: appName,
|
|
||||||
providers: make([]Provider, len(providers)),
|
providers: make([]Provider, len(providers)),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +64,7 @@ func (p *provider) init(ctx context.Context) error {
|
|||||||
return nil
|
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 {
|
if err := p.init(ctx); err != nil {
|
||||||
return fmt.Errorf("init read:%w", err)
|
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
|
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 fmt.Errorf("factory provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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 {
|
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 {
|
if err != nil {
|
||||||
return Variable{}, fmt.Errorf("factory provider: %w", err)
|
return nil, fmt.Errorf("factory provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return variable, nil
|
return variable, nil
|
||||||
@@ -98,53 +96,34 @@ func (p *provider) Read(ctx context.Context, key Key) (Variable, error) {
|
|||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
providers []Provider
|
providers []Provider
|
||||||
appName string
|
|
||||||
namespace string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) key(name string) Key {
|
func (c *Client) Name() string {
|
||||||
return Key{
|
return "client"
|
||||||
Name: name,
|
|
||||||
AppName: c.appName,
|
|
||||||
Namespace: c.namespace,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Value get value by name.
|
// Value get value by name.
|
||||||
// nolint: ireturn
|
func (c *Client) Value(ctx context.Context, path ...string) (Value, error) {
|
||||||
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) {
|
|
||||||
var (
|
var (
|
||||||
variable Variable
|
value Value
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
key := c.key(name)
|
|
||||||
|
|
||||||
for _, provider := range c.providers {
|
for _, provider := range c.providers {
|
||||||
variable, err = provider.Read(ctx, key)
|
value, err = provider.Value(ctx, path...)
|
||||||
if err == nil || !(errors.Is(err, ErrVariableNotFound) || errors.Is(err, ErrInitFactory)) {
|
if err == nil || !(errors.Is(err, ErrValueNotFound) || errors.Is(err, ErrInitFactory)) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
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 {
|
func (c *Client) Watch(ctx context.Context, callback WatchCallback, path ...string) error {
|
||||||
key := c.key(name)
|
|
||||||
|
|
||||||
for idx, prov := range c.providers {
|
for idx, prov := range c.providers {
|
||||||
provider, ok := prov.(WatchProvider)
|
provider, ok := prov.(WatchProvider)
|
||||||
@@ -152,9 +131,9 @@ func (c *Client) Watch(ctx context.Context, name string, callback WatchCallback)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err := provider.Watch(ctx, key, callback)
|
err := provider.Watch(ctx, callback, path...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrVariableNotFound) || errors.Is(err, ErrInitFactory) {
|
if errors.Is(err, ErrValueNotFound) || errors.Is(err, ErrInitFactory) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ExampleClient_Value() {
|
func ExampleClient_Value() {
|
||||||
|
const (
|
||||||
|
namespace = "fdevs"
|
||||||
|
appName = "config"
|
||||||
|
)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
_ = os.Setenv("FDEVS_CONFIG_LISTEN", "8080")
|
_ = os.Setenv("FDEVS_CONFIG_LISTEN", "8080")
|
||||||
_ = os.Setenv("FDEVS_CONFIG_HOST", "localhost")
|
_ = os.Setenv("FDEVS_CONFIG_HOST", "localhost")
|
||||||
@@ -51,11 +56,11 @@ func ExampleClient_Value() {
|
|||||||
// read json config
|
// read json config
|
||||||
jsonConfig := test.ReadFile("config.json")
|
jsonConfig := test.ReadFile("config.json")
|
||||||
|
|
||||||
config, err := config.New(test.Namespace, test.AppName,
|
config, err := config.New(
|
||||||
arg.New(),
|
arg.New(),
|
||||||
env.New(),
|
env.New(test.Namespace, test.AppName),
|
||||||
etcd.NewProvider(etcdClient),
|
etcd.NewProvider(namespace, appName, etcdClient),
|
||||||
vault.NewSecretKV2(vaultClient),
|
vault.NewSecretKV2(namespace, appName, vaultClient),
|
||||||
json.New(jsonConfig),
|
json.New(jsonConfig),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -64,30 +69,30 @@ func ExampleClient_Value() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dsn, err := config.Value(ctx, "example:dsn")
|
dsn, err := config.Value(ctx, "example", "dsn")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print("example:dsn", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
port, err := config.Value(ctx, "listen")
|
port, err := config.Value(ctx, "listen")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print("listen", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
enabled, err := config.Value(ctx, "maintain")
|
enabled, err := config.Value(ctx, "maintain")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print("maintain", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
title, err := config.Value(ctx, "app.name.title")
|
title, err := config.Value(ctx, "app.name.title")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print("app.name.title", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -125,6 +130,11 @@ func ExampleClient_Value() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ExampleClient_Watch() {
|
func ExampleClient_Watch() {
|
||||||
|
const (
|
||||||
|
namespace = "fdevs"
|
||||||
|
appName = "config"
|
||||||
|
)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -155,10 +165,10 @@ func ExampleClient_Watch() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
watcher, err := config.New(test.Namespace, test.AppName,
|
watcher, err := config.New(
|
||||||
watcher.New(time.Microsecond, env.New()),
|
watcher.New(time.Microsecond, env.New(test.Namespace, test.AppName)),
|
||||||
watcher.New(time.Microsecond, yaml.NewWatch("test/fixture/config.yaml")),
|
watcher.New(time.Microsecond, yaml.NewWatch("test/fixture/config.yaml")),
|
||||||
etcd.NewProvider(etcdClient),
|
etcd.NewProvider(namespace, appName, etcdClient),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
@@ -169,10 +179,10 @@ func ExampleClient_Watch() {
|
|||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
|
|
||||||
err = watcher.Watch(ctx, "example_enable", func(ctx context.Context, oldVar, newVar config.Variable) {
|
err = watcher.Watch(ctx, func(ctx context.Context, oldVar, newVar config.Value) {
|
||||||
fmt.Println("update ", oldVar.Provider, " variable:", oldVar.Name, ", old: ", oldVar.Value.Bool(), " new:", newVar.Value.Bool())
|
fmt.Println("update example_enable old: ", oldVar.Bool(), " new:", newVar.Bool())
|
||||||
wg.Done()
|
wg.Done()
|
||||||
})
|
}, "example_enable")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
|
|
||||||
@@ -181,10 +191,10 @@ func ExampleClient_Watch() {
|
|||||||
|
|
||||||
_ = os.Setenv("FDEVS_CONFIG_EXAMPLE_ENABLE", "false")
|
_ = os.Setenv("FDEVS_CONFIG_EXAMPLE_ENABLE", "false")
|
||||||
|
|
||||||
err = watcher.Watch(ctx, "example_db_dsn", func(ctx context.Context, oldVar, newVar config.Variable) {
|
err = watcher.Watch(ctx, func(ctx context.Context, oldVar, newVar config.Value) {
|
||||||
fmt.Println("update ", oldVar.Provider, " variable:", oldVar.Name, ", old: ", oldVar.Value.String(), " new:", newVar.Value.String())
|
fmt.Println("update example_db_dsn old: ", oldVar.String(), " new:", newVar.String())
|
||||||
wg.Done()
|
wg.Done()
|
||||||
})
|
}, "example_db_dsn")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
|
|
||||||
@@ -202,8 +212,8 @@ func ExampleClient_Watch() {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
// Output:
|
// Output:
|
||||||
// update env variable: FDEVS_CONFIG_EXAMPLE_ENABLE , old: true new: false
|
// update 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_db_dsn old: pgsql://user@pass:127.0.0.1:5432 new: mysql://localhost:5432
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleClient_Value_factory() {
|
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"}
|
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(),
|
arg.New(),
|
||||||
env.New(),
|
env.New(test.Namespace, test.AppName),
|
||||||
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-json")
|
val, err := cfg.Value(ctx, "config-json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed read config file:%w", err)
|
return nil, fmt.Errorf("failed read config file:%w", err)
|
||||||
@@ -231,7 +241,7 @@ func ExampleClient_Value_factory() {
|
|||||||
|
|
||||||
return json.New(jsonConfig), nil
|
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")
|
val, err := cfg.Value(ctx, "config-yaml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed read config file:%w", err)
|
return nil, fmt.Errorf("failed read config file:%w", err)
|
||||||
@@ -258,14 +268,14 @@ func ExampleClient_Value_factory() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
title, err := config.Value(ctx, "app.name.title")
|
title, err := config.Value(ctx, "app", "name", "title")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
yamlTitle, err := config.Value(ctx, "app/title")
|
yamlTitle, err := config.Value(ctx, "app", "title")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
|
|
||||||
|
|||||||
29
definition/defenition.go
Executable file
29
definition/defenition.go
Executable file
@@ -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
Normal file
70
definition/generate/generator.go
Normal file
@@ -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
Normal file
16
definition/generate/helpers.go
Normal file
@@ -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
Normal file
87
definition/generate/imports.go
Normal file
@@ -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
Normal file
38
definition/generate/run.go
Normal file
@@ -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
Normal file
41
definition/generate/template.go
Normal file
@@ -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
Normal file
60
definition/generate/view.go
Normal file
@@ -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
Executable file
27
definition/group/group.go
Executable file
@@ -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
Normal file
85
definition/group/view.go
Normal file
@@ -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
Executable file
27
definition/option.go
Executable file
@@ -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
Executable file
100
definition/option/option.go
Executable file
@@ -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
Normal file
33
definition/option/tpl/option.tmpl
Normal file
@@ -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
Normal file
3
definition/option/tpl/parse.tmpl
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{{block "Parse" .}}
|
||||||
|
return {{.ValName}}.Parse{{ .FuncType}}()
|
||||||
|
{{end}}
|
||||||
8
definition/option/tpl/unmarshal_json.tmpl
Normal file
8
definition/option/tpl/unmarshal_json.tmpl
Normal file
@@ -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
Normal file
8
definition/option/tpl/unmarshal_text.tmpl
Normal file
@@ -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
Normal file
226
definition/option/view.go
Normal file
@@ -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
Normal file
6
definition/option/view_params.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
const (
|
||||||
|
ViewParamFunctName = "view.funcName"
|
||||||
|
ViewParamDescription = "view.description"
|
||||||
|
)
|
||||||
27
definition/proto/proto.go
Normal file
27
definition/proto/proto.go
Normal file
@@ -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
Normal file
76
definition/proto/view.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -2,7 +2,7 @@ version: '3'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
vault:
|
vault:
|
||||||
image: vault:latest
|
image: vault:1.13.3
|
||||||
cap_add:
|
cap_add:
|
||||||
- IPC_LOCK
|
- IPC_LOCK
|
||||||
ports:
|
ports:
|
||||||
@@ -10,7 +10,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
VAULT_DEV_ROOT_TOKEN_ID: "dev"
|
VAULT_DEV_ROOT_TOKEN_ID: "dev"
|
||||||
etcd:
|
etcd:
|
||||||
image: bitnami/etcd
|
image: bitnami/etcd:3.5.11
|
||||||
environment:
|
environment:
|
||||||
ALLOW_NONE_AUTHENTICATION: "yes"
|
ALLOW_NONE_AUTHENTICATION: "yes"
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
2
error.go
2
error.go
@@ -3,7 +3,7 @@ package config
|
|||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrVariableNotFound = errors.New("variable not found")
|
ErrValueNotFound = errors.New("value not found")
|
||||||
ErrInvalidValue = errors.New("invalid value")
|
ErrInvalidValue = errors.New("invalid value")
|
||||||
ErrUnknowType = errors.New("unknow type")
|
ErrUnknowType = errors.New("unknow type")
|
||||||
ErrInitFactory = errors.New("init factory")
|
ErrInitFactory = errors.New("init factory")
|
||||||
|
|||||||
41
go.mod
41
go.mod
@@ -1,11 +1,11 @@
|
|||||||
module gitoa.ru/go-4devs/config
|
module gitoa.ru/go-4devs/config
|
||||||
|
|
||||||
go 1.16
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/hashicorp/vault/api v1.1.0
|
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/pelletier/go-toml v1.9.0
|
||||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/tidwall/gjson v1.7.5
|
github.com/tidwall/gjson v1.7.5
|
||||||
go.etcd.io/etcd/api/v3 v3.5.0-alpha.0
|
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/ini.v1 v1.62.0
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
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
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 h1:e1ok06zGrWJW91rzRroyl5nRNqraaBe4d5hiKcVZuHM=
|
||||||
github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
|
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/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/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.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
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-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-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-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-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 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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=
|
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
|
||||||
|
|||||||
17
provider.go
17
provider.go
@@ -3,17 +3,18 @@ package config
|
|||||||
import "context"
|
import "context"
|
||||||
|
|
||||||
type Provider interface {
|
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 NamedProvider interface {
|
||||||
|
Name() string
|
||||||
|
Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
type WatchCallback func(ctx context.Context, oldVar, newVar Value)
|
||||||
|
|
||||||
type WatchProvider interface {
|
type WatchProvider interface {
|
||||||
Watch(ctx context.Context, key Key, callback WatchCallback) error
|
Watch(ctx context.Context, callback WatchCallback, path ...string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReadConfig interface {
|
type Factory func(ctx context.Context, cfg Provider) (Provider, error)
|
||||||
Value(ctx context.Context, name string) (Value, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Factory func(ctx context.Context, cfg ReadConfig) (Provider, error)
|
|
||||||
|
|||||||
@@ -7,23 +7,27 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitoa.ru/go-4devs/config"
|
"gitoa.ru/go-4devs/config"
|
||||||
"gitoa.ru/go-4devs/config/key"
|
|
||||||
"gitoa.ru/go-4devs/config/value"
|
"gitoa.ru/go-4devs/config/value"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const Name = "arg"
|
||||||
|
|
||||||
var _ config.Provider = (*Provider)(nil)
|
var _ config.Provider = (*Provider)(nil)
|
||||||
|
|
||||||
type Option func(*Provider)
|
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 }
|
return func(p *Provider) { p.key = factory }
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(opts ...Option) *Provider {
|
func New(opts ...Option) *Provider {
|
||||||
prov := Provider{
|
prov := Provider{
|
||||||
key: key.Name,
|
key: func(s ...string) string {
|
||||||
|
return strings.Join(s, "-")
|
||||||
|
},
|
||||||
args: make(map[string][]string, len(os.Args[1:])),
|
args: make(map[string][]string, len(os.Args[1:])),
|
||||||
|
name: Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
@@ -35,7 +39,8 @@ func New(opts ...Option) *Provider {
|
|||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
args map[string][]string
|
args map[string][]string
|
||||||
key config.KeyFactory
|
key func(...string) string
|
||||||
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint: cyclop
|
// nolint: cyclop
|
||||||
@@ -98,45 +103,29 @@ func (p *Provider) parse() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) Name() string {
|
func (p *Provider) Name() string {
|
||||||
return "arg"
|
return p.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) IsSupport(ctx context.Context, key config.Key) bool {
|
func (p *Provider) Value(ctx context.Context, path ...string) (config.Value, error) {
|
||||||
return p.key(ctx, key) != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) Read(ctx context.Context, key config.Key) (config.Variable, error) {
|
|
||||||
if err := p.parse(); err != nil {
|
if err := p.parse(); err != nil {
|
||||||
return config.Variable{
|
return nil, err
|
||||||
Name: "",
|
|
||||||
Value: nil,
|
|
||||||
Provider: p.Name(),
|
|
||||||
}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
name := p.key(ctx, key)
|
name := p.key(path...)
|
||||||
if val, ok := p.args[name]; ok {
|
if val, ok := p.args[name]; ok {
|
||||||
switch {
|
switch {
|
||||||
case len(val) == 1:
|
case len(val) == 1:
|
||||||
return config.Variable{
|
return value.JString(val[0]), nil
|
||||||
Name: name,
|
|
||||||
Provider: p.Name(),
|
|
||||||
Value: value.JString(val[0]),
|
|
||||||
}, nil
|
|
||||||
default:
|
default:
|
||||||
var yNode yaml.Node
|
var yNode yaml.Node
|
||||||
|
|
||||||
if err := yaml.Unmarshal([]byte("["+strings.Join(val, ",")+"]"), &yNode); err != nil {
|
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{
|
return value.Decode(yNode.Decode), nil
|
||||||
Name: name,
|
|
||||||
Provider: p.Name(),
|
|
||||||
Value: 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,15 +31,15 @@ func TestProvider(t *testing.T) {
|
|||||||
"--end-after=2008-01-02T15:04:05+03:00",
|
"--end-after=2008-01-02T15:04:05+03:00",
|
||||||
}
|
}
|
||||||
read := []test.Read{
|
read := []test.Read{
|
||||||
test.NewRead("listen", 8080),
|
test.NewRead(8080, "listen"),
|
||||||
test.NewRead("config", "config.hcl"),
|
test.NewRead("config.hcl", "config"),
|
||||||
test.NewRead("start-at", test.Time("2010-01-02T15:04:05Z")),
|
test.NewRead(test.Time("2010-01-02T15:04:05Z"), "start-at"),
|
||||||
test.NewReadUnmarshal("url", &[]string{"http://4devs.io", "https://4devs.io"}, &[]string{}),
|
test.NewReadUnmarshal(&[]string{"http://4devs.io", "https://4devs.io"}, &[]string{}, "url"),
|
||||||
test.NewReadUnmarshal("timeout", &[]time.Duration{time.Minute, time.Hour}, &[]time.Duration{}),
|
test.NewReadUnmarshal(&[]time.Duration{time.Minute, time.Hour}, &[]time.Duration{}, "timeout"),
|
||||||
test.NewReadUnmarshal("end-after", &[]time.Time{
|
test.NewReadUnmarshal(&[]time.Time{
|
||||||
test.Time("2009-01-02T15:04:05Z"),
|
test.Time("2009-01-02T15:04:05Z"),
|
||||||
test.Time("2008-01-02T15:04:05+03:00"),
|
test.Time("2008-01-02T15:04:05+03:00"),
|
||||||
}, &[]time.Time{}),
|
}, &[]time.Time{}, "end-after"),
|
||||||
}
|
}
|
||||||
|
|
||||||
prov := arg.New()
|
prov := arg.New()
|
||||||
|
|||||||
35
provider/env/provider.go
vendored
35
provider/env/provider.go
vendored
@@ -2,27 +2,30 @@ package env
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitoa.ru/go-4devs/config"
|
"gitoa.ru/go-4devs/config"
|
||||||
"gitoa.ru/go-4devs/config/key"
|
|
||||||
"gitoa.ru/go-4devs/config/value"
|
"gitoa.ru/go-4devs/config/value"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const Name = "env"
|
||||||
|
|
||||||
var _ config.Provider = (*Provider)(nil)
|
var _ config.Provider = (*Provider)(nil)
|
||||||
|
|
||||||
type Option func(*Provider)
|
type Option func(*Provider)
|
||||||
|
|
||||||
func WithKeyFactory(factory config.KeyFactory) Option {
|
func WithKeyFactory(factory func(...string) string) Option {
|
||||||
return func(p *Provider) { p.key = factory }
|
return func(p *Provider) { p.key = factory }
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(opts ...Option) *Provider {
|
func New(namespace, appName string, opts ...Option) *Provider {
|
||||||
provider := Provider{
|
provider := Provider{
|
||||||
key: func(ctx context.Context, k config.Key) string {
|
key: func(path ...string) string {
|
||||||
return strings.ToUpper(key.NsAppName("_")(ctx, k))
|
return strings.ToUpper(strings.Join(path, "_"))
|
||||||
},
|
},
|
||||||
|
prefix: strings.ToUpper(namespace + "_" + appName + "_"),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
@@ -33,26 +36,20 @@ func New(opts ...Option) *Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
key config.KeyFactory
|
key func(...string) string
|
||||||
|
name string
|
||||||
|
prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) Name() string {
|
func (p *Provider) Name() string {
|
||||||
return "env"
|
return p.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) IsSupport(ctx context.Context, key config.Key) bool {
|
func (p *Provider) Value(ctx context.Context, path ...string) (config.Value, error) {
|
||||||
return p.key(ctx, key) != ""
|
name := p.prefix + p.key(path...)
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) Read(ctx context.Context, key config.Key) (config.Variable, error) {
|
|
||||||
name := p.key(ctx, key)
|
|
||||||
if val, ok := os.LookupEnv(name); ok {
|
if val, ok := os.LookupEnv(name); ok {
|
||||||
return config.Variable{
|
return value.JString(val), nil
|
||||||
Name: name,
|
|
||||||
Provider: p.Name(),
|
|
||||||
Value: 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
vendored
6
provider/env/provider_test.go
vendored
@@ -14,11 +14,11 @@ func TestProvider(t *testing.T) {
|
|||||||
os.Setenv("FDEVS_CONFIG_DSN", test.DSN)
|
os.Setenv("FDEVS_CONFIG_DSN", test.DSN)
|
||||||
os.Setenv("FDEVS_CONFIG_PORT", "8080")
|
os.Setenv("FDEVS_CONFIG_PORT", "8080")
|
||||||
|
|
||||||
provider := env.New()
|
provider := env.New("fdevs", "config")
|
||||||
|
|
||||||
read := []test.Read{
|
read := []test.Read{
|
||||||
test.NewRead("dsn", test.DSN),
|
test.NewRead(test.DSN, "dsn"),
|
||||||
test.NewRead("port", 8080),
|
test.NewRead(8080, "port"),
|
||||||
}
|
}
|
||||||
test.Run(t, provider, read)
|
test.Run(t, provider, read)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,19 @@ package etcd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitoa.ru/go-4devs/config"
|
"gitoa.ru/go-4devs/config"
|
||||||
"gitoa.ru/go-4devs/config/key"
|
|
||||||
"gitoa.ru/go-4devs/config/value"
|
"gitoa.ru/go-4devs/config/value"
|
||||||
pb "go.etcd.io/etcd/api/v3/mvccpb"
|
pb "go.etcd.io/etcd/api/v3/mvccpb"
|
||||||
client "go.etcd.io/etcd/client/v3"
|
client "go.etcd.io/etcd/client/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Name = "etcd"
|
||||||
|
Separator = "/"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ config.Provider = (*Provider)(nil)
|
_ config.Provider = (*Provider)(nil)
|
||||||
_ config.WatchProvider = (*Provider)(nil)
|
_ config.WatchProvider = (*Provider)(nil)
|
||||||
@@ -21,10 +26,14 @@ type Client interface {
|
|||||||
client.Watcher
|
client.Watcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProvider(client Client) *Provider {
|
func NewProvider(namespace, appName string, client Client) *Provider {
|
||||||
p := Provider{
|
p := Provider{
|
||||||
client: client,
|
client: client,
|
||||||
key: key.NsAppName("/"),
|
key: func(s ...string) string {
|
||||||
|
return strings.Join(s, Separator)
|
||||||
|
},
|
||||||
|
name: Name,
|
||||||
|
prefix: namespace + Separator + appName,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &p
|
return &p
|
||||||
@@ -32,34 +41,35 @@ func NewProvider(client Client) *Provider {
|
|||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
client Client
|
client Client
|
||||||
key config.KeyFactory
|
key func(...string) string
|
||||||
}
|
name string
|
||||||
|
prefix string
|
||||||
func (p *Provider) IsSupport(ctx context.Context, key config.Key) bool {
|
|
||||||
return p.key(ctx, key) != ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) Name() 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) {
|
func (p *Provider) Value(ctx context.Context, path ...string) (config.Value, error) {
|
||||||
name := p.key(ctx, key)
|
name := p.Key(path)
|
||||||
|
|
||||||
resp, err := p.client.Get(ctx, name, client.WithPrefix())
|
resp, err := p.client.Get(ctx, name, client.WithPrefix())
|
||||||
if err != nil {
|
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)
|
val, err := p.resolve(name, resp.Kvs)
|
||||||
if err != nil {
|
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
|
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) {
|
go func(ctx context.Context, key string, callback config.WatchCallback) {
|
||||||
watch := p.client.Watch(ctx, key, client.WithPrevKV(), client.WithPrefix())
|
watch := p.client.Watch(ctx, key, client.WithPrevKV(), client.WithPrefix())
|
||||||
for w := range watch {
|
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)
|
callback(ctx, oldVar, newVar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}(ctx, p.key(ctx, key), callback)
|
}(ctx, p.Key(path), callback)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -87,23 +97,15 @@ func (p *Provider) getEventKvs(events []*client.Event) ([]*pb.KeyValue, []*pb.Ke
|
|||||||
return kvs, old
|
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 {
|
for _, kv := range kvs {
|
||||||
switch {
|
switch {
|
||||||
case kv == nil:
|
case kv == nil:
|
||||||
return config.Variable{
|
return nil, nil
|
||||||
Name: key,
|
|
||||||
Provider: p.Name(),
|
|
||||||
Value: nil,
|
|
||||||
}, nil
|
|
||||||
case string(kv.Key) == key:
|
case string(kv.Key) == key:
|
||||||
return config.Variable{
|
return value.JBytes(kv.Value), nil
|
||||||
Value: value.JBytes(kv.Value),
|
|
||||||
Name: key,
|
|
||||||
Provider: p.Name(),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.Variable{}, fmt.Errorf("%w: name %s", config.ErrVariableNotFound, key)
|
return nil, fmt.Errorf("%w: name %s", config.ErrValueNotFound, key)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,17 +23,17 @@ func TestProvider(t *testing.T) {
|
|||||||
et, err := test.NewEtcd(ctx)
|
et, err := test.NewEtcd(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
provider := etcd.NewProvider(et)
|
provider := etcd.NewProvider("fdevs", "config", et)
|
||||||
read := []test.Read{
|
read := []test.Read{
|
||||||
test.NewRead("db_dsn", test.DSN),
|
test.NewRead(test.DSN, "db_dsn"),
|
||||||
test.NewRead("duration", 12*time.Minute),
|
test.NewRead(12*time.Minute, "duration"),
|
||||||
test.NewRead("port", 8080),
|
test.NewRead(8080, "port"),
|
||||||
test.NewRead("maintain", true),
|
test.NewRead(true, "maintain"),
|
||||||
test.NewRead("start_at", test.Time("2020-01-02T15:04:05Z")),
|
test.NewRead(test.Time("2020-01-02T15:04:05Z"), "start_at"),
|
||||||
test.NewRead("percent", .064),
|
test.NewRead(.064, "percent"),
|
||||||
test.NewRead("count", uint(2020)),
|
test.NewRead(uint(2020), "count"),
|
||||||
test.NewRead("int64", int64(2021)),
|
test.NewRead(int64(2021), "int64"),
|
||||||
test.NewRead("uint64", int64(2022)),
|
test.NewRead(int64(2022), "uint64"),
|
||||||
test.NewReadConfig("config"),
|
test.NewReadConfig("config"),
|
||||||
}
|
}
|
||||||
test.Run(t, provider, read)
|
test.Run(t, provider, read)
|
||||||
@@ -49,11 +49,7 @@ func TestWatcher(t *testing.T) {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
key := config.Key{
|
key := "test_watch"
|
||||||
AppName: "config",
|
|
||||||
Namespace: "fdevs",
|
|
||||||
Name: "test_watch",
|
|
||||||
}
|
|
||||||
|
|
||||||
et, err := test.NewEtcd(ctx)
|
et, err := test.NewEtcd(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -65,24 +61,24 @@ func TestWatcher(t *testing.T) {
|
|||||||
|
|
||||||
var cnt, cnt2 int32
|
var cnt, cnt2 int32
|
||||||
|
|
||||||
prov := etcd.NewProvider(et)
|
prov := etcd.NewProvider("fdevs", "config", et)
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
wg.Add(6)
|
wg.Add(6)
|
||||||
|
|
||||||
watch := func(cnt *int32) 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.Variable) {
|
return func(ctx context.Context, oldVar, newVar config.Value) {
|
||||||
switch *cnt {
|
switch *cnt {
|
||||||
case 0:
|
case 0:
|
||||||
assert.Equal(t, value(*cnt), newVar.Value.String())
|
assert.Equal(t, value(*cnt), newVar.String())
|
||||||
assert.Nil(t, oldVar.Value)
|
assert.Nil(t, oldVar)
|
||||||
case 1:
|
case 1:
|
||||||
assert.Equal(t, value(*cnt), newVar.Value.String())
|
assert.Equal(t, value(*cnt), newVar.String())
|
||||||
assert.Equal(t, value(*cnt-1), oldVar.Value.String())
|
assert.Equal(t, value(*cnt-1), oldVar.String())
|
||||||
case 2:
|
case 2:
|
||||||
_, perr := newVar.Value.ParseString()
|
_, perr := newVar.ParseString()
|
||||||
assert.NoError(t, perr)
|
assert.NoError(t, perr)
|
||||||
assert.Equal(t, "", newVar.Value.String())
|
assert.Equal(t, "", newVar.String())
|
||||||
assert.Equal(t, value(*cnt-1), oldVar.Value.String())
|
assert.Equal(t, value(*cnt-1), oldVar.String())
|
||||||
default:
|
default:
|
||||||
assert.Fail(t, "unexpected watch")
|
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, watch(&cnt), key)
|
||||||
err = prov.Watch(ctx, key, watch(&cnt2))
|
err = prov.Watch(ctx, watch(&cnt2), key)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
time.AfterFunc(time.Second, func() {
|
time.AfterFunc(time.Second, func() {
|
||||||
|
|||||||
@@ -10,55 +10,49 @@ import (
|
|||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Name = "ini"
|
||||||
|
Separator = "."
|
||||||
|
)
|
||||||
|
|
||||||
var _ config.Provider = (*Provider)(nil)
|
var _ config.Provider = (*Provider)(nil)
|
||||||
|
|
||||||
func New(data *ini.File) *Provider {
|
func New(data *ini.File) *Provider {
|
||||||
const nameParts = 2
|
|
||||||
|
|
||||||
return &Provider{
|
return &Provider{
|
||||||
data: data,
|
data: data,
|
||||||
resolve: func(ctx context.Context, key config.Key) (string, string) {
|
resolve: func(path []string) (string, string) {
|
||||||
keys := strings.SplitN(key.Name, "/", nameParts)
|
if len(path) == 1 {
|
||||||
if len(keys) == 1 {
|
return "", path[0]
|
||||||
return "", keys[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 {
|
type Provider struct {
|
||||||
data *ini.File
|
data *ini.File
|
||||||
resolve func(ctx context.Context, key config.Key) (string, string)
|
resolve func(path []string) (string, string)
|
||||||
}
|
name string
|
||||||
|
|
||||||
func (p *Provider) IsSupport(ctx context.Context, key config.Key) bool {
|
|
||||||
section, name := p.resolve(ctx, key)
|
|
||||||
|
|
||||||
return section != "" && name != ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) 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) {
|
func (p *Provider) Value(ctx context.Context, path ...string) (config.Value, error) {
|
||||||
section, name := p.resolve(ctx, key)
|
section, name := p.resolve(path)
|
||||||
|
|
||||||
iniSection, err := p.data.GetSection(section)
|
iniSection, err := p.data.GetSection(section)
|
||||||
if err != nil {
|
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)
|
iniKey, err := iniSection.GetKey(name)
|
||||||
if err != nil {
|
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{
|
return value.JString(iniKey.String()), nil
|
||||||
Name: section + ":" + name,
|
|
||||||
Provider: p.Name(),
|
|
||||||
Value: value.JString(iniKey.String()),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ func TestProvider(t *testing.T) {
|
|||||||
file := test.NewINI()
|
file := test.NewINI()
|
||||||
|
|
||||||
read := []test.Read{
|
read := []test.Read{
|
||||||
test.NewRead("project/PROJECT_BOARD_BASIC_KANBAN_TYPE", "To Do, In Progress, Done"),
|
test.NewRead("To Do, In Progress, Done", "project", "PROJECT_BOARD_BASIC_KANBAN_TYPE"),
|
||||||
test.NewRead("repository.editor/PREVIEWABLE_FILE_MODES", "markdown"),
|
test.NewRead("markdown", "repository.editor", "PREVIEWABLE_FILE_MODES"),
|
||||||
test.NewRead("server/LOCAL_ROOT_URL", "http://0.0.0.0:3000/"),
|
test.NewRead("http://0.0.0.0:3000/", "server", "LOCAL_ROOT_URL"),
|
||||||
test.NewRead("server/LFS_HTTP_AUTH_EXPIRY", 20*time.Minute),
|
test.NewRead(20*time.Minute, "server", "LFS_HTTP_AUTH_EXPIRY"),
|
||||||
test.NewRead("repository.pull-request/DEFAULT_MERGE_MESSAGE_SIZE", 5120),
|
test.NewRead(5120, "repository.pull-request", "DEFAULT_MERGE_MESSAGE_SIZE"),
|
||||||
test.NewRead("ui/SHOW_USER_EMAIL", true),
|
test.NewRead(true, "ui", "SHOW_USER_EMAIL"),
|
||||||
test.NewRead("cors/ENABLED", false),
|
test.NewRead(false, "cors", "enabled"),
|
||||||
}
|
}
|
||||||
|
|
||||||
prov := ini.New(file)
|
prov := ini.New(file)
|
||||||
|
|||||||
@@ -3,20 +3,27 @@ package json
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"gitoa.ru/go-4devs/config"
|
"gitoa.ru/go-4devs/config"
|
||||||
"gitoa.ru/go-4devs/config/key"
|
|
||||||
"gitoa.ru/go-4devs/config/value"
|
"gitoa.ru/go-4devs/config/value"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Name = "json"
|
||||||
|
Separator = "."
|
||||||
|
)
|
||||||
|
|
||||||
var _ config.Provider = (*Provider)(nil)
|
var _ config.Provider = (*Provider)(nil)
|
||||||
|
|
||||||
func New(json []byte, opts ...Option) *Provider {
|
func New(json []byte, opts ...Option) *Provider {
|
||||||
provider := Provider{
|
provider := Provider{
|
||||||
key: key.Name,
|
key: func(s ...string) string {
|
||||||
|
return strings.Join(s, Separator)
|
||||||
|
},
|
||||||
data: json,
|
data: json,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +35,7 @@ func New(json []byte, opts ...Option) *Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewFile(path string, opts ...Option) (*Provider, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: unable to read config file %#q: file not found or unreadable", err, path)
|
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 {
|
type Provider struct {
|
||||||
data []byte
|
data []byte
|
||||||
key config.KeyFactory
|
key func(...string) string
|
||||||
}
|
name string
|
||||||
|
|
||||||
func (p *Provider) IsSupport(ctx context.Context, key config.Key) bool {
|
|
||||||
return p.key(ctx, key) != ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) 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) {
|
func (p *Provider) Value(ctx context.Context, path ...string) (config.Value, error) {
|
||||||
path := p.key(ctx, key)
|
key := p.key(path...)
|
||||||
|
if val := gjson.GetBytes(p.data, key); val.Exists() {
|
||||||
if val := gjson.GetBytes(p.data, path); val.Exists() {
|
return value.JString(val.String()), nil
|
||||||
return config.Variable{
|
|
||||||
Name: path,
|
|
||||||
Provider: p.Name(),
|
|
||||||
Value: value.JString(val.String()),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.Variable{}, config.ErrVariableNotFound
|
return nil, fmt.Errorf("%v:%w", p.Name(), config.ErrValueNotFound)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ func TestProvider(t *testing.T) {
|
|||||||
prov := provider.New(js)
|
prov := provider.New(js)
|
||||||
sl := []string{}
|
sl := []string{}
|
||||||
read := []test.Read{
|
read := []test.Read{
|
||||||
test.NewRead("app.name.title", "config title"),
|
test.NewRead("config title", "app.name.title"),
|
||||||
test.NewRead("app.name.timeout", time.Minute),
|
test.NewRead(time.Minute, "app.name.timeout"),
|
||||||
test.NewReadUnmarshal("app.name.var", &[]string{"name"}, &sl),
|
test.NewReadUnmarshal(&[]string{"name"}, &sl, "app.name.var"),
|
||||||
test.NewReadConfig("cfg"),
|
test.NewReadConfig("cfg"),
|
||||||
test.NewRead("app.name.success", true),
|
test.NewRead(true, "app", "name", "success"),
|
||||||
}
|
}
|
||||||
|
|
||||||
test.Run(t, prov, read)
|
test.Run(t, prov, read)
|
||||||
|
|||||||
@@ -3,13 +3,18 @@ package toml
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/pelletier/go-toml"
|
"github.com/pelletier/go-toml"
|
||||||
"gitoa.ru/go-4devs/config"
|
"gitoa.ru/go-4devs/config"
|
||||||
"gitoa.ru/go-4devs/config/key"
|
|
||||||
"gitoa.ru/go-4devs/config/value"
|
"gitoa.ru/go-4devs/config/value"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Name = "toml"
|
||||||
|
Separator = "."
|
||||||
|
)
|
||||||
|
|
||||||
var _ config.Provider = (*Provider)(nil)
|
var _ config.Provider = (*Provider)(nil)
|
||||||
|
|
||||||
func NewFile(file string, opts ...Option) (*Provider, error) {
|
func NewFile(file string, opts ...Option) (*Provider, error) {
|
||||||
@@ -26,7 +31,9 @@ type Option func(*Provider)
|
|||||||
func configure(tree *toml.Tree, opts ...Option) *Provider {
|
func configure(tree *toml.Tree, opts ...Option) *Provider {
|
||||||
prov := &Provider{
|
prov := &Provider{
|
||||||
tree: tree,
|
tree: tree,
|
||||||
key: key.Name,
|
key: func(s []string) string {
|
||||||
|
return strings.Join(s, Separator)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
@@ -47,25 +54,18 @@ func New(data []byte, opts ...Option) (*Provider, error) {
|
|||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
tree *toml.Tree
|
tree *toml.Tree
|
||||||
key config.KeyFactory
|
key func([]string) string
|
||||||
}
|
name string
|
||||||
|
|
||||||
func (p *Provider) IsSupport(ctx context.Context, key config.Key) bool {
|
|
||||||
return p.key(ctx, key) != ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) 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) {
|
func (p *Provider) Value(ctx context.Context, path ...string) (config.Value, error) {
|
||||||
if k := p.key(ctx, key); p.tree.Has(k) {
|
if k := p.key(path); p.tree.Has(k) {
|
||||||
return config.Variable{
|
return Value{Value: value.Value{Val: p.tree.Get(k)}}, nil
|
||||||
Name: k,
|
|
||||||
Provider: p.Name(),
|
|
||||||
Value: Value{Value: value.Value{Val: p.tree.Get(k)}},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.Variable{}, config.ErrVariableNotFound
|
return nil, config.ErrValueNotFound
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ func TestProvider(t *testing.T) {
|
|||||||
m := []int{}
|
m := []int{}
|
||||||
|
|
||||||
read := []test.Read{
|
read := []test.Read{
|
||||||
test.NewRead("database.server", "192.168.1.1"),
|
test.NewRead("192.168.1.1", "database.server"),
|
||||||
test.NewRead("title", "TOML Example"),
|
test.NewRead("TOML Example", "title"),
|
||||||
test.NewRead("servers.alpha.ip", "10.0.0.1"),
|
test.NewRead("10.0.0.1", "servers.alpha.ip"),
|
||||||
test.NewRead("database.enabled", true),
|
test.NewRead(true, "database.enabled"),
|
||||||
test.NewRead("database.connection_max", 5000),
|
test.NewRead(5000, "database.connection_max"),
|
||||||
test.NewReadUnmarshal("database.ports", &[]int{8001, 8001, 8002}, &m),
|
test.NewReadUnmarshal(&[]int{8001, 8001, 8002}, &m, "database", "ports"),
|
||||||
}
|
}
|
||||||
|
|
||||||
test.Run(t, prov, read)
|
test.Run(t, prov, read)
|
||||||
|
|||||||
@@ -4,25 +4,42 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/api"
|
"github.com/hashicorp/vault/api"
|
||||||
"gitoa.ru/go-4devs/config"
|
"gitoa.ru/go-4devs/config"
|
||||||
"gitoa.ru/go-4devs/config/key"
|
|
||||||
"gitoa.ru/go-4devs/config/value"
|
"gitoa.ru/go-4devs/config/value"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Name = "vault"
|
||||||
|
Separator = "/"
|
||||||
|
Prefix = "secret/data/"
|
||||||
|
ValueName = "value"
|
||||||
|
)
|
||||||
|
|
||||||
var _ config.Provider = (*SecretKV2)(nil)
|
var _ config.Provider = (*SecretKV2)(nil)
|
||||||
|
|
||||||
type SecretOption func(*SecretKV2)
|
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 }
|
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{
|
prov := SecretKV2{
|
||||||
client: client,
|
client: client,
|
||||||
resolve: key.LastIndexField(":", "value", key.PrefixName("secret/data/", key.NsAppName("/"))),
|
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 {
|
for _, opt := range opts {
|
||||||
@@ -34,57 +51,69 @@ func NewSecretKV2(client *api.Client, opts ...SecretOption) *SecretKV2 {
|
|||||||
|
|
||||||
type SecretKV2 struct {
|
type SecretKV2 struct {
|
||||||
client *api.Client
|
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)
|
|
||||||
|
|
||||||
return path != ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SecretKV2) Name() string {
|
func (p *SecretKV2) Name() string {
|
||||||
return "vault"
|
return p.name
|
||||||
}
|
}
|
||||||
|
func (p *SecretKV2) Key(in []string) (string, string) {
|
||||||
|
path, val := p.resolve(in)
|
||||||
|
if path == "" {
|
||||||
|
return p.prefix, val
|
||||||
|
}
|
||||||
|
|
||||||
func (p *SecretKV2) Read(ctx context.Context, key config.Key) (config.Variable, error) {
|
return p.prefix + Separator + path, val
|
||||||
path, field := p.resolve(ctx, key)
|
}
|
||||||
|
func (p *SecretKV2) read(path, key string) (*api.Secret, error) {
|
||||||
secret, err := p.client.Logical().Read(path)
|
secret, err := p.client.Logical().Read(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.Variable{}, fmt.Errorf("%w: path:%s, field:%s, provider:%s", err, path, field, p.Name())
|
return nil, err
|
||||||
|
}
|
||||||
|
if secret == nil && key != ValueName {
|
||||||
|
return p.read(path+Separator+key, ValueName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SecretKV2) Value(ctx context.Context, key ...string) (config.Value, error) {
|
||||||
|
path, field := p.Key(key)
|
||||||
|
|
||||||
|
secret, err := p.read(path, field)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: path:%s, field:%s, provider:%s", err, path, field, p.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
if secret == nil || len(secret.Data) == 0 {
|
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 {
|
if len(secret.Warnings) > 0 {
|
||||||
return config.Variable{},
|
return nil,
|
||||||
fmt.Errorf("%w: warn: %s, path:%s, field:%s, provider:%s", config.ErrVariableNotFound, secret.Warnings, path, field, p.Name())
|
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{})
|
data, ok := secret.Data["data"].(map[string]interface{})
|
||||||
if !ok {
|
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 {
|
if val, ok := data[field]; ok {
|
||||||
return config.Variable{
|
return value.JString(fmt.Sprint(val)), nil
|
||||||
Name: path + field,
|
}
|
||||||
Provider: p.Name(),
|
|
||||||
Value: value.JString(fmt.Sprint(val)),
|
if val, ok := data[ValueName]; ok {
|
||||||
}, nil
|
return value.JString(fmt.Sprint(val)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
md, err := json.Marshal(data)
|
md, err := json.Marshal(data)
|
||||||
if err != nil {
|
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{
|
return value.JBytes(md), nil
|
||||||
Name: path + field,
|
|
||||||
Provider: p.Name(),
|
|
||||||
Value: value.JBytes(md),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ func TestProvider(t *testing.T) {
|
|||||||
cl, err := test.NewVault()
|
cl, err := test.NewVault()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
provider := vault.NewSecretKV2(cl)
|
provider := vault.NewSecretKV2("fdevs", "config", cl)
|
||||||
|
|
||||||
read := []test.Read{
|
read := []test.Read{
|
||||||
test.NewReadConfig("database"),
|
test.NewReadConfig("database"),
|
||||||
test.NewRead("db:dsn", test.DSN),
|
test.NewRead(test.DSN, "db", "dsn"),
|
||||||
test.NewRead("db:timeout", time.Minute),
|
test.NewRead(time.Minute, "db", "timeout"),
|
||||||
}
|
}
|
||||||
test.Run(t, provider, read)
|
test.Run(t, provider, read)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ var (
|
|||||||
_ config.WatchProvider = (*Provider)(nil)
|
_ 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{
|
prov := &Provider{
|
||||||
Provider: provider,
|
NamedProvider: provider,
|
||||||
ticker: time.NewTicker(duration),
|
ticker: time.NewTicker(duration),
|
||||||
logger: func(_ context.Context, msg string) {
|
logger: func(_ context.Context, msg string) {
|
||||||
log.Print(msg)
|
log.Print(msg)
|
||||||
@@ -39,13 +39,13 @@ func WithLogger(l func(context.Context, string)) Option {
|
|||||||
type Option func(*Provider)
|
type Option func(*Provider)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
config.Provider
|
config.NamedProvider
|
||||||
ticker *time.Ticker
|
ticker *time.Ticker
|
||||||
logger func(context.Context, string)
|
logger func(context.Context, string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) Watch(ctx context.Context, key config.Key, callback config.WatchCallback) error {
|
func (p *Provider) Watch(ctx context.Context, callback config.WatchCallback, key ...string) error {
|
||||||
oldVar, err := p.Provider.Read(ctx, key)
|
oldVar, err := p.NamedProvider.Value(ctx, key...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed watch variable: %w", err)
|
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 {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-p.ticker.C:
|
case <-p.ticker.C:
|
||||||
newVar, err := p.Provider.Read(ctx, key)
|
newVar, err := p.NamedProvider.Value(ctx, key...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.logger(ctx, err.Error())
|
p.logger(ctx, err.Error())
|
||||||
} else if !newVar.IsEquals(oldVar) {
|
} else if !newVar.IsEquals(oldVar) {
|
||||||
|
|||||||
@@ -22,14 +22,10 @@ func (p *provider) Name() string {
|
|||||||
return "test"
|
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++
|
p.cnt++
|
||||||
|
|
||||||
return config.Variable{
|
return value.JString(fmt.Sprint(p.cnt)), nil
|
||||||
Name: "tmpname",
|
|
||||||
Provider: p.Name(),
|
|
||||||
Value: value.JString(fmt.Sprint(p.cnt)),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWatcher(t *testing.T) {
|
func TestWatcher(t *testing.T) {
|
||||||
@@ -46,11 +42,11 @@ func TestWatcher(t *testing.T) {
|
|||||||
|
|
||||||
err := w.Watch(
|
err := w.Watch(
|
||||||
ctx,
|
ctx,
|
||||||
config.Key{Name: "tmpname"},
|
func(ctx context.Context, oldVar, newVar config.Value) {
|
||||||
func(ctx context.Context, oldVar, newVar config.Variable) {
|
|
||||||
atomic.AddInt32(&cnt, 1)
|
atomic.AddInt32(&cnt, 1)
|
||||||
wg.Done()
|
wg.Done()
|
||||||
},
|
},
|
||||||
|
"tmpname",
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|||||||
@@ -4,22 +4,21 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitoa.ru/go-4devs/config"
|
"gitoa.ru/go-4devs/config"
|
||||||
"gitoa.ru/go-4devs/config/value"
|
"gitoa.ru/go-4devs/config/value"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Name = "yaml"
|
||||||
|
)
|
||||||
|
|
||||||
var _ config.Provider = (*Provider)(nil)
|
var _ config.Provider = (*Provider)(nil)
|
||||||
|
|
||||||
func keyFactory(_ context.Context, key config.Key) []string {
|
|
||||||
return strings.Split(key.Name, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFile(name string, opts ...Option) (*Provider, error) {
|
func NewFile(name string, opts ...Option) (*Provider, error) {
|
||||||
in, err := ioutil.ReadFile(name)
|
in, err := os.ReadFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("yaml_file: read error: %w", err)
|
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 {
|
func create(opts ...Option) *Provider {
|
||||||
prov := Provider{
|
prov := Provider{
|
||||||
key: keyFactory,
|
name: Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
@@ -52,22 +51,20 @@ type Option func(*Provider)
|
|||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
data node
|
data node
|
||||||
key func(context.Context, config.Key) []string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) 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) {
|
func (p *Provider) Value(_ context.Context, path ...string) (config.Value, error) {
|
||||||
k := p.key(ctx, key)
|
|
||||||
|
|
||||||
return p.data.read(p.Name(), k)
|
return p.data.read(p.Name(), path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) With(data *yaml.Node) *Provider {
|
func (p *Provider) With(data *yaml.Node) *Provider {
|
||||||
return &Provider{
|
return &Provider{
|
||||||
key: p.key,
|
|
||||||
data: node{Node: data},
|
data: node{Node: data},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,21 +73,17 @@ type node struct {
|
|||||||
*yaml.Node
|
*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)
|
val, err := getData(n.Node.Content[0].Content, keys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, config.ErrVariableNotFound) {
|
if errors.Is(err, config.ErrValueNotFound) {
|
||||||
return config.Variable{}, fmt.Errorf("%w: %s", config.ErrVariableNotFound, name)
|
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{
|
return value.Decode(val), nil
|
||||||
Name: strings.Join(keys, "."),
|
|
||||||
Provider: name,
|
|
||||||
Value: value.Decode(val),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getData(node []*yaml.Node, keys []string) (func(interface{}) error, error) {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ func TestProvider(t *testing.T) {
|
|||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
read := []test.Read{
|
read := []test.Read{
|
||||||
test.NewRead("duration_var", 21*time.Minute),
|
test.NewRead(21*time.Minute, "duration_var"),
|
||||||
test.NewRead("app/name/bool_var", true),
|
test.NewRead(true, "app", "name", "bool_var"),
|
||||||
test.NewRead("time_var", test.Time("2020-01-02T15:04:05Z")),
|
test.NewRead(test.Time("2020-01-02T15:04:05Z"), "time_var"),
|
||||||
test.NewReadConfig("cfg"),
|
test.NewReadConfig("cfg"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ package yaml
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
|
|
||||||
"gitoa.ru/go-4devs/config"
|
"gitoa.ru/go-4devs/config"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const NameWatch = "yaml_watch"
|
||||||
|
|
||||||
func NewWatch(name string, opts ...Option) *Watch {
|
func NewWatch(name string, opts ...Option) *Watch {
|
||||||
f := Watch{
|
f := Watch{
|
||||||
file: name,
|
file: name,
|
||||||
prov: create(opts...),
|
prov: create(opts...),
|
||||||
|
name: NameWatch,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &f
|
return &f
|
||||||
@@ -21,22 +24,23 @@ func NewWatch(name string, opts ...Option) *Watch {
|
|||||||
type Watch struct {
|
type Watch struct {
|
||||||
file string
|
file string
|
||||||
prov *Provider
|
prov *Provider
|
||||||
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Watch) 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) {
|
func (p *Watch) Value(ctx context.Context, path ...string) (config.Value, error) {
|
||||||
in, err := ioutil.ReadFile(p.file)
|
in, err := os.ReadFile(p.file)
|
||||||
if err != nil {
|
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
|
var yNode yaml.Node
|
||||||
if err = yaml.Unmarshal(in, &yNode); err != nil {
|
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...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ type ProviderSuite struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Read struct {
|
type Read struct {
|
||||||
Key config.Key
|
Key []string
|
||||||
Assert func(t *testing.T, v config.Value)
|
Assert func(t *testing.T, v config.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,22 +46,18 @@ type Config struct {
|
|||||||
Enabled bool
|
Enabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReadConfig(key string) Read {
|
func NewReadConfig(key ...string) Read {
|
||||||
ex := &Config{
|
ex := &Config{
|
||||||
Duration: 21 * time.Minute,
|
Duration: 21 * time.Minute,
|
||||||
Enabled: true,
|
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{
|
return Read{
|
||||||
Key: config.Key{
|
Key: key,
|
||||||
Namespace: "fdevs",
|
|
||||||
AppName: "config",
|
|
||||||
Name: key,
|
|
||||||
},
|
|
||||||
Assert: func(t *testing.T, v config.Value) {
|
Assert: func(t *testing.T, v config.Value) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
require.NoErrorf(t, v.Unmarshal(target), "unmarshal")
|
require.NoErrorf(t, v.Unmarshal(target), "unmarshal")
|
||||||
@@ -77,13 +73,9 @@ func Time(value string) time.Time {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// nolint: cyclop
|
// nolint: cyclop
|
||||||
func NewRead(key string, expected interface{}) Read {
|
func NewRead(expected interface{}, key ...string) Read {
|
||||||
return Read{
|
return Read{
|
||||||
Key: config.Key{
|
Key: key,
|
||||||
Namespace: "fdevs",
|
|
||||||
AppName: "config",
|
|
||||||
Name: key,
|
|
||||||
},
|
|
||||||
Assert: func(t *testing.T, v config.Value) {
|
Assert: func(t *testing.T, v config.Value) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var (
|
var (
|
||||||
@@ -134,9 +126,9 @@ func (ps *ProviderSuite) TestReadKeys() {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
for _, read := range ps.read {
|
for _, read := range ps.read {
|
||||||
val, err := ps.provider.Read(ctx, read.Key)
|
val, err := ps.provider.Value(ctx, read.Key...)
|
||||||
require.NoError(ps.T(), err, read.Key.String())
|
require.NoError(ps.T(), err, read.Key)
|
||||||
read.Assert(ps.T(), val.Value)
|
read.Assert(ps.T(), val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
value.go
1
value.go
@@ -8,6 +8,7 @@ type Value interface {
|
|||||||
ReadValue
|
ReadValue
|
||||||
ParseValue
|
ParseValue
|
||||||
UnmarshalValue
|
UnmarshalValue
|
||||||
|
IsEquals(Value) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnmarshalValue interface {
|
type UnmarshalValue interface {
|
||||||
|
|||||||
@@ -108,3 +108,7 @@ func (s Decode) Time() time.Time {
|
|||||||
|
|
||||||
return in
|
return in
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Decode) IsEquals(in config.Value) bool {
|
||||||
|
return s.String() == in.String()
|
||||||
|
}
|
||||||
|
|||||||
@@ -110,3 +110,7 @@ func (s JBytes) Time() time.Time {
|
|||||||
|
|
||||||
return in
|
return in
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s JBytes) IsEquals(in config.Value) bool {
|
||||||
|
return s.String() == in.String()
|
||||||
|
}
|
||||||
|
|||||||
@@ -110,3 +110,7 @@ func (s JString) Time() time.Time {
|
|||||||
|
|
||||||
return in
|
return in
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s JString) IsEquals(in config.Value) bool {
|
||||||
|
return s.String() == in.String()
|
||||||
|
}
|
||||||
|
|||||||
@@ -156,3 +156,7 @@ func (s Value) ParseTime() (time.Time, error) {
|
|||||||
|
|
||||||
return time.Time{}, config.ErrInvalidValue
|
return time.Time{}, config.ErrInvalidValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Value) IsEquals(in config.Value) bool {
|
||||||
|
return s.String() == in.String()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user