update client

This commit is contained in:
2025-12-26 14:23:54 +03:00
parent f4962e54a3
commit a5659c5d02
75 changed files with 2436 additions and 288 deletions

View File

@@ -4,8 +4,6 @@ import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
)
func Must(providers ...any) *Client {
@@ -27,14 +25,7 @@ func New(providers ...any) (*Client, error) {
case Provider:
client.providers[idx] = current
case Factory:
client.providers[idx] = &provider{
factory: func(ctx context.Context) (Provider, error) {
return current(ctx, client)
},
mu: sync.Mutex{},
done: 0,
provider: nil,
}
client.providers[idx] = WrapFactory(current, client)
default:
return nil, fmt.Errorf("provier[%d]: %w %T", idx, ErrUnknowType, prov)
}
@@ -43,60 +34,6 @@ func New(providers ...any) (*Client, error) {
return client, nil
}
type provider struct {
mu sync.Mutex
done uint32
provider Provider
factory func(ctx context.Context) (Provider, error)
}
func (p *provider) Watch(ctx context.Context, callback WatchCallback, path ...string) error {
if err := p.init(ctx); err != nil {
return fmt.Errorf("init read:%w", err)
}
watch, ok := p.provider.(WatchProvider)
if !ok {
return nil
}
if err := watch.Watch(ctx, callback, path...); err != nil {
return fmt.Errorf("factory provider: %w", err)
}
return nil
}
func (p *provider) Value(ctx context.Context, path ...string) (Value, error) {
if err := p.init(ctx); err != nil {
return nil, fmt.Errorf("init read:%w", err)
}
variable, err := p.provider.Value(ctx, path...)
if err != nil {
return nil, fmt.Errorf("factory provider: %w", err)
}
return variable, nil
}
func (p *provider) init(ctx context.Context) error {
if atomic.LoadUint32(&p.done) == 0 {
if !p.mu.TryLock() {
return fmt.Errorf("%w", ErrInitFactory)
}
defer atomic.StoreUint32(&p.done, 1)
defer p.mu.Unlock()
var err error
if p.provider, err = p.factory(ctx); err != nil {
return fmt.Errorf("init provider factory:%w", err)
}
}
return nil
}
type Client struct {
providers []Provider
}
@@ -114,7 +51,7 @@ func (c *Client) Value(ctx context.Context, path ...string) (Value, error) {
for _, provider := range c.providers {
value, err = provider.Value(ctx, path...)
if err == nil || (!errors.Is(err, ErrValueNotFound) && !errors.Is(err, ErrInitFactory)) {
if err == nil || (!errors.Is(err, ErrNotFound) && !errors.Is(err, ErrInitFactory)) {
break
}
}
@@ -135,7 +72,7 @@ func (c *Client) Watch(ctx context.Context, callback WatchCallback, path ...stri
err := provider.Watch(ctx, callback, path...)
if err != nil {
if errors.Is(err, ErrValueNotFound) || errors.Is(err, ErrInitFactory) {
if errors.Is(err, ErrNotFound) || errors.Is(err, ErrInitFactory) {
continue
}
@@ -145,3 +82,18 @@ func (c *Client) Watch(ctx context.Context, callback WatchCallback, path ...stri
return nil
}
func (c *Client) Bind(ctx context.Context, data Variables) error {
for idx, prov := range c.providers {
provider, ok := prov.(BindProvider)
if !ok {
continue
}
if err := provider.Bind(ctx, data); err != nil {
return fmt.Errorf("bind[%d] %v:%w", idx, provider.Name(), err)
}
}
return nil
}

View File

@@ -42,14 +42,14 @@ func ExampleClient_Value() {
port, err := config.Value(ctx, "listen")
if err != nil {
log.Print("listen", err)
log.Print("listen: ", err)
return
}
hostValue, err := config.Value(ctx, "host")
if err != nil {
log.Print("host ", err)
log.Print("host:", err)
return
}

View File

@@ -1,7 +1,6 @@
version: '3'
services:
vault:
container_name: vault
image: vault:1.13.3
cap_add:
- IPC_LOCK
@@ -10,8 +9,11 @@ services:
environment:
VAULT_DEV_ROOT_TOKEN_ID: "dev"
etcd:
image: bitnami/etcd:3.5.11
container_name: etcd
image: quay.io/coreos/etcd:v3.6.7
environment:
ALLOW_NONE_AUTHENTICATION: "yes"
ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
ETCD_ADVERTISE_CLIENT_URLS: "http://etcd:2379"
ports:
- "2379:2379"

4
definition/defenition.go Executable file → Normal file
View File

@@ -4,9 +4,9 @@ import (
"gitoa.ru/go-4devs/config"
)
func New() *Definition {
func New(opts ...config.Option) *Definition {
return &Definition{
options: nil,
options: opts,
}
}

View File

@@ -0,0 +1,206 @@
// Code generated gitoa.ru/go-4devs/config DO NOT EDIT.
package example
import (
context "context"
fmt "fmt"
config "gitoa.ru/go-4devs/config"
)
func WithInputConfigLog(log func(context.Context, string, ...any)) func(*InputConfig) {
return func(ci *InputConfig) {
ci.log = log
}
}
func NewInputConfig(prov config.Provider, opts ...func(*InputConfig)) InputConfig {
i := InputConfig{
Provider: prov,
log: func(_ context.Context, format string, args ...any) {
fmt.Printf(format, args...)
},
}
for _, opt := range opts {
opt(&i)
}
return i
}
type InputConfig struct {
config.Provider
log func(context.Context, string, ...any)
}
// readTest test string.
func (i InputConfig) readTest(ctx context.Context) (v string, e error) {
val, err := i.Value(ctx, "test")
if err != nil {
return v, fmt.Errorf("read [%v]:%w", []string{"test"}, err)
}
return val.ParseString()
}
// ReadTest test string.
func (i InputConfig) ReadTest() (string, error) {
return i.readTest(context.Background())
}
// Test test string.
func (i InputConfig) Test() string {
val, err := i.readTest(context.Background())
if err != nil {
i.log(context.Background(), "get [%v]: %v", []string{"test"}, err)
}
return val
}
type InputConfigUser struct {
InputConfig
}
// User configure user.
func (i InputConfig) User() InputConfigUser {
return InputConfigUser{i}
}
// readName name.
func (i InputConfigUser) readName(ctx context.Context) (v string, e error) {
val, err := i.Value(ctx, "user", "name")
if err != nil {
i.log(context.Background(), "read [%v]: %v", []string{"user", "name"}, err)
return "4devs", nil
}
return val.ParseString()
}
// ReadName name.
func (i InputConfigUser) ReadName(ctx context.Context) (string, error) {
return i.readName(ctx)
}
// Name name.
func (i InputConfigUser) Name(ctx context.Context) string {
val, err := i.readName(ctx)
if err != nil {
i.log(ctx, "get [%v]: %v", []string{"user", "name"}, err)
}
return val
}
// readPass password.
func (i InputConfigUser) readPass(ctx context.Context) (v string, e error) {
val, err := i.Value(ctx, "user", "pass")
if err != nil {
return v, fmt.Errorf("read [%v]:%w", []string{"user", "pass"}, err)
}
return val.ParseString()
}
// ReadPass password.
func (i InputConfigUser) ReadPass(ctx context.Context) (string, error) {
return i.readPass(ctx)
}
// Pass password.
func (i InputConfigUser) Pass(ctx context.Context) string {
val, err := i.readPass(ctx)
if err != nil {
i.log(ctx, "get [%v]: %v", []string{"user", "pass"}, err)
}
return val
}
type InputConfigLog struct {
InputConfig
}
// Log configure logger.
func (i InputConfig) Log() InputConfigLog {
return InputConfigLog{i}
}
// readLevel log level.
func (i InputConfigLog) readLevel(ctx context.Context) (v Level, e error) {
val, err := i.Value(ctx, "log", "level")
if err != nil {
return v, fmt.Errorf("read [%v]:%w", []string{"log", "level"}, err)
}
pval, perr := val.ParseString()
if perr != nil {
return v, fmt.Errorf("read [%v]:%w", []string{"log", "level"}, perr)
}
return v, v.UnmarshalText([]byte(pval))
}
// ReadLevel log level.
func (i InputConfigLog) ReadLevel(ctx context.Context) (Level, error) {
return i.readLevel(ctx)
}
// Level log level.
func (i InputConfigLog) Level(ctx context.Context) Level {
val, err := i.readLevel(ctx)
if err != nil {
i.log(ctx, "get [%v]: %v", []string{"log", "level"}, err)
}
return val
}
type InputConfigLogService struct {
InputConfigLog
service string
}
// Service servise logger.
func (i InputConfigLog) Service(key string) InputConfigLogService {
return InputConfigLogService{i, key}
}
// readLevel log level.
func (i InputConfigLogService) readLevel(ctx context.Context) (v Level, e error) {
val, err := i.Value(ctx, "log", i.service, "level")
if err != nil {
return v, fmt.Errorf("read [%v]:%w", []string{"log", i.service, "level"}, err)
}
pval, perr := val.ParseString()
if perr != nil {
return v, fmt.Errorf("read [%v]:%w", []string{"log", i.service, "level"}, perr)
}
return v, v.UnmarshalText([]byte(pval))
}
// ReadLevel log level.
func (i InputConfigLogService) ReadLevel(ctx context.Context) (Level, error) {
return i.readLevel(ctx)
}
// Level log level.
func (i InputConfigLogService) Level(ctx context.Context) Level {
val, err := i.readLevel(ctx)
if err != nil {
i.log(ctx, "get [%v]: %v", []string{"log", i.service, "level"}, err)
}
return val
}

View File

@@ -0,0 +1,59 @@
package example_test
import (
"context"
"os"
"testing"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/generate"
"gitoa.ru/go-4devs/config/definition/generate/bootstrap"
"gitoa.ru/go-4devs/config/definition/generate/example"
)
func TestGenerate_Bootstrap(t *testing.T) {
t.SkipNow()
t.Parallel()
ctx := context.Background()
options := definition.New()
_ = example.Config(ctx, options)
cfg, _ := generate.NewGConfig("./config.go",
generate.WithMethods("Config"),
generate.WithFullPkg("gitoa.ru/go-4devs/config/definition/generate/example"),
)
path, err := bootstrap.Bootstrap(ctx, cfg)
if err != nil {
t.Error(err)
t.FailNow()
}
os.Remove(path)
t.Log(path)
t.FailNow()
}
func TestGenerate_Genereate(t *testing.T) {
t.SkipNow()
t.Parallel()
ctx := context.Background()
options := definition.New()
_ = example.Config(ctx, options)
cfg, _ := generate.NewGConfig("./config.go",
generate.WithMethods("Config"),
generate.WithFullPkg("gitoa.ru/go-4devs/config/definition/generate/example"),
)
err := generate.Generate(ctx, cfg)
if err != nil {
t.Error(err)
t.FailNow()
}
t.FailNow()
}

0
definition/generate/pkg/imports.go Executable file → Normal file
View File

View File

@@ -1,8 +1,6 @@
package render
import (
"log"
"gitoa.ru/go-4devs/config/definition/generate/pkg"
"gitoa.ru/go-4devs/config/definition/generate/view"
)
@@ -53,8 +51,6 @@ func (d ViewData) Value(name, val string) string {
}
func (d ViewData) Default(name string) string {
log.Print(d.View.Default())
return Data(d.View.Default(), name, d)
}

View File

@@ -4,7 +4,7 @@ import (
"strings"
"gitoa.ru/go-4devs/config/definition/generate/pkg"
"gitoa.ru/go-4devs/config/definition/key"
"gitoa.ru/go-4devs/config/key"
)
func Keys(keys []string, val string) string {

0
definition/generate/render/tpl/data/any.go.tpl Executable file → Normal file
View File

0
definition/generate/render/tpl/data/parse.go.tpl Executable file → Normal file
View File

View File

View File

0
definition/generate/render/tpl/option/option.go.tpl Executable file → Normal file
View File

View File

@@ -67,7 +67,7 @@ func newView(name string, get param.Params, in any, opts ...Option) View {
kind: reflect.TypeOf(in),
name: name,
Params: get,
dtype: option.DataType(get),
dtype: param.Type(get),
children: nil,
keys: nil,
parent: "",

0
definition/group/group.go Executable file → Normal file
View File

View File

@@ -22,7 +22,7 @@ func (o Error) Unwrap() error {
return o.Err
}
func Err(err error, key ...string) Error {
func Err(err error, key []string) Error {
return Error{
Key: key,
Err: err,

View File

@@ -13,7 +13,6 @@ const (
paramRequired
paramSlice
paramBool
paramType
paramPos
paramShort
)
@@ -30,15 +29,17 @@ func ParamShort(fn param.Params) (string, bool) {
return data, ok
}
func HasShort(short string, fn param.Params) bool {
data, ok := param.String(fn, paramShort)
func HasShort(short string) param.Has {
return func(fn param.Params) bool {
data, ok := param.String(fn, paramShort)
return ok && data == short
return ok && data == short
}
}
func WithType(in any) param.Option {
return func(v param.Params) param.Params {
out := param.With(v, paramType, in)
out := param.WithType(in)(v)
if _, ok := in.(bool); ok {
return param.With(out, paramBool, ok)
}
@@ -117,12 +118,6 @@ func IsRequired(fn param.Params) bool {
return ok && data
}
func DataType(fn param.Params) any {
param, _ := fn.Param(paramType)
return param
}
func DataDescription(fn param.Params) string {
data, _ := param.String(fn, paramDesc)

View File

@@ -2,8 +2,8 @@ package proto
import (
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/key"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/key"
"gitoa.ru/go-4devs/config/param"
)

View File

@@ -1,11 +1,20 @@
package config
import "errors"
import (
"errors"
"fmt"
)
var (
ErrValueNotFound = errors.New("value not found")
ErrInvalidValue = errors.New("invalid value")
ErrUnknowType = errors.New("unknow type")
ErrInitFactory = errors.New("init factory")
ErrStopWatch = errors.New("stop watch")
ErrInvalidValue = errors.New("invalid value")
ErrUnknowType = errors.New("unknow type")
ErrInitFactory = errors.New("init factory")
ErrStopWatch = errors.New("stop watch")
ErrNotFound = errors.New("not found")
ErrValueNotFound = fmt.Errorf("value %w", ErrNotFound)
ErrToManyArgs = errors.New("to many args")
ErrWrongType = errors.New("wrong type")
ErrInvalidName = errors.New("ivalid name")
ErrUnexpectedType = errors.New("unexpected type")
ErrRequired = errors.New("required")
)

97
factory.go Normal file
View File

@@ -0,0 +1,97 @@
package config
import (
"context"
"fmt"
"sync"
"sync/atomic"
)
func WrapFactory(fn Factory, prov Provider) *WrapProvider {
return &WrapProvider{
factory: func(ctx context.Context) (Provider, error) {
return fn(ctx, prov)
},
mu: sync.Mutex{},
done: 0,
provider: nil,
}
}
type WrapProvider struct {
mu sync.Mutex
done uint32
provider Provider
factory func(ctx context.Context) (Provider, error)
}
func (p *WrapProvider) Watch(ctx context.Context, callback WatchCallback, path ...string) error {
if err := p.init(ctx); err != nil {
return fmt.Errorf("init read:%w", err)
}
watch, ok := p.provider.(WatchProvider)
if !ok {
return nil
}
if err := watch.Watch(ctx, callback, path...); err != nil {
return fmt.Errorf("factory provider: %w", err)
}
return nil
}
func (p *WrapProvider) Value(ctx context.Context, path ...string) (Value, error) {
if err := p.init(ctx); err != nil {
return nil, fmt.Errorf("init read:%w", err)
}
variable, err := p.provider.Value(ctx, path...)
if err != nil {
return nil, fmt.Errorf("factory provider: %w", err)
}
return variable, nil
}
func (p *WrapProvider) Name() string {
if err := p.init(context.Background()); err != nil {
return fmt.Sprintf("%T", p.provider)
}
return p.provider.Name()
}
func (p *WrapProvider) Bind(ctx context.Context, data Variables) error {
if err := p.init(ctx); err != nil {
return fmt.Errorf("init bind: %w", err)
}
prov, ok := p.provider.(BindProvider)
if !ok {
return nil
}
if perr := prov.Bind(ctx, data); perr != nil {
return fmt.Errorf("init bind provider: %w", perr)
}
return nil
}
func (p *WrapProvider) init(ctx context.Context) error {
if atomic.LoadUint32(&p.done) == 0 {
if !p.mu.TryLock() {
return fmt.Errorf("%w", ErrInitFactory)
}
defer atomic.StoreUint32(&p.done, 1)
defer p.mu.Unlock()
var err error
if p.provider, err = p.factory(ctx); err != nil {
return fmt.Errorf("init provider factory:%w", err)
}
}
return nil
}

121
key/map.go Normal file
View File

@@ -0,0 +1,121 @@
package key
import (
"strings"
)
const (
prefixByPath = "byPath"
)
func newMap() *Map {
return &Map{
idx: 0,
wild: nil,
children: nil,
}
}
type Map struct {
idx int
wild *Map
children map[string]*Map
}
func ByPath(name, sep string) []string {
return []string{prefixByPath, name, sep}
}
func (m *Map) Index(path []string) (int, bool) {
if data, ok := m.find(path); ok {
return data.idx, true
}
if len(path) == 3 && path[0] == prefixByPath {
data, ok := m.byPath(path[1], path[2])
return data.idx, ok
}
return 0, false
}
func (m *Map) Add(idx int, path []string) {
m.add(path).idx = idx
}
func (m *Map) add(path []string) *Map {
name, path := path[0], path[1:]
if IsWild(name) {
m.wild = newMap()
return m.wild.add(path)
}
if m.children == nil {
m.children = map[string]*Map{}
}
if _, ok := m.children[name]; !ok {
m.children[name] = newMap()
}
if len(path) > 0 {
return m.children[name].add(path)
}
return m.children[name]
}
func (m *Map) byPath(path, sep string) (*Map, bool) {
for name := range m.children {
if after, ok := strings.CutPrefix(path, name); ok {
data := m.children[name]
if len(after) == 0 {
return data, true
}
after, ok = strings.CutPrefix(after, sep)
if !ok {
return data, false
}
if data.wild == nil {
return data.byPath(after, sep)
}
if idx := strings.Index(after, sep); idx != -1 {
return data.wild.byPath(after[idx+1:], sep)
}
return data, false
}
}
return m, false
}
func (m *Map) find(path []string) (*Map, bool) {
name := path[0]
last := len(path) == 1
if !last {
path = path[1:]
}
if m.wild != nil {
return m.wild.find(path)
}
data, ok := m.children[name]
if !ok {
return data, false
}
if last {
return data, data.children == nil
}
return data.find(path)
}

83
key/map_test.go Normal file
View File

@@ -0,0 +1,83 @@
package key_test
import (
"testing"
"gitoa.ru/go-4devs/config/key"
)
func TestMap_ByPath(t *testing.T) {
t.Parallel()
const (
expID int = 1
newID int = 42
)
data := key.Map{}
data.Add(expID, []string{"test", "data", "three"})
data.Add(expID, []string{"test", "other"})
data.Add(newID, []string{"new", "{data}", "test"})
idx, ok := data.Index(key.ByPath("test-other", "-"))
if !ok {
t.Error("key not found")
}
if idx != expID {
t.Errorf("idx exp:%v got:%v", expID, idx)
}
if nidx, nok := data.Index(key.ByPath("new-service-test", "-")); !nok && nidx != newID {
t.Errorf("idx exp:%v got:%v", newID, nidx)
}
}
func TestMap_Add(t *testing.T) {
t.Parallel()
const (
expID int = 1
newID int = 42
)
data := key.Map{}
data.Add(expID, []string{"test", "data"})
data.Add(expID, []string{"test", "other"})
data.Add(newID, []string{"new"})
idx, ok := data.Index([]string{"test", "data"})
if !ok {
t.Error("key not found")
}
if idx != expID {
t.Errorf("idx exp:%v got:%v", expID, idx)
}
if nidx, nok := data.Index([]string{"new"}); !nok && nidx != newID {
t.Errorf("idx exp:%v got:%v", newID, nidx)
}
}
func TestMap_Wild(t *testing.T) {
t.Parallel()
const (
expID int = 1
newID int = 42
)
data := key.Map{}
data.Add(expID, []string{"test", "{data}", "id"})
data.Add(newID, []string{"new", "data"})
idx, ok := data.Index([]string{"test", "data", "id"})
if !ok {
t.Error("key not found")
}
if idx != expID {
t.Errorf("idx exp:%v got:%v", expID, idx)
}
}

View File

@@ -1,7 +1,5 @@
package param
import "gitoa.ru/go-4devs/config"
func String(fn Params, key any) (string, bool) {
val, ok := fn.Param(key)
if !ok {
@@ -24,17 +22,6 @@ func Bool(key any, fn Params) (bool, bool) {
return data, ok
}
func Value(key any, fn Params) (config.Value, bool) {
data, ok := fn.Param(key)
if !ok {
return nil, false
}
res, ok := data.(config.Value)
return res, ok
}
func Uint64(key any, fn Params) (uint64, bool) {
data, ok := fn.Param(key)
if !ok {

View File

@@ -4,6 +4,7 @@ type key int
const (
paramTimeFormat key = iota + 1
paramType
)
func WithTimeFormat(format string) Option {
@@ -15,3 +16,15 @@ func WithTimeFormat(format string) Option {
func TimeFormat(fn Params) (string, bool) {
return String(fn, paramTimeFormat)
}
func WithType(in any) Option {
return func(v Params) Params {
return With(v, paramType, in)
}
}
func Type(fn Params) any {
param, _ := fn.Param(paramType)
return param
}

View File

@@ -9,7 +9,7 @@ var (
)
type Option func(p Params) Params
type Has func(Params) bool
type Params interface {
Param(key any) (any, bool)
}

View File

@@ -1,14 +1,14 @@
package config
import "context"
import (
"context"
"gitoa.ru/go-4devs/config/param"
)
type Provider interface {
Value(ctx context.Context, path ...string) (Value, error)
}
type NamedProvider interface {
Name() string
Provider
}
type WatchCallback func(ctx context.Context, oldVar, newVar Value) error
@@ -33,10 +33,18 @@ type Options interface {
Options() []Option
}
type BindProvider interface {
Provider
Bind(ctx context.Context, data Variables) error
}
type Variables interface {
ByName(name ...string) (Variable, error)
ByParam(filter param.Has) (Variable, error)
Variables() []Variable
}
type Definition interface {
Add(opts ...Option)
}
type BindProvider interface {
Bind(ctx context.Context, def Definition)
}

59
provider/arg/option.go Normal file
View File

@@ -0,0 +1,59 @@
package arg
import (
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/param"
"gitoa.ru/go-4devs/config/value"
)
func Default(in any) param.Option {
return option.Default(value.New(in))
}
func Required(v param.Params) {
option.Required(v)
}
func Slice(v param.Params) {
option.Slice(v)
}
func String(name, description string, opts ...param.Option) option.Option {
return option.String(name, description, append(opts, Argument)...)
}
func Bool(name, description string, opts ...param.Option) option.Option {
return option.Bool(name, description, append(opts, Argument)...)
}
func Duration(name, description string, opts ...param.Option) option.Option {
return option.Duration(name, description, append(opts, Argument)...)
}
func Float64(name, description string, opts ...param.Option) option.Option {
return option.Float64(name, description, append(opts, Argument)...)
}
func Int(name, description string, opts ...param.Option) option.Option {
return option.Int(name, description, append(opts, Argument)...)
}
func Int64(name, description string, opts ...param.Option) option.Option {
return option.Int64(name, description, append(opts, Argument)...)
}
func Time(name, description string, opts ...param.Option) option.Option {
return option.Time(name, description, append(opts, Argument)...)
}
func Uint(name, description string, opts ...param.Option) option.Option {
return option.Uint(name, description, append(opts, Argument)...)
}
func Uint64(name, descriontion string, opts ...param.Option) option.Option {
return option.Uint64(name, descriontion, append(opts, Argument)...)
}
func Err(err error, key ...string) option.Error {
return option.Err(err, key)
}

38
provider/arg/param.go Normal file
View File

@@ -0,0 +1,38 @@
package arg
import (
"sync/atomic"
"gitoa.ru/go-4devs/config/param"
)
type keyParam int
const (
paramArgument keyParam = iota + 1
)
//nolint:gochecknoglobals
var argNum uint64
func Argument(v param.Params) param.Params {
return param.With(v, paramArgument, atomic.AddUint64(&argNum, 1))
}
func ParamArgument(fn param.Params) (uint64, bool) {
return param.Uint64(paramArgument, fn)
}
func PosArgument(in uint64) param.Has {
return func(p param.Params) bool {
idx, ok := ParamArgument(p)
return ok && idx == in
}
}
func HasArgument(fn param.Params) bool {
_, ok := ParamArgument(fn)
return ok
}

View File

@@ -2,86 +2,230 @@ package arg
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/value"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/key"
"gitoa.ru/go-4devs/config/param"
"gitoa.ru/go-4devs/config/provider/memory"
)
const Name = "arg"
const (
doubleDash = `--`
defaultLenLognOption = 2
dash = `-`
)
var _ config.Provider = (*Provider)(nil)
func WithSkip(skip int) func(*Argv) {
return func(ar *Argv) {
res := 2
type Option func(*Provider)
switch {
case skip > 0 && len(os.Args) > skip:
res = skip
case skip > 0:
res = len(os.Args)
case len(os.Args) == 1:
res = 1
case len(os.Args) > 1 && os.Args[1][0] == '-':
res = 1
}
func WithKeyFactory(factory func(s ...string) string) Option {
return func(p *Provider) { p.key = factory }
ar.skip = res
}
}
func New(opts ...Option) *Provider {
prov := Provider{
key: func(s ...string) string {
return strings.Join(s, "-")
},
args: make(map[string][]string, len(os.Args[1:])),
name: Name,
func New(opts ...func(*Argv)) *Argv {
arg := &Argv{
args: nil,
pos: 0,
Map: memory.Map{},
skip: 1,
}
for _, opt := range opts {
opt(&prov)
opt(arg)
}
return &prov
arg.args = os.Args[arg.skip:]
return arg
}
type Provider struct {
args map[string][]string
key func(...string) string
name string
type Argv struct {
memory.Map
args []string
pos uint64
skip int
}
func (p *Provider) Name() string {
return p.name
}
func (i *Argv) Value(ctx context.Context, key ...string) (config.Value, error) {
if err := i.parse(); err != nil {
return nil, fmt.Errorf("parse:%w", err)
}
func (p *Provider) Value(_ context.Context, path ...string) (config.Value, error) {
err := p.parse()
data, err := i.Map.Value(ctx, key...)
if err != nil {
return nil, err
return nil, fmt.Errorf("%w", err)
}
name := p.key(path...)
if val, ok := p.args[name]; ok {
return data, nil
}
func (i *Argv) Bind(ctx context.Context, def config.Variables) error {
options := true
for len(i.args) > 0 {
var err error
arg := i.args[0]
i.args = i.args[1:]
switch {
case len(val) == 1:
return value.JString(val[0]), nil
default:
data, jerr := json.Marshal(val)
if jerr != nil {
return nil, fmt.Errorf("failed load data:%w", jerr)
case options && arg == doubleDash:
options = false
case options && len(arg) > 2 && arg[0:2] == doubleDash:
err = i.parseLongOption(arg[2:], def)
case options && arg[0:1] == "-":
if len(arg) == 1 {
return fmt.Errorf("%w: option name required given '-'", config.ErrInvalidName)
}
return value.Decode(func(v any) error {
err := json.Unmarshal(data, v)
if err != nil {
return fmt.Errorf("unmarshal:%w", err)
}
err = i.parseShortOption(arg[1:], def)
default:
err = i.parseArgument(arg, def)
}
return nil
}), nil
if err != nil {
return fmt.Errorf("%w", err)
}
}
return nil, fmt.Errorf("%s:%w", p.Name(), config.ErrValueNotFound)
if err := i.Map.Bind(ctx, def); err != nil {
return fmt.Errorf("%w", err)
}
return nil
}
func (i *Argv) parseLongOption(arg string, def config.Variables) error {
var value *string
name := arg
if strings.Contains(arg, "=") {
vals := strings.SplitN(arg, "=", defaultLenLognOption)
name = vals[0]
value = &vals[1]
}
opt, err := def.ByName(key.ByPath(name, dash)...)
if err != nil {
return Err(err, name)
}
return i.appendOption(value, opt)
}
func (i *Argv) appendOption(data *string, opt config.Variable) error {
if i.HasOption(opt.Key()...) && !option.IsSlice(opt) {
return fmt.Errorf("%w: got: array, expect: %T", config.ErrUnexpectedType, param.Type(opt))
}
var val string
switch {
case data != nil:
val = *data
case option.IsBool(opt):
val = "true"
case len(i.args) > 0 && len(i.args[0]) > 0 && i.args[0][0:1] != "-":
val = i.args[0]
i.args = i.args[1:]
default:
return Err(config.ErrRequired, opt.Key()...)
}
err := i.AppendOption(val, opt.Key()...)
if err != nil {
return Err(err, opt.Key()...)
}
return nil
}
func (i *Argv) parseShortOption(arg string, def config.Variables) error {
name := arg
var value string
if len(name) > 1 {
name, value = arg[0:1], arg[1:]
}
opt, err := def.ByParam(option.HasShort(name))
if err != nil {
return fmt.Errorf("%w", err)
}
if option.IsBool(opt) && value != "" {
err := i.parseShortOption(value, def)
if err != nil {
return err
}
value = ""
}
if value == "" {
return i.appendOption(nil, opt)
}
return i.appendOption(&value, opt)
}
func (i *Argv) parseArgument(arg string, def config.Variables) error {
opt, err := def.ByParam(PosArgument(i.pos))
if err != nil {
return fmt.Errorf("%w", err)
}
i.pos++
if err := i.AppendOption(arg, opt.Key()...); err != nil {
return Err(err, opt.Key()...)
}
return nil
}
func (i *Argv) parse() error {
if i.Len() > 0 {
return nil
}
for _, arg := range i.args {
name, value, err := i.parseOne(arg)
if err != nil {
return err
}
if name != "" {
if err := i.AppendOption(value, name); err != nil {
return fmt.Errorf("append %v: %w", name, err)
}
}
}
return nil
}
// parseOne return name, value, error.
//
//nolint:cyclop
func (p *Provider) parseOne(arg string) (string, string, error) {
func (i *Argv) parseOne(arg string) (string, string, error) {
if arg[0] != '-' {
return "", "", nil
}
@@ -118,22 +262,3 @@ func (p *Provider) parseOne(arg string) (string, string, error) {
return name, val, nil
}
func (p *Provider) parse() error {
if len(p.args) > 0 {
return nil
}
for _, arg := range os.Args[1:] {
name, value, err := p.parseOne(arg)
if err != nil {
return err
}
if name != "" {
p.args[name] = append(p.args[name], value)
}
}
return nil
}

42
provider/chain/chain.go Normal file
View File

@@ -0,0 +1,42 @@
package chain
import (
"context"
"fmt"
"gitoa.ru/go-4devs/config"
)
const Name = "chain"
func New(c ...config.Provider) config.BindProvider {
return chain(c)
}
type chain []config.Provider
func (c chain) Value(ctx context.Context, name ...string) (config.Value, error) {
for _, in := range c {
if val, err := in.Value(ctx, name...); err == nil {
return val, nil
}
}
return nil, fmt.Errorf("%w", config.ErrNotFound)
}
func (c chain) Bind(ctx context.Context, def config.Variables) error {
for _, input := range c {
if prov, ok := input.(config.BindProvider); ok {
if err := prov.Bind(ctx, def); err != nil {
return fmt.Errorf("%T:%w", input, err)
}
}
}
return nil
}
func (c chain) Name() string {
return Name
}

View File

@@ -52,5 +52,5 @@ func (p *Provider) Value(_ context.Context, path ...string) (config.Value, error
return value.JString(val), nil
}
return nil, fmt.Errorf("%v:%w", p.Name(), config.ErrValueNotFound)
return nil, fmt.Errorf("%v:%w", p.Name(), config.ErrNotFound)
}

View File

@@ -38,7 +38,7 @@ func NewEtcd(ctx context.Context) (*client.Client, error) {
}
for name, val := range values {
_, err = et.KV.Put(ctx, name, val)
_, err = et.Put(ctx, name, val)
if err != nil {
return nil, err
}

View File

@@ -1,6 +1,6 @@
module gitoa.ru/go-4devs/config/provider/etcd
go 1.21
go 1.22
require (
github.com/stretchr/testify v1.8.4

View File

@@ -3,6 +3,7 @@ package etcd
import (
"context"
"fmt"
"log"
"strings"
"gitoa.ru/go-4devs/config"
@@ -34,6 +35,9 @@ func New(namespace, appName string, client Client) *Provider {
},
name: Name,
prefix: namespace + Separator + appName,
log: func(_ context.Context, format string, args ...any) {
log.Printf(format, args...)
},
}
return &prov
@@ -102,6 +106,7 @@ func (p *Provider) getEventKvs(events []*client.Event) ([]*pb.KeyValue, []*pb.Ke
return kvs, old
}
//nolint:nilnil
func (p *Provider) resolve(key string, kvs []*pb.KeyValue) (config.Value, error) {
for _, kv := range kvs {
switch {

View File

@@ -65,7 +65,7 @@ func ExampleClient_Watch() {
return
}
_, err = etcdClient.KV.Put(ctx, "fdevs/config/example_db_dsn", "pgsql://user@pass:127.0.0.1:5432")
_, err = etcdClient.Put(ctx, "fdevs/config/example_db_dsn", "pgsql://user@pass:127.0.0.1:5432")
if err != nil {
log.Print(err)
@@ -75,7 +75,7 @@ func ExampleClient_Watch() {
defer func() {
cancel()
if _, err = etcdClient.KV.Delete(context.Background(), "fdevs/config/example_db_dsn"); err != nil {
if _, err = etcdClient.Delete(context.Background(), "fdevs/config/example_db_dsn"); err != nil {
log.Print(err)
return
@@ -107,7 +107,7 @@ func ExampleClient_Watch() {
}
time.AfterFunc(time.Second, func() {
if _, err := etcdClient.KV.Put(ctx, "fdevs/config/example_db_dsn", "mysql://localhost:5432"); err != nil {
if _, err := etcdClient.Put(ctx, "fdevs/config/example_db_dsn", "mysql://localhost:5432"); err != nil {
log.Print(err)
return

View File

@@ -55,7 +55,7 @@ func TestWatcher(t *testing.T) {
require.NoError(t, err)
defer func() {
_, err = et.KV.Delete(context.Background(), "fdevs/config/test_watch")
_, err = et.Delete(context.Background(), "fdevs/config/test_watch")
require.NoError(t, err)
}()
@@ -66,7 +66,7 @@ func TestWatcher(t *testing.T) {
wg.Add(6)
watch := func(cnt *int32) config.WatchCallback {
return func(ctx context.Context, oldVar, newVar config.Value) error {
return func(_ context.Context, oldVar, newVar config.Value) error {
switch *cnt {
case 0:
assert.Equal(t, value(*cnt), newVar.String())
@@ -77,7 +77,7 @@ func TestWatcher(t *testing.T) {
case 2:
_, perr := newVar.ParseString()
require.NoError(t, perr)
assert.Equal(t, "", newVar.String())
assert.Empty(t, newVar.String())
assert.Equal(t, value(*cnt-1), oldVar.String())
default:
t.Error("unexpected watch")
@@ -96,11 +96,11 @@ func TestWatcher(t *testing.T) {
require.NoError(t, err)
time.AfterFunc(time.Second, func() {
_, err = et.KV.Put(ctx, "fdevs/config/test_watch", value(0))
_, err = et.Put(ctx, "fdevs/config/test_watch", value(0))
require.NoError(t, err)
_, err = et.KV.Put(ctx, "fdevs/config/test_watch", value(1))
_, err = et.Put(ctx, "fdevs/config/test_watch", value(1))
require.NoError(t, err)
_, err = et.KV.Delete(ctx, "fdevs/config/test_watch")
_, err = et.Delete(ctx, "fdevs/config/test_watch")
require.NoError(t, err)
})

View File

@@ -1,6 +1,6 @@
module gitoa.ru/go-4devs/config/provider/ini
go 1.21
go 1.22
require (
github.com/stretchr/testify v1.8.4

View File

@@ -1,6 +1,6 @@
module gitoa.ru/go-4devs/config/provider/json
go 1.21
go 1.22
require (
github.com/tidwall/gjson v1.17.0

View File

@@ -1,4 +1,4 @@
package json
package json //nolint:revive
import (
"context"
@@ -25,6 +25,7 @@ func New(json []byte, opts ...Option) *Provider {
return strings.Join(s, Separator)
},
data: json,
name: Name,
}
for _, opt := range opts {

44
provider/memory/def.go Normal file
View File

@@ -0,0 +1,44 @@
package memory
import (
"context"
"fmt"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
)
const NameDefault = "default"
var _ config.BindProvider = (*Default)(nil)
type Default struct {
data Map
name string
}
func (a *Default) Value(ctx context.Context, key ...string) (config.Value, error) {
if v, err := a.data.Value(ctx, key...); err == nil {
return v, nil
}
return nil, fmt.Errorf("%w", config.ErrNotFound)
}
func (a *Default) Bind(_ context.Context, def config.Variables) error {
for _, opt := range def.Variables() {
if data, ok := option.DataDefaut(opt); ok {
a.data.SetOption(data, opt.Key()...)
}
}
return nil
}
func (a *Default) Name() string {
if a.name != "" {
return a.name
}
return NameDefault
}

92
provider/memory/map.go Normal file
View File

@@ -0,0 +1,92 @@
package memory
import (
"context"
"fmt"
"sync"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/key"
"gitoa.ru/go-4devs/config/value"
)
const NameMap = "map"
var _ config.BindProvider = (*Map)(nil)
type Map struct {
mu sync.Mutex
vals []config.Value
idx key.Map
name string
}
func (m *Map) Len() int {
return len(m.vals)
}
func (m *Map) Value(_ context.Context, key ...string) (config.Value, error) {
m.mu.Lock()
defer m.mu.Unlock()
idx, ok := m.idx.Index(key)
if !ok {
return nil, fmt.Errorf("%w", config.ErrNotFound)
}
val := m.vals[idx]
return val, nil
}
func (m *Map) Bind(_ context.Context, _ config.Variables) error {
return nil
}
func (m *Map) Name() string {
if m.name != "" {
return m.name
}
return NameMap
}
func (m *Map) HasOption(key ...string) bool {
_, ok := m.idx.Index(key)
return ok
}
func (m *Map) SetOption(val any, key ...string) {
m.mu.Lock()
defer m.mu.Unlock()
if id, ok := m.idx.Index(key); ok {
m.vals[id] = value.New(val)
return
}
m.idx.Add(len(m.vals), key)
m.vals = append(m.vals, value.New(val))
}
func (m *Map) AppendOption(val string, keys ...string) error {
id, ok := m.idx.Index(keys)
if !ok {
data := value.Strings{val}
m.SetOption(data, keys...)
return nil
}
old, tok := m.vals[id].(value.Strings)
if !tok {
return fmt.Errorf("%w:%T", config.ErrWrongType, m.vals[id])
}
m.SetOption(old.Append(val), keys...)
return nil
}

View File

@@ -0,0 +1,44 @@
package memory
import (
"context"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/validator"
"gitoa.ru/go-4devs/config/value"
)
var _ config.BindProvider = (*Map)(nil)
func Valid(in config.Provider) Validate {
return Validate{
Provider: in,
name: "",
}
}
type Validate struct {
config.Provider
name string
}
func (a Validate) Bind(ctx context.Context, def config.Variables) error {
for _, opt := range def.Variables() {
if val, err := a.Value(ctx, opt.Key()...); err != nil && !value.IsEmpty(val) {
if err := validator.Validate(opt.Key(), opt, val); err != nil {
return option.Err(err, opt.Key())
}
}
}
return nil
}
func (a Validate) Name() string {
if a.name != "" {
return a.name
}
return a.Provider.Name()
}

View File

@@ -1,15 +1,8 @@
module gitoa.ru/go-4devs/config/provider/toml
go 1.21
go 1.22
require (
github.com/pelletier/go-toml v1.9.5
github.com/stretchr/testify v1.8.4
gitoa.ru/go-4devs/config v0.0.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,14 +1,4 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gitoa.ru/go-4devs/config v0.0.1 h1:9KrOO09YbIMO8qL8aVn/G74DurGdOIW5y3O02bays4I=
gitoa.ru/go-4devs/config v0.0.1/go.mod h1:xfEC2Al9xnMLJUuekYs3KhJ5BIzWAseNwkMwbN6/xss=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -34,6 +34,7 @@ func configure(tree *toml.Tree, opts ...Option) *Provider {
key: func(s []string) string {
return strings.Join(s, Separator)
},
name: Name,
}
for _, opt := range opts {

View File

@@ -27,7 +27,7 @@ func (s Value) ParseInt() (int, error) {
return int(v), nil
}
func (s Value) Unmarshal(target interface{}) error {
func (s Value) Unmarshal(target any) error {
b, err := json.Marshal(s.Raw())
if err != nil {
return fmt.Errorf("%w: %w", config.ErrInvalidValue, err)

View File

@@ -1,6 +1,6 @@
module gitoa.ru/go-4devs/config/provider/vault
go 1.21
go 1.22
require (
github.com/hashicorp/vault/api v1.11.0

View File

@@ -68,19 +68,6 @@ func (p *Provider) Key(in []string) (string, string) {
return p.prefix + Separator + path, val
}
func (p *Provider) read(path, key string) (*api.Secret, error) {
secret, err := p.client.Logical().Read(path)
if err != nil {
return nil, fmt.Errorf("read[%s:%s]:%w", path, key, err)
}
if secret == nil && key != ValueName {
return p.read(path+Separator+key, ValueName)
}
return secret, nil
}
func (p *Provider) Value(_ context.Context, key ...string) (config.Value, error) {
path, field := p.Key(key)
@@ -98,7 +85,7 @@ func (p *Provider) Value(_ context.Context, key ...string) (config.Value, error)
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]any)
if !ok {
return nil, fmt.Errorf("%w: path:%s, field:%s, provider:%s", config.ErrValueNotFound, path, field, p.Name())
}
@@ -118,3 +105,16 @@ func (p *Provider) Value(_ context.Context, key ...string) (config.Value, error)
return value.JBytes(md), nil
}
func (p *Provider) read(path, key string) (*api.Secret, error) {
secret, err := p.client.Logical().Read(path)
if err != nil {
return nil, fmt.Errorf("read[%s:%s]:%w", path, key, err)
}
if secret == nil && key != ValueName {
return p.read(path+Separator+key, ValueName)
}
return secret, nil
}

View File

@@ -33,7 +33,7 @@ func NewVault() (*api.Client, error) {
cl.SetToken(tokenID)
values := map[string]map[string]interface{}{
values := map[string]map[string]any{
"database": {
"duration": 1260000000000,
"enabled": true,
@@ -57,9 +57,9 @@ func NewVault() (*api.Client, error) {
return cl, nil
}
func create(host, token, path string, data map[string]interface{}) error {
func create(host, token, path string, data map[string]any) error {
type Req struct {
Data interface{} `json:"data"`
Data any `json:"data"`
}
b, err := json.Marshal(Req{Data: data})

View File

@@ -1,6 +1,6 @@
module gitoa.ru/go-4devs/config/provider/yaml
go 1.21
go 1.22
require (
gitoa.ru/go-4devs/config v0.0.1

View File

@@ -36,9 +36,9 @@ func New(yml []byte, opts ...Option) (*Provider, error) {
}
func create(opts ...Option) *Provider {
prov := Provider{
name: Name,
}
var prov Provider
prov.name = Name
for _, opt := range opts {
opt(&prov)
@@ -65,6 +65,7 @@ func (p *Provider) Value(_ context.Context, path ...string) (config.Value, error
func (p *Provider) With(data *yaml.Node) *Provider {
return &Provider{
data: node{Node: data},
name: p.name,
}
}
@@ -85,7 +86,7 @@ func (n *node) read(name string, keys []string) (config.Value, error) {
return value.Decode(val), nil
}
func getData(node []*yaml.Node, keys []string) (func(interface{}) error, error) {
func getData(node []*yaml.Node, keys []string) (func(any) error, error) {
for idx := len(node) - 1; idx > 0; idx -= 2 {
if node[idx-1].Value == keys[0] {
if len(keys) > 1 {

View File

@@ -67,8 +67,6 @@ func Time(value string) time.Time {
}
// NewRead test data.
//
//nolint:cyclop
func NewRead(expected any, key ...string) Read {
return Read{
Key: key,

View File

@@ -1,6 +1,7 @@
package require
import (
"errors"
"testing"
)
@@ -21,3 +22,12 @@ func NoErrorf(t *testing.T, err error, msg string, args ...any) {
t.FailNow()
}
}
func ErrorIs(t *testing.T, err, ex error, msgAndArgs ...any) {
t.Helper()
if errors.Is(ex, err) {
t.Error(msgAndArgs...)
t.FailNow()
}
}

19
validator/enum.go Normal file
View File

@@ -0,0 +1,19 @@
package validator
import (
"slices"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/param"
)
func Enum(enum ...string) Validator {
return func(_ param.Params, in config.Value) error {
val := in.String()
if slices.Contains(enum, val) {
return nil
}
return NewError(ErrInvalid, val, enum)
}
}

30
validator/enum_test.go Normal file
View File

@@ -0,0 +1,30 @@
package validator_test
import (
"errors"
"testing"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/validator"
"gitoa.ru/go-4devs/config/value"
)
func TestEnum(t *testing.T) {
t.Parallel()
vars := option.String("test", "test")
validValue := value.New("valid")
invalidValue := value.New("invalid")
enum := validator.Enum("valid", "other", "three")
err := enum(vars, validValue)
if err != nil {
t.Errorf("expected valid value got err:%s", err)
}
iErr := enum(vars, invalidValue)
if !errors.Is(iErr, validator.ErrInvalid) {
t.Errorf("expected err:%s, got: %s", validator.ErrInvalid, iErr)
}
}

37
validator/error.go Normal file
View File

@@ -0,0 +1,37 @@
package validator
import (
"errors"
"fmt"
)
var (
ErrInvalid = errors.New("invalid value")
ErrNotBlank = errors.New("not blank")
)
func NewError(err error, value, expect any) Error {
return Error{
err: err,
value: value,
expect: expect,
}
}
type Error struct {
err error
value any
expect any
}
func (e Error) Error() string {
return fmt.Sprintf("%s: expext: %s, given: %s", e.err, e.expect, e.value)
}
func (e Error) Is(err error) bool {
return errors.Is(e.err, err)
}
func (e Error) Unwrap() error {
return e.err
}

83
validator/not_blank.go Normal file
View File

@@ -0,0 +1,83 @@
package validator
import (
"fmt"
"time"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/param"
)
func NotBlank(fn param.Params, in config.Value) error {
dataType := param.Type(fn)
if option.IsSlice(fn) {
return sliceByType(dataType, in)
}
if notBlank(dataType, in) {
return nil
}
return ErrNotBlank
}
func sliceByType(vType any, in config.Value) error {
switch vType.(type) {
case string:
return sliceBy[string](in)
case int:
return sliceBy[int](in)
case int64:
return sliceBy[int64](in)
case uint:
return sliceBy[uint](in)
case uint64:
return sliceBy[uint64](in)
case float64:
return sliceBy[float64](in)
case bool:
return sliceBy[bool](in)
case time.Duration:
return sliceBy[time.Duration](in)
case time.Time:
return sliceBy[time.Time](in)
default:
return sliceBy[any](in)
}
}
func sliceBy[T any](in config.Value) error {
var data []T
if err := in.Unmarshal(&data); err != nil {
return fmt.Errorf("%w:%w", ErrNotBlank, err)
}
if len(data) > 0 {
return nil
}
return fmt.Errorf("%w", ErrNotBlank)
}
func notBlank(vType any, in config.Value) bool {
switch vType.(type) {
case int:
return in.Int() != 0
case int64:
return in.Int64() != 0
case uint:
return in.Uint() != 0
case uint64:
return in.Uint64() != 0
case float64:
return in.Float64() != 0
case time.Duration:
return in.Duration() != 0
case time.Time:
return !in.Time().IsZero()
case string:
return len(in.String()) > 0
default:
return in.Any() != nil
}
}

128
validator/not_blank_test.go Normal file
View File

@@ -0,0 +1,128 @@
package validator_test
import (
"errors"
"testing"
"time"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/validator"
"gitoa.ru/go-4devs/config/value"
)
func TestNotBlank(t *testing.T) {
t.Parallel()
for name, ca := range casesNotBlank() {
valid := validator.NotBlank
if err := valid(ca.vars, ca.value); err != nil {
t.Errorf("case: %s, expected error <nil>, got: %s", name, err)
}
if ca.empty == nil {
ca.empty = value.EmptyValue()
}
emptErr := valid(ca.vars, ca.empty)
if emptErr == nil || !errors.Is(emptErr, validator.ErrNotBlank) {
t.Errorf("case empty: %s, expect: %s, got:%v", name, validator.ErrNotBlank, emptErr)
}
}
}
type cases struct {
vars config.Option
value config.Value
empty config.Value
}
func casesNotBlank() map[string]cases {
return map[string]cases{
"any": {
vars: option.New("any", "any", nil),
value: value.New(float32(1)),
},
"array int": {
vars: option.Int("int", "array int", option.Slice),
value: value.New([]int{1}),
empty: value.New([]int{}),
},
"array int64": {
vars: option.Int64("int64", "array int64", option.Slice),
value: value.New([]int64{1}),
empty: value.New([]int64{}),
},
"array uint": {
vars: option.Uint("uint", "array uint", option.Slice),
value: value.New([]uint{1}),
empty: value.New([]uint{}),
},
"array uint64": {
vars: option.Uint64("uint64", "array uint64", option.Slice),
value: value.New([]uint64{1}),
empty: value.New([]uint64{}),
},
"array float64": {
vars: option.Float64("float64", "array float64", option.Slice),
value: value.New([]float64{0.2}),
empty: value.New([]float64{}),
},
"array bool": {
vars: option.Bool("bool", "array bool", option.Slice),
value: value.New([]bool{true, false}),
empty: value.New([]bool{}),
},
"array duration": {
vars: option.Duration("duration", "array duration", option.Slice),
value: value.New([]time.Duration{time.Second}),
empty: value.New([]time.Duration{}),
},
"array time": {
vars: option.Time("time", "array time", option.Slice),
value: value.New([]time.Time{time.Now()}),
empty: value.New([]time.Time{}),
},
"array string": {
vars: option.String("string", "array string", option.Slice),
value: value.New([]string{"value"}),
empty: value.New([]string{}),
},
"int": {
vars: option.Int("int", "int"),
value: value.New(int(1)),
},
"int64": {
vars: option.Int64("int64", "int64"),
value: value.New(int64(2)),
},
"uint": {
vars: option.Uint("uint", "uint"),
value: value.New(uint(1)),
empty: value.New([]uint{1}),
},
"uint64": {
vars: option.Uint64("uint64", "uint64"),
value: value.New(uint64(10)),
},
"float64": {
vars: option.Float64("float64", "float64"),
value: value.New(float64(.00001)),
},
"duration": {
vars: option.Duration("duration", "duration"),
value: value.New(time.Minute),
empty: value.New("same string"),
},
"time": {
vars: option.Time("time", "time"),
value: value.New(time.Now()),
},
"string": {
vars: option.String("string", "string"),
value: value.New("string"),
empty: value.New(""),
},
}
}

49
validator/valid.go Normal file
View File

@@ -0,0 +1,49 @@
package validator
import (
"fmt"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/param"
)
func Chain(v ...Validator) Validator {
return func(vr param.Params, in config.Value) error {
for _, valid := range v {
err := valid(vr, in)
if err != nil {
return err
}
}
return nil
}
}
const paramValid = "param.valid"
type Validator func(param.Params, config.Value) error
func Valid(in ...Validator) param.Option {
return func(v param.Params) param.Params {
return param.With(v, paramValid, in)
}
}
func Validate(key []string, fn param.Params, in config.Value) error {
params, ok := fn.Param(paramValid)
if !ok {
return nil
}
valids, _ := params.([]Validator)
for _, valid := range valids {
err := valid(fn, in)
if err != nil {
return fmt.Errorf("%s:%w", key, err)
}
}
return nil
}

33
validator/valid_test.go Normal file
View File

@@ -0,0 +1,33 @@
package validator_test
import (
"errors"
"testing"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/validator"
"gitoa.ru/go-4devs/config/value"
)
func TestChain(t *testing.T) {
t.Parallel()
vars := option.String("test", "test")
validValue := value.New("one")
invalidValue := value.New([]string{"one"})
valid := validator.Chain(
validator.NotBlank,
validator.Enum("one", "two"),
)
err := valid(vars, validValue)
if err != nil {
t.Errorf("expected valid value, got: %s", err)
}
ierr := valid(vars, invalidValue)
if !errors.Is(ierr, validator.ErrNotBlank) {
t.Errorf("expected not blank, got:%s", ierr)
}
}

View File

@@ -25,6 +25,7 @@ type ReadValue interface {
Bool() bool
Duration() time.Duration
Time() time.Time
Any() any
}
type ParseValue interface {

View File

@@ -111,6 +111,10 @@ func (s Decode) Time() time.Time {
return in
}
func (s Decode) Any() any {
return s
}
func (s Decode) IsEquals(in config.Value) bool {
return s.String() == in.String()
}

View File

@@ -1,13 +1,28 @@
package value
import (
"errors"
"time"
"gitoa.ru/go-4devs/config"
)
var (
emptyValue = Empty{Err: nil}
)
func EmptyValue() config.Value {
return emptyValue
}
type Empty struct {
Err error
}
func IsEmpty(v config.Value) bool {
return v == nil || v == emptyValue
}
func (e Empty) Unmarshal(_ any) error {
return e.Err
}
@@ -83,3 +98,13 @@ func (e Empty) Duration() time.Duration {
func (e Empty) Time() time.Time {
return time.Time{}
}
func (e Empty) Any() any {
return e.Err
}
func (e Empty) IsEquals(v config.Value) bool {
em, ok := v.(Empty)
return ok && errors.Is(em.Err, e.Err)
}

123
value/float64.go Normal file
View File

@@ -0,0 +1,123 @@
package value
import (
"fmt"
"time"
"gitoa.ru/go-4devs/config"
)
var _ config.Value = Float64(0)
type Float64 float64
func (f Float64) Any() any {
return float64(f)
}
func (f Float64) ParseString() (string, error) {
return fmt.Sprint(float64(f)), nil
}
func (f Float64) ParseInt() (int, error) {
return int(f), nil
}
func (f Float64) ParseInt64() (int64, error) {
return int64(f), nil
}
func (f Float64) ParseUint() (uint, error) {
return uint(f), nil
}
func (f Float64) ParseUint64() (uint64, error) {
return uint64(f), nil
}
func (f Float64) ParseFloat64() (float64, error) {
return float64(f), nil
}
func (f Float64) ParseBool() (bool, error) {
return false, fmt.Errorf("float64:%w", config.ErrWrongType)
}
func (f Float64) ParseDuration() (time.Duration, error) {
return time.Duration(f), nil
}
func (f Float64) ParseTime() (time.Time, error) {
return time.Unix(0, int64(f*Float64(time.Second))), nil
}
func (f Float64) Unmarshal(in any) error {
v, ok := in.(*float64)
if !ok {
return fmt.Errorf("%w: expect *float64", config.ErrWrongType)
}
*v = float64(f)
return nil
}
func (f Float64) IsEquals(val config.Value) bool {
data, ok := val.(Float64)
return ok && data == f
}
func (f Float64) String() string {
data, _ := f.ParseString()
return data
}
func (f Float64) Int() int {
data, _ := f.ParseInt()
return data
}
func (f Float64) Int64() int64 {
data, _ := f.ParseInt64()
return data
}
func (f Float64) Uint() uint {
data, _ := f.ParseUint()
return data
}
func (f Float64) Uint64() uint64 {
data, _ := f.ParseUint64()
return data
}
func (f Float64) Float64() float64 {
data, _ := f.ParseFloat64()
return data
}
func (f Float64) Bool() bool {
data, _ := f.ParseBool()
return data
}
func (f Float64) Duration() time.Duration {
data, _ := f.ParseDuration()
return data
}
func (f Float64) Time() time.Time {
data, _ := f.ParseTime()
return data
}

51
value/float64_test.go Normal file
View File

@@ -0,0 +1,51 @@
package value_test
import (
"encoding/json"
"math"
"testing"
"gitoa.ru/go-4devs/config/test/require"
"gitoa.ru/go-4devs/config/value"
)
func TestFloat64_Unmarshal(t *testing.T) {
t.Parallel()
f := value.Float64(math.Pi)
var out float64
require.NoError(t, f.Unmarshal(&out))
require.Equal(t, math.Pi, out)
}
func TestFloat64_Any(t *testing.T) {
t.Parallel()
f := value.Float64(math.Pi)
require.Equal(t, math.Pi, f.Any(), 0)
}
func TestFloat64s_Unmarshal(t *testing.T) {
t.Parallel()
data, err := json.Marshal([]float64{math.Pi, math.Sqrt2})
require.NoError(t, err)
f := value.JBytes(data)
var out []float64
require.NoError(t, f.Unmarshal(&out))
require.Equal(t, []float64{math.Pi, math.Sqrt2}, out)
}
func TestFloat64s_Any(t *testing.T) {
t.Parallel()
f := value.New([]float64{math.Pi, math.Sqrt2})
require.Equal(t, []float64{math.Pi, math.Sqrt2}, f.Any())
}

View File

@@ -18,7 +18,7 @@ func ParseDuration(raw string) (time.Duration, error) {
return d, nil
}
func ParseInt(s string) (int64, error) {
func ParseInt64(s string) (int64, error) {
i, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, fmt.Errorf("%w: %w", config.ErrInvalidValue, err)
@@ -27,13 +27,13 @@ func ParseInt(s string) (int64, error) {
return i, nil
}
func ParseUint(s string) (uint64, error) {
func ParseUint(s string) (uint, error) {
i, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return 0, fmt.Errorf("%w: %w", config.ErrInvalidValue, err)
}
return i, nil
return uint(i), nil
}
func Atoi(s string) (int, error) {
@@ -79,3 +79,12 @@ func JUnmarshal(b []byte, v any) error {
return nil
}
func ParseUint64(s string) (uint64, error) {
i, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return 0, fmt.Errorf("%w: %w", config.ErrInvalidValue, err)
}
return i, nil
}

View File

@@ -23,20 +23,15 @@ func (s JBytes) ParseInt() (int, error) {
}
func (s JBytes) ParseInt64() (int64, error) {
return ParseInt(s.String())
return ParseInt64(s.String())
}
func (s JBytes) ParseUint() (uint, error) {
u64, err := ParseUint(s.String())
if err != nil {
return 0, err
}
return uint(u64), nil
return ParseUint(s.String())
}
func (s JBytes) ParseUint64() (uint64, error) {
return ParseUint(s.String())
return ParseUint64(s.String())
}
func (s JBytes) ParseFloat64() (float64, error) {
@@ -111,6 +106,10 @@ func (s JBytes) Time() time.Time {
return in
}
func (s JBytes) Any() any {
return s
}
func (s JBytes) IsEquals(in config.Value) bool {
return s.String() == in.String()
}

View File

@@ -23,20 +23,15 @@ func (s JString) ParseInt() (int, error) {
}
func (s JString) ParseInt64() (int64, error) {
return ParseInt(s.String())
return ParseInt64(s.String())
}
func (s JString) ParseUint() (uint, error) {
u64, err := ParseUint(s.String())
if err != nil {
return 0, err
}
return uint(u64), nil
return ParseUint(s.String())
}
func (s JString) ParseUint64() (uint64, error) {
return ParseUint(s.String())
return ParseUint64(s.String())
}
func (s JString) ParseFloat64() (float64, error) {
@@ -111,6 +106,10 @@ func (s JString) Time() time.Time {
return in
}
func (s JString) Any() any {
return s
}
func (s JString) IsEquals(in config.Value) bool {
return s.String() == in.String()
}

161
value/string.go Normal file
View File

@@ -0,0 +1,161 @@
package value
import (
"fmt"
"strconv"
"time"
"gitoa.ru/go-4devs/config"
)
var _ config.Value = (String)("")
type String string
func (s String) ParseString() (string, error) {
return string(s), nil
}
func (s String) Unmarshal(in any) error {
v, ok := in.(*string)
if !ok {
return fmt.Errorf("%w: expect *string", config.ErrWrongType)
}
*v = string(s)
return nil
}
func (s String) Any() any {
return string(s)
}
func (s String) ParseInt() (int, error) {
v, err := strconv.Atoi(string(s))
if err != nil {
return 0, fmt.Errorf("string int:%w", err)
}
return v, nil
}
func (s String) Int64() int64 {
out, _ := s.ParseInt64()
return out
}
func (s String) ParseInt64() (int64, error) {
v, err := strconv.ParseInt(string(s), 10, 64)
if err != nil {
return 0, fmt.Errorf("string int64:%w", err)
}
return v, nil
}
func (s String) ParseUint() (uint, error) {
uout, err := s.ParseUint64()
return uint(uout), err
}
func (s String) ParseUint64() (uint64, error) {
uout, err := strconv.ParseUint(string(s), 10, 64)
if err != nil {
return 0, fmt.Errorf("string uint:%w", err)
}
return uout, nil
}
func (s String) ParseFloat64() (float64, error) {
fout, err := strconv.ParseFloat(string(s), 64)
if err != nil {
return 0, fmt.Errorf("string float64:%w", err)
}
return fout, nil
}
func (s String) ParseBool() (bool, error) {
v, err := strconv.ParseBool(string(s))
if err != nil {
return false, fmt.Errorf("string bool:%w", err)
}
return v, nil
}
func (s String) ParseDuration() (time.Duration, error) {
v, err := time.ParseDuration(string(s))
if err != nil {
return 0, fmt.Errorf("string duration:%w", err)
}
return v, nil
}
func (s String) ParseTime() (time.Time, error) {
v, err := time.Parse(time.RFC3339, string(s))
if err != nil {
return time.Time{}, fmt.Errorf("string time:%w", err)
}
return v, nil
}
func (s String) IsEquals(val config.Value) bool {
data, ok := val.(String)
return ok && data == s
}
func (s String) String() string {
data, _ := s.ParseString()
return data
}
func (s String) Int() int {
data, _ := s.ParseInt()
return data
}
func (s String) Uint() uint {
data, _ := s.ParseUint()
return data
}
func (s String) Uint64() uint64 {
data, _ := s.ParseUint64()
return data
}
func (s String) Float64() float64 {
data, _ := s.ParseFloat64()
return data
}
func (s String) Bool() bool {
data, _ := s.ParseBool()
return data
}
func (s String) Duration() time.Duration {
data, _ := s.ParseDuration()
return data
}
func (s String) Time() time.Time {
data, _ := s.ParseTime()
return data
}

34
value/string_test.go Normal file
View File

@@ -0,0 +1,34 @@
package value_test
import (
"encoding/json"
"testing"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/test/require"
"gitoa.ru/go-4devs/config/value"
)
func TestStringUnmarshal(t *testing.T) {
t.Parallel()
st := value.String("test")
data, err := json.Marshal([]string{"test1", "test2"})
require.NoError(t, err)
sta := value.JBytes(data)
ac := ""
require.NoError(t, st.Unmarshal(&ac))
require.Equal(t, "test", ac)
aca := []string{}
require.NoError(t, sta.Unmarshal(&aca))
require.Equal(t, []string{"test1", "test2"}, aca)
require.ErrorIs(t, sta.Unmarshal(ac), config.ErrWrongType)
require.ErrorIs(t, sta.Unmarshal(&ac), config.ErrWrongType)
require.ErrorIs(t, st.Unmarshal(aca), config.ErrWrongType)
require.ErrorIs(t, st.Unmarshal(&aca), config.ErrWrongType)
}

179
value/strings.go Normal file
View File

@@ -0,0 +1,179 @@
package value
import (
"encoding/json"
"fmt"
"time"
"gitoa.ru/go-4devs/config"
)
type Strings []string
func (s Strings) Unmarshal(in any) error {
data, jerr := json.Marshal([]string(s))
if jerr != nil {
return fmt.Errorf("failed load data:%w", jerr)
}
if err := json.Unmarshal(data, in); err != nil {
return fmt.Errorf("unmarshal:%w", err)
}
return nil
}
func (s Strings) ParseString() (string, error) {
return s.String(), nil
}
func (s Strings) ParseInt() (int, error) {
data, err := Atoi(s.String())
if err != nil {
return 0, fmt.Errorf("strings: %w", err)
}
return data, nil
}
func (s Strings) ParseInt64() (int64, error) {
data, err := ParseInt64(s.String())
if err != nil {
return 0, fmt.Errorf("strings: %w", err)
}
return data, nil
}
func (s Strings) ParseUint() (uint, error) {
data, err := ParseUint(s.String())
if err != nil {
return 0, fmt.Errorf("strings: %w", err)
}
return data, nil
}
func (s Strings) ParseUint64() (uint64, error) {
data, err := ParseUint64(s.String())
if err != nil {
return 0, fmt.Errorf("strings: %w", err)
}
return data, nil
}
func (s Strings) ParseFloat64() (float64, error) {
data, err := ParseFloat(s.String())
if err != nil {
return 0, fmt.Errorf("strings: %w", err)
}
return data, nil
}
func (s Strings) ParseBool() (bool, error) {
data, err := ParseBool(s.String())
if err != nil {
return false, fmt.Errorf("strings: %w", err)
}
return data, nil
}
func (s Strings) ParseDuration() (time.Duration, error) {
data, err := ParseDuration(s.String())
if err != nil {
return 0, fmt.Errorf("strings: %w", err)
}
return data, nil
}
func (s Strings) ParseTime() (time.Time, error) {
data, err := ParseTime(s.String())
if err != nil {
return time.Time{}, fmt.Errorf("strings: %w", err)
}
return data, nil
}
func (s Strings) String() string {
if len(s) == 1 {
return s[0]
}
return fmt.Sprintf("%v", []string(s))
}
func (s Strings) Int() int {
val, _ := s.ParseInt()
return val
}
func (s Strings) Int64() int64 {
val, _ := s.ParseInt64()
return val
}
func (s Strings) Uint() uint {
val, _ := s.ParseUint()
return val
}
func (s Strings) Uint64() uint64 {
val, _ := s.ParseUint64()
return val
}
func (s Strings) Float64() float64 {
val, _ := s.ParseFloat64()
return val
}
func (s Strings) Bool() bool {
val, _ := s.ParseBool()
return val
}
func (s Strings) Duration() time.Duration {
val, _ := s.ParseDuration()
return val
}
func (s Strings) Time() time.Time {
val, _ := s.ParseTime()
return val
}
func (s Strings) Any() any {
return s
}
func (s Strings) Append(data string) Strings {
return append(s, data)
}
func (s Strings) IsEquals(in config.Value) bool {
val, ok := in.(Strings)
if !ok || len(s) != len(val) {
return false
}
for idx := range val {
if s[idx] != val[idx] {
return false
}
}
return true
}

View File

@@ -1,8 +1,8 @@
package value
import (
"encoding/json"
"fmt"
"reflect"
"time"
"gitoa.ru/go-4devs/config"
@@ -10,6 +10,15 @@ import (
var _ config.Value = (*Value)(nil)
func New(data any) config.Value {
switch val := data.(type) {
case config.Value:
return val
default:
return Value{Val: data}
}
}
type Value struct {
Val any
}
@@ -62,10 +71,6 @@ func (s Value) Duration() time.Duration {
return v
}
func (s Value) Raw() any {
return s.Val
}
func (s Value) Time() time.Time {
v, _ := s.ParseTime()
@@ -73,20 +78,11 @@ func (s Value) Time() time.Time {
}
func (s Value) Unmarshal(target any) error {
if v, ok := s.Raw().([]byte); ok {
err := json.Unmarshal(v, target)
if err != nil {
return fmt.Errorf("%w: %w", config.ErrInvalidValue, err)
}
return nil
}
return config.ErrInvalidValue
return typeAssert(s.Val, target)
}
func (s Value) ParseInt() (int, error) {
if r, ok := s.Raw().(int); ok {
if r, ok := s.Any().(int); ok {
return r, nil
}
@@ -94,7 +90,7 @@ func (s Value) ParseInt() (int, error) {
}
func (s Value) ParseInt64() (int64, error) {
if r, ok := s.Raw().(int64); ok {
if r, ok := s.Any().(int64); ok {
return r, nil
}
@@ -102,7 +98,7 @@ func (s Value) ParseInt64() (int64, error) {
}
func (s Value) ParseUint() (uint, error) {
if r, ok := s.Raw().(uint); ok {
if r, ok := s.Any().(uint); ok {
return r, nil
}
@@ -110,7 +106,7 @@ func (s Value) ParseUint() (uint, error) {
}
func (s Value) ParseUint64() (uint64, error) {
if r, ok := s.Raw().(uint64); ok {
if r, ok := s.Any().(uint64); ok {
return r, nil
}
@@ -118,7 +114,7 @@ func (s Value) ParseUint64() (uint64, error) {
}
func (s Value) ParseFloat64() (float64, error) {
if r, ok := s.Raw().(float64); ok {
if r, ok := s.Any().(float64); ok {
return r, nil
}
@@ -126,7 +122,7 @@ func (s Value) ParseFloat64() (float64, error) {
}
func (s Value) ParseString() (string, error) {
if r, ok := s.Raw().(string); ok {
if r, ok := s.Any().(string); ok {
return r, nil
}
@@ -134,7 +130,7 @@ func (s Value) ParseString() (string, error) {
}
func (s Value) ParseBool() (bool, error) {
if b, ok := s.Raw().(bool); ok {
if b, ok := s.Any().(bool); ok {
return b, nil
}
@@ -142,7 +138,7 @@ func (s Value) ParseBool() (bool, error) {
}
func (s Value) ParseDuration() (time.Duration, error) {
if b, ok := s.Raw().(time.Duration); ok {
if b, ok := s.Any().(time.Duration); ok {
return b, nil
}
@@ -150,7 +146,7 @@ func (s Value) ParseDuration() (time.Duration, error) {
}
func (s Value) ParseTime() (time.Time, error) {
if b, ok := s.Raw().(time.Time); ok {
if b, ok := s.Any().(time.Time); ok {
return b, nil
}
@@ -158,5 +154,100 @@ func (s Value) ParseTime() (time.Time, error) {
}
func (s Value) IsEquals(in config.Value) bool {
return s.String() == in.String()
return s.Any() == in.Any()
}
func (s Value) Any() any {
return s.Val
}
func typeAssert(source, target any) error {
if source == nil {
return nil
}
if directTypeAssert(source, target) {
return nil
}
valTarget := reflect.ValueOf(target)
if !valTarget.IsValid() || valTarget.Kind() != reflect.Ptr {
return fmt.Errorf("ptr target:%w", config.ErrInvalidValue)
}
valTarget = valTarget.Elem()
if !valTarget.IsValid() {
return fmt.Errorf("elem targer:%w", config.ErrInvalidValue)
}
valSource := reflect.ValueOf(source)
if !valSource.IsValid() {
return fmt.Errorf("source:%w", config.ErrInvalidValue)
}
valSource = deReference(valSource)
if err := canSet(valSource, valTarget); err != nil {
return fmt.Errorf("can set:%w", err)
}
valTarget.Set(valSource)
return nil
}
func canSet(source, target reflect.Value) error {
if source.Kind() != target.Kind() {
return fmt.Errorf("source=%v target=%v:%w", source.Kind(), target.Kind(), config.ErrInvalidValue)
}
if source.Kind() == reflect.Slice && source.Type().Elem().Kind() != target.Type().Elem().Kind() {
return fmt.Errorf("slice source=%v, slice target=%v:%w",
source.Type().Elem().Kind(), target.Type().Elem().Kind(), config.ErrInvalidValue)
}
return nil
}
func directTypeAssert(source, target any) bool {
var ok bool
switch val := target.(type) {
case *string:
*val, ok = source.(string)
case *[]byte:
*val, ok = source.([]byte)
case *int:
*val, ok = source.(int)
case *int64:
*val, ok = source.(int64)
case *uint:
*val, ok = source.(uint)
case *uint64:
*val, ok = source.(uint64)
case *bool:
*val, ok = source.(bool)
case *float64:
*val, ok = source.(float64)
case *time.Duration:
*val, ok = source.(time.Duration)
case *time.Time:
*val, ok = source.(time.Time)
case *[]string:
*val, ok = source.([]string)
case *map[string]string:
*val, ok = source.(map[string]string)
case *map[string]any:
*val, ok = source.(map[string]any)
}
return ok
}
func deReference(v reflect.Value) reflect.Value {
if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && !v.IsNil() {
return v.Elem()
}
return v
}

122
variable.go Normal file
View File

@@ -0,0 +1,122 @@
package config
import (
"fmt"
"gitoa.ru/go-4devs/config/key"
"gitoa.ru/go-4devs/config/param"
)
func NewVar(opt Option) Variable {
return Variable{
param: opt,
names: []string{opt.Name()},
}
}
type Variable struct {
names []string
param param.Params
}
func (v Variable) Param(key any) (any, bool) {
return v.param.Param(key)
}
func (v Variable) Key() []string {
return v.names
}
func (v Variable) Type() any {
return param.Type(v)
}
func (v Variable) With(opt Option) Variable {
return Variable{
names: append(v.Key(), opt.Name()),
param: param.Chain(v.param, opt),
}
}
func NewVars(opts ...Option) Vars {
vars := newVars(opts...)
pos := key.Map{}
for idx, one := range vars {
pos.Add(idx, one.Key())
}
return Vars{
vars: vars,
pos: pos,
}
}
func newVars(opts ...Option) []Variable {
vars := make([]Variable, 0, len(opts))
for _, opt := range opts {
one := NewVar(opt)
switch data := opt.(type) {
case Group:
vars = append(vars, groupVars(one, data.Options()...)...)
default:
vars = append(vars, one)
}
}
return vars
}
func groupVars(parent Variable, opts ...Option) []Variable {
vars := make([]Variable, 0, len(opts))
for _, opt := range opts {
switch data := opt.(type) {
case Group:
vars = append(vars, groupVars(parent.With(opt), data.Options()...)...)
default:
vars = append(vars, parent.With(opt))
}
}
return vars
}
type Vars struct {
vars []Variable
pos key.Map
}
func (d Vars) Variables() []Variable {
return d.vars
}
func (d Vars) ByName(path ...string) (Variable, error) {
if idx, ok := d.pos.Index(path); ok {
return d.vars[idx], nil
}
return Variable{}, ErrNotFound
}
func (d Vars) ByParam(fn param.Has) (Variable, error) {
opts := d.filter(fn)
switch {
case len(opts) == 0:
return Variable{}, fmt.Errorf("%w:%T", ErrNotFound, fn)
case len(opts) > 1:
return Variable{}, fmt.Errorf("%w:%v", ErrToManyArgs, opts)
default:
return opts[0], nil
}
}
func (d Vars) filter(fn param.Has) []Variable {
opts := make([]Variable, 0, len(d.vars))
for idx := range d.vars {
if fn(d.vars[idx]) {
opts = append(opts, d.vars[idx])
}
}
return opts
}