9 Commits

Author SHA1 Message Date
954187a07a Merge pull request 'update values' (#16) from json into master
All checks were successful
Go Action / goaction (push) Successful in 27s
Reviewed-on: #16
2025-12-27 10:37:44 +03:00
915660eab1 update jstring test
All checks were successful
Go Action / goaction (push) Successful in 28s
Go Action / goaction (pull_request) Successful in 30s
2025-12-27 10:30:28 +03:00
6d42e1a5f0 update values
All checks were successful
Go Action / goaction (pull_request) Successful in 30s
Go Action / goaction (push) Successful in 48s
2025-12-26 20:59:11 +03:00
d2bc7dc1ec Merge pull request 'update json provider' (#15) from json into master
All checks were successful
Go Action / goaction (push) Successful in 28s
Reviewed-on: #15
2025-12-26 16:14:33 +03:00
6dafb6f7ce update json provider
All checks were successful
Go Action / goaction (push) Successful in 45s
Go Action / goaction (pull_request) Successful in 33s
2025-12-26 16:12:44 +03:00
7f144a5084 Merge pull request 'update ini provider' (#14) from ini into master
All checks were successful
Go Action / goaction (push) Successful in 27s
Reviewed-on: #14
2025-12-26 16:09:17 +03:00
fafe002796 update ini provider
All checks were successful
Go Action / goaction (push) Successful in 40s
Go Action / goaction (pull_request) Successful in 28s
2025-12-26 16:06:00 +03:00
c11f08ee07 update yaml provider (#13)
All checks were successful
Go Action / goaction (push) Successful in 38s
Reviewed-on: #13
2025-12-26 15:55:38 +03:00
f9a0411192 def (#12)
All checks were successful
Go Action / goaction (push) Successful in 29s
Reviewed-on: #12
2025-12-26 14:55:42 +03:00
138 changed files with 4904 additions and 1493 deletions

View File

@@ -1,165 +0,0 @@
---
kind: pipeline
name: default
steps:
- name: test
image: golang
commands:
# - go test -parallel 10 -race ./...
- go test ./...
- name: golangci-lint
image: golangci/golangci-lint:v1.55
commands:
- golangci-lint run
---
kind: pipeline
name: json
steps:
- name: test
image: golang
commands:
- cd provider/json
- go test ./...
- name: golangci-lint
image: golangci/golangci-lint:v1.55
commands:
- cd provider/json
- golangci-lint run
---
kind: pipeline
name: yaml
steps:
- name: test
image: golang
commands:
- cd provider/yaml
- go test ./...
- name: golangci-lint
image: golangci/golangci-lint:v1.55
commands:
- cd provider/yaml
- golangci-lint run
---
kind: pipeline
type: docker
name: ini
steps:
- name: test
image: golang
failure: ignore # runtime/cgo: pthread_create failed: Operation not permitted
commands:
- cd provider/ini
- go test ./...
- name: golangci-lint
image: golangci/golangci-lint:v1.55
commands:
- cd provider/ini
- golangci-lint run
---
kind: pipeline
type: docker
name: toml
steps:
- name: test
image: golang
commands:
- cd provider/toml
- go test ./...
- name: golangci-lint
image: golangci/golangci-lint:v1.55
commands:
- cd provider/toml
- golangci-lint run
---
kind: pipeline
type: docker
name: etcd
environment:
FDEVS_CONFIG_ETCD_HOST: etcd:2379
services:
- name: etcd
image: bitnami/etcd:3.5.11
environment:
ALLOW_NONE_AUTHENTICATION: yes
steps:
- name: test
image: golang
failure: ignore # runtime/cgo: pthread_create failed: Operation not permitted
commands:
- cd provider/etcd
- go test ./...
- name: golangci-lint
image: golangci/golangci-lint:v1.55
commands:
- cd provider/etcd
- golangci-lint run
---
kind: pipeline
type: docker
name: vault
environment:
VAULT_DEV_LISTEN_ADDRESS: http://vault:8200
VAULT_DEV_ROOT_TOKEN_ID: dev
services:
- name: vault
image: vault:1.13.3
environment:
VAULT_DEV_ROOT_TOKEN_ID: dev
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
steps:
- name: test
image: golang
failure: ignore # runtime/cgo: pthread_create failed: Operation not permitted
commands:
- cd provider/vault
- go test ./...
- name: golangci-lint
image: golangci/golangci-lint:v1.55
commands:
- cd provider/vault
- golangci-lint run
---
kind: pipeline
type: docker
name: definition
steps:
- name: test
image: golang
commands:
- cd definition
- go test ./...
- name: golangci-lint
image: golangci/golangci-lint:v1.55
commands:
- cd definition
- golangci-lint run

40
.gitea/workflows/etcd.yml Normal file
View File

@@ -0,0 +1,40 @@
name: Go Action
on: [push, pull_request]
jobs:
goaction:
runs-on: ubuntu-latest # Use a Gitea Actions runner label
services:
# The label "etcd-server" becomes the hostname for the service.
etcd-server:
# Use the official etcd Docker image
image: quay.io/coreos/etcd:v3.6.7
env:
ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
ETCD_ADVERTISE_CLIENT_URLS: http://etcd-server:2379
env:
FDEVS_CONFIG_ETCD_HOST: 'etcd-server:2379'
steps:
- name: Check out repository code
uses: actions/checkout@v4 # Action to clone the repo
- name: Set up Go
uses: actions/setup-go@v5 # Action to install a specific Go version
with:
go-version: '1.25.5' # Specify your required Go version
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v8 # Use the golangci-lint action
with:
version: v2.7.2 # Specify the linter version
# Optional: additional arguments
args: --verbose
working-directory: ./provider/etcd
- name: Run go test
run: go test ./...
working-directory: ./provider/etcd

View File

@@ -0,0 +1,26 @@
name: Go Action
on: [push, pull_request]
jobs:
goaction:
runs-on: ubuntu-latest # Use a Gitea Actions runner label
steps:
- name: Check out repository code
uses: actions/checkout@v4 # Action to clone the repo
- name: Set up Go
uses: actions/setup-go@v5 # Action to install a specific Go version
with:
go-version: '1.25.5' # Specify your required Go version
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v8 # Use the golangci-lint action
with:
version: v2.7.2 # Specify the linter version
# Optional: additional arguments
args: --verbose
- name: Run go test
run: go test ./...

28
.gitea/workflows/ini.yml Normal file
View File

@@ -0,0 +1,28 @@
name: Go Action
on: [push, pull_request]
jobs:
goaction:
runs-on: ubuntu-latest # Use a Gitea Actions runner label
steps:
- name: Check out repository code
uses: actions/checkout@v4 # Action to clone the repo
- name: Set up Go
uses: actions/setup-go@v5 # Action to install a specific Go version
with:
go-version: '1.25.5' # Specify your required Go version
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v8 # Use the golangci-lint action
with:
version: v2.7.2 # Specify the linter version
# Optional: additional arguments
args: --verbose
working-directory: ./provider/ini
- name: Run go test
run: go test ./...
working-directory: ./provider/ini

28
.gitea/workflows/json.yml Normal file
View File

@@ -0,0 +1,28 @@
name: Go Action
on: [push, pull_request]
jobs:
goaction:
runs-on: ubuntu-latest # Use a Gitea Actions runner label
steps:
- name: Check out repository code
uses: actions/checkout@v4 # Action to clone the repo
- name: Set up Go
uses: actions/setup-go@v5 # Action to install a specific Go version
with:
go-version: '1.25.5' # Specify your required Go version
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v8 # Use the golangci-lint action
with:
version: v2.7.2 # Specify the linter version
# Optional: additional arguments
args: --verbose
working-directory: ./provider/json
- name: Run go test
run: go test ./...
working-directory: ./provider/json

View File

@@ -1,7 +1,20 @@
run:
timeout: 5m
linters-settings:
version: "2"
linters:
default: all
disable:
- noinlineerr
- depguard
- ireturn
- gochecknoglobals
# deprecated
- wsl
settings:
recvcheck:
disable-builtin: true
exclusions:
- "*.String"
funcorder:
constructor: false
dupl:
threshold: 100
funlen:
@@ -10,18 +23,14 @@ linters-settings:
goconst:
min-len: 2
min-occurrences: 2
cyclop:
max-complexity: 15
gocyclo:
min-complexity: 15
golint:
min-confidence: 0
govet:
check-shadowing: true
lll:
line-length: 140
maligned:
suggest-new: true
misspell:
locale: US
locale: US
varnamelen:
min-name-length: 2
ignore-decls:
@@ -31,43 +40,43 @@ linters-settings:
- i int
- b bytes.Buffer
- h Handle
linters:
enable-all: true
disable:
- exhaustivestruct
- maligned
- interfacer
- scopelint
- exhaustruct
- depguard
- nolintlint
#deprecated
- structcheck
- varcheck
- golint
- deadcode
- ifshort
- nosnakecase
- ireturn # implement provider interface
issues:
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
- path: _test\.go
linters:
- gomnd
- exhaustivestruct
- wrapcheck
- exhaustruct
- varnamelen
- tenv
- funlen
- path: test/*
linters:
- gomnd
- exhaustivestruct
- wrapcheck
- exhaustruct
- varnamelen
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- exhaustivestruct
- exhaustruct
- funlen
- mnd
- tenv
- varnamelen
- wrapcheck
path: _test\.go
- linters:
- exhaustivestruct
- exhaustruct
- mnd
- varnamelen
- wrapcheck
path: test/*
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@@ -1,5 +1,5 @@
# config
[![Build Status](https://drone.gitoa.ru/api/badges/go-4devs/cache/status.svg)](https://drone.gitoa.ru/go-4devs/config)
![Build Status](https://gitoa.ru/go-4devs/config/actions/workflows/goaction.yml/badge.svg)
[![Go Report Card](https://goreportcard.com/badge/gitoa.ru/go-4devs/config)](https://goreportcard.com/report/gitoa.ru/go-4devs/config)
[![GoDoc](https://godoc.org/gitoa.ru/go-4devs/config?status.svg)](http://godoc.org/gitoa.ru/go-4devs/config)

View File

@@ -4,11 +4,9 @@ import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
)
func Must(providers ...interface{}) *Client {
func Must(providers ...any) *Client {
client, err := New(providers...)
if err != nil {
panic(err)
@@ -17,7 +15,7 @@ func Must(providers ...interface{}) *Client {
return client
}
func New(providers ...interface{}) (*Client, error) {
func New(providers ...any) (*Client, error) {
client := &Client{
providers: make([]Provider, len(providers)),
}
@@ -27,11 +25,7 @@ func New(providers ...interface{}) (*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)
},
}
client.providers[idx] = WrapFactory(current, client)
default:
return nil, fmt.Errorf("provier[%d]: %w %T", idx, ErrUnknowType, prov)
}
@@ -40,60 +34,6 @@ func New(providers ...interface{}) (*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) 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
}
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
}
type Client struct {
providers []Provider
}
@@ -111,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
}
}
@@ -132,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
}
@@ -142,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
}
@@ -79,7 +79,7 @@ func ExampleClient_Watch() {
wg := sync.WaitGroup{}
wg.Add(1)
err = watcher.Watch(ctx, func(ctx context.Context, oldVar, newVar config.Value) error {
err = watcher.Watch(ctx, func(_ context.Context, oldVar, newVar config.Value) error {
fmt.Println("update example_enable old: ", oldVar.Bool(), " new:", newVar.Bool())
wg.Done()
@@ -93,7 +93,7 @@ func ExampleClient_Watch() {
_ = os.Setenv("FDEVS_CONFIG_EXAMPLE_ENABLE", "false")
err = watcher.Watch(ctx, func(ctx context.Context, oldVar, newVar config.Value) error {
err = watcher.Watch(ctx, func(_ context.Context, oldVar, newVar config.Value) error {
fmt.Println("update example_db_dsn old: ", oldVar.String(), " new:", newVar.String())
wg.Done()

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"

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

@@ -1,29 +1,23 @@
package definition
import (
"fmt"
"gitoa.ru/go-4devs/config"
)
func New() Definition {
return Definition{}
func New(opts ...config.Option) *Definition {
return &Definition{
options: opts,
}
}
type Definition struct {
options Options
options []config.Option
}
func (d *Definition) Add(opts ...Option) *Definition {
func (d *Definition) Add(opts ...config.Option) {
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
func (d *Definition) Options() []config.Option {
return d.options
}

View File

@@ -0,0 +1,94 @@
package bootstrap
import (
"context"
"embed"
"fmt"
"os"
"path/filepath"
"text/template"
"gitoa.ru/go-4devs/config/definition/generate/pkg"
)
//go:embed *.tpl
var tpls embed.FS
type Boot struct {
Config
imp *pkg.Imports
Configure []string
OutName string
}
func (b Boot) Imports() []pkg.Import {
return b.imp.Imports()
}
type Config interface {
File() string
Methods() []string
SkipContext() bool
Prefix() string
Suffix() string
FullPkg() string
Pkg() string
}
func Bootstrap(ctx context.Context, cfg Config) (string, error) {
fInfo, err := os.Stat(cfg.File())
if err != nil {
return "", fmt.Errorf("stat:%w", err)
}
pkgPath, err := pkg.ByPath(ctx, cfg.File(), fInfo.IsDir())
if err != nil {
return "", fmt.Errorf("pkg by path:%w", err)
}
tmpFile, err := os.CreateTemp(filepath.Dir(fInfo.Name()), "config-bootstrap")
if err != nil {
return "", fmt.Errorf("create tmp file:%w", err)
}
tpl, err := template.ParseFS(tpls, "bootstrap.go.tpl")
if err != nil {
return "", fmt.Errorf("parse template:%w", err)
}
imports := pkg.NewImports("main").
Adds(
"context",
"gitoa.ru/go-4devs/config/definition",
"gitoa.ru/go-4devs/config/definition/generate",
"os",
"fmt",
"go/format",
pkgPath,
)
data := Boot{
imp: imports,
Configure: cfg.Methods(),
OutName: fInfo.Name()[0:len(fInfo.Name())-3] + "_config.go",
Config: cfg,
}
if err := tpl.Execute(tmpFile, data); err != nil {
return "", fmt.Errorf("execute:%w", err)
}
src := tmpFile.Name()
if err := tmpFile.Close(); err != nil {
return src, fmt.Errorf("close file:%w", err)
}
dest := src + ".go"
if err := os.Rename(src, dest); err != nil {
return dest, fmt.Errorf("rename idt:%w", err)
}
return dest, nil
}

View File

@@ -0,0 +1,59 @@
//go:build ignore
// +build ignore
package main
import (
{{range .Imports}}
{{- .Alias }}"{{ .Package }}"
{{end}}
)
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
ctx := context.Background()
f, err := os.Create("{{.OutName}}")
if err != nil {
return err
}
defs:=make([]generate.Input,0)
{{ range .Configure }}
def{{.}} := definition.New()
if err := {{$.Pkg}}.{{.}}(ctx, def{{.}}); err != nil {
return err
}
defs = append(defs,generate.NewInput("{{.}}",def{{.}}))
{{ end }}
opts := make([]generate.Option,0)
{{ if .SkipContext }}opts = append(opts, generate.WithSkipContext){{ end }}
opts = append(opts,
generate.WithPrefix("{{.Prefix}}"),
generate.WithSuffix("{{.Suffix}}"),
generate.WithFullPkg("{{.FullPkg}}"),
)
if gerr := generate.Run(ctx,generate.NewConfig(opts...),f, defs...);gerr != nil {
return gerr
}
in, err := os.ReadFile(f.Name())
if err != nil {
return err
}
out, err := format.Source(in)
if err != nil {
return err
}
return os.WriteFile(f.Name(), out, 0644)
}

View File

@@ -0,0 +1,37 @@
package example
import (
"context"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/generate/view"
"gitoa.ru/go-4devs/config/definition/group"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/definition/proto"
)
type Level string
func (l *Level) UnmarshalText(in []byte) error {
data := string(in)
*l = Level(data)
return nil
}
func Config(_ context.Context, def *definition.Definition) error {
def.Add(
option.String("test", "test string", view.WithSkipContext),
group.New("user", "configure user",
option.String("name", "name", option.Default("4devs")),
option.String("pass", "password"),
),
group.New("log", "configure logger",
option.New("level", "log level", Level("")),
proto.New("service", "servise logger", option.New("level", "log level", Level(""))),
),
)
return nil
}

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

View File

@@ -0,0 +1,78 @@
package generate
import (
"context"
"fmt"
"go/format"
"os"
"os/exec"
"path/filepath"
"gitoa.ru/go-4devs/config/definition/generate/bootstrap"
)
func NewGConfig(fname string, opts ...Option) (Config, error) {
opts = append([]Option{
WithFile(fname),
}, opts...)
return NewConfig(opts...), nil
}
type GConfig interface {
BuildTags() string
OutName() string
bootstrap.Config
}
func Generate(ctx context.Context, cfg GConfig) error {
path, err := bootstrap.Bootstrap(ctx, cfg)
defer os.Remove(path)
if err != nil {
return fmt.Errorf("build bootstrap:%w", err)
}
tmpFile, err := os.Create(cfg.File() + ".tmp")
if err != nil {
return fmt.Errorf("create tmp file:%w", err)
}
defer os.Remove(tmpFile.Name()) // will not remove after rename
execArgs := []string{"run"}
if len(cfg.BuildTags()) > 0 {
execArgs = append(execArgs, "-tags", cfg.BuildTags())
}
execArgs = append(execArgs, filepath.Base(path))
cmd := exec.CommandContext(ctx, "go", execArgs...)
cmd.Stdout = tmpFile
cmd.Stderr = os.Stderr
cmd.Dir = filepath.Dir(path)
if err = cmd.Run(); err != nil {
return fmt.Errorf("start cmd:%w", err)
}
tmpFile.Close()
// format file and write to out path
in, err := os.ReadFile(tmpFile.Name())
if err != nil {
return fmt.Errorf("read file: %w", err)
}
out, err := format.Source(in)
if err != nil {
return fmt.Errorf("format source:%w", err)
}
err = os.WriteFile(cfg.OutName(), out, 0o644) //nolint:gosec,mnd
if err != nil {
return fmt.Errorf("write file:%w", err)
}
return nil
}

View File

@@ -0,0 +1,233 @@
package generate
import (
"bytes"
"context"
"embed"
"fmt"
"io"
"strings"
"text/template"
"unicode"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/generate/pkg"
"gitoa.ru/go-4devs/config/definition/generate/render"
"gitoa.ru/go-4devs/config/definition/generate/view"
"gitoa.ru/go-4devs/config/param"
)
type Option func(*Config)
func WithSkipContext(c *Config) {
view.WithSkipContext(c.Params)
}
func WithPrefix(name string) Option {
return func(c *Config) {
c.prefix = name
}
}
func WithSuffix(name string) Option {
return func(c *Config) {
c.suffix = name
}
}
// WithMethods set methosd.
//
// generate.WithMethods(runtime.FuncForPC(reflect.ValueOf(configure).Pointer()).Name()).
func WithMethods(in ...string) Option {
return func(c *Config) {
c.methods = in
}
}
func WithOutName(in string) Option {
return func(c *Config) {
c.outName = in
}
}
func WithFile(in string) Option {
return func(c *Config) {
c.file = in
}
}
func WithFullPkg(in string) Option {
return func(c *Config) {
c.fullPkg = in
}
}
func NewConfig(opts ...Option) Config {
var cfg Config
cfg.Params = param.New()
cfg.prefix = "Input"
for _, opt := range opts {
opt(&cfg)
}
return cfg
}
type Config struct {
param.Params
methods []string
prefix string
suffix string
fullPkg string
pkg string
file string
buildTags string
outName string
}
func (c Config) BuildTags() string {
return c.buildTags
}
func (c Config) Pkg() string {
if c.pkg == "" {
if idx := strings.LastIndex(c.fullPkg, "/"); idx != -1 {
c.pkg = c.fullPkg[idx+1:]
}
}
return c.pkg
}
func (c Config) FullPkg() string {
return c.fullPkg
}
func (c Config) SkipContext() bool {
return view.IsSkipContext(c.Params)
}
func (c Config) Methods() []string {
return c.methods
}
func (c Config) Prefix() string {
return c.prefix
}
func (c Config) Suffix() string {
return c.suffix
}
func (c Config) File() string {
return c.file
}
func (c Config) OutName() string {
return c.outName
}
//go:embed tpl/*
var tpls embed.FS
var initTpl = template.Must(template.New("tpls").ParseFS(tpls, "tpl/*.tpl")).Lookup("init.go.tpl")
func Run(_ context.Context, cfg Config, w io.Writer, inputs ...Input) error {
data := Data{
Config: cfg,
imp: pkg.NewImports(cfg.FullPkg()).Adds("fmt", "context", "gitoa.ru/go-4devs/config"),
}
var buff bytes.Buffer
for _, in := range inputs {
vi := view.NewViews(in.Method(), data.Params, in.Options(), view.WithKeys())
if err := render.Render(&buff, vi, data); err != nil {
return fmt.Errorf("render:%w", err)
}
}
if err := initTpl.Execute(w, data); err != nil {
return fmt.Errorf("render base:%w", err)
}
if _, err := io.Copy(w, &buff); err != nil {
return fmt.Errorf("copy:%w", err)
}
return nil
}
type Data struct {
Config
imp *pkg.Imports
}
func (f Data) Imports() []pkg.Import {
return f.imp.Imports()
}
func (f Data) StructName(name string) string {
return f.Prefix() + FuncName(name) + f.Suffix()
}
func (f Data) FuncName(in string) string {
return FuncName(in)
}
func (f Data) AddType(pkg string) (string, error) {
short, err := f.imp.AddType(pkg)
if err != nil {
return "", fmt.Errorf("data: %w", err)
}
return short, nil
}
func FuncName(name string) string {
data := strings.Builder{}
toUp := true
for _, char := range name {
isLeter := unicode.IsLetter(char)
isAllowed := isLeter || unicode.IsDigit(char)
switch {
case isAllowed && !toUp:
data.WriteRune(char)
case !isAllowed:
toUp = true
case toUp:
data.WriteString(strings.ToUpper(string(char)))
toUp = false
}
}
return data.String()
}
func NewInput(method string, options config.Options) Input {
return Input{
method: method,
options: options,
}
}
type Input struct {
options config.Options
method string
}
func (i Input) Method() string {
return i.method
}
func (i Input) Options() config.Options {
return i.options
}

View File

@@ -0,0 +1,33 @@
package generate_test
import (
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/generate/view"
"gitoa.ru/go-4devs/config/definition/group"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/definition/proto"
)
type LogLevel string
func (l *LogLevel) UnmarshalText(in []byte) error {
data := string(in)
*l = LogLevel(data)
return nil
}
func Configure(def *definition.Definition) {
def.Add(
option.String("test", "test string", view.WithSkipContext),
group.New("user", "configure user",
option.String("name", "name", option.Default("4devs")),
option.String("pass", "password"),
),
group.New("log", "configure logger",
option.New("level", "log level", LogLevel("")),
proto.New("service", "servise logger", option.New("level", "log level", LogLevel(""))),
),
)
}

View File

@@ -1,70 +0,0 @@
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)
}

View File

@@ -1,18 +0,0 @@
package generate
import (
"errors"
"github.com/iancoleman/strcase"
)
var (
ErrNotFound = errors.New("not found")
ErrAlreadyExist = errors.New("already exist")
ErrWrongType = errors.New("wrong type")
ErrWrongFormat = errors.New("wrong format")
)
func FuncName(in string) string {
return strcase.ToCamel(in)
}

View File

@@ -0,0 +1,29 @@
package pkg
import (
"strings"
"unicode"
)
func AliasName(name string) string {
data := strings.Builder{}
toUp := false
for _, char := range name {
isLeter := unicode.IsLetter(char)
isAllowed := isLeter || unicode.IsDigit(char)
switch {
case isAllowed && !toUp:
data.WriteRune(char)
case !isAllowed && data.Len() > 0:
toUp = true
case toUp:
data.WriteString(strings.ToUpper(string(char)))
toUp = false
}
}
return data.String()
}

View File

@@ -0,0 +1,8 @@
package pkg
import "errors"
var (
ErrWrongFormat = errors.New("wrong format")
ErrNotFound = errors.New("not found")
)

View File

@@ -1,4 +1,4 @@
package generate
package pkg
import (
"fmt"
@@ -6,17 +6,21 @@ import (
"strings"
)
func NewImports() Imports {
return Imports{
func NewImports(pkg string) *Imports {
imp := Imports{
data: make(map[string]string),
pkg: pkg,
}
return &imp
}
type Imports struct {
data map[string]string
pkg string
}
func (i Imports) Imports() []Import {
func (i *Imports) Imports() []Import {
imports := make([]Import, 0, len(i.data))
for name, alias := range i.data {
imports = append(imports, Import{
@@ -44,25 +48,38 @@ func (i *Imports) Short(fullType string) (string, error) {
func (i *Imports) AddType(fullType string) (string, error) {
idx := strings.LastIndexByte(fullType, '.')
if idx == -1 {
return "", fmt.Errorf("%w: expect pckage.Type", ErrWrongFormat)
return "", fmt.Errorf("%w: expect pckage.Type got %v", ErrWrongFormat, fullType)
}
imp := i.Add(fullType[:idx])
if imp.Alias == "" {
return fullType[idx+1:], nil
}
return imp.Alias + fullType[idx:], nil
}
func (i *Imports) Adds(pkgs ...string) {
func (i *Imports) Adds(pkgs ...string) *Imports {
for _, pkg := range pkgs {
i.Add(pkg)
}
return i
}
func (i *Imports) Add(pkg string) Import {
if pkg == i.pkg {
return Import{
Alias: "",
Package: pkg,
}
}
alias := pkg
if idx := strings.LastIndexByte(pkg, '/'); idx != -1 {
alias = pkg[idx+1:]
alias = AliasName(pkg[idx+1:])
}
if al, ok := i.data[pkg]; ok {

View File

@@ -0,0 +1,187 @@
package pkg
import (
"bytes"
"context"
"fmt"
"go/build"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
)
var cache = sync.Map{}
func ByPath(ctx context.Context, fname string, isDir bool) (string, error) {
if !filepath.IsAbs(fname) {
pwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("%w", err)
}
fname = filepath.Join(pwd, fname)
}
goModPath, _ := goModPath(ctx, fname, isDir)
if strings.Contains(goModPath, "go.mod") {
pkgPath, err := getPkgPathFromGoMod(fname, isDir, goModPath)
if err != nil {
return "", err
}
return pkgPath, nil
}
return getPkgPathFromGOPATH(fname, isDir)
}
// empty if no go.mod, GO111MODULE=off or go without go modules support.
func goModPath(ctx context.Context, fname string, isDir bool) (string, error) {
root := fname
if !isDir {
root = filepath.Dir(fname)
}
var modPath string
loadModPath, ok := cache.Load(root)
if ok {
modPath, _ = loadModPath.(string)
return modPath, nil
}
defer func() {
cache.Store(root, modPath)
}()
cmd := exec.CommandContext(ctx, "go", "env", "GOMOD")
cmd.Dir = root
stdout, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("%w", err)
}
modPath = string(bytes.TrimSpace(stdout))
return modPath, nil
}
func getPkgPathFromGoMod(fname string, isDir bool, goModPath string) (string, error) {
modulePath := getModulePath(goModPath)
if modulePath == "" {
return "", fmt.Errorf("c%w module path from %s", ErrNotFound, goModPath)
}
rel := path.Join(modulePath, filePathToPackagePath(strings.TrimPrefix(fname, filepath.Dir(goModPath))))
if !isDir {
return path.Dir(rel), nil
}
return path.Clean(rel), nil
}
func getModulePath(goModPath string) string {
var pkgPath string
cacheOkgPath, ok := cache.Load(goModPath)
if ok {
pkgPath, _ = cacheOkgPath.(string)
return pkgPath
}
defer func() {
cache.Store(goModPath, pkgPath)
}()
data, err := os.ReadFile(goModPath)
if err != nil {
return ""
}
pkgPath = modulePath(data)
return pkgPath
}
func getPkgPathFromGOPATH(fname string, isDir bool) (string, error) {
gopath := os.Getenv("GOPATH")
if gopath == "" {
gopath = build.Default.GOPATH
}
for _, p := range strings.Split(gopath, string(filepath.ListSeparator)) {
prefix := filepath.Join(p, "src") + string(filepath.Separator)
rel, err := filepath.Rel(prefix, fname)
if err == nil && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
if !isDir {
return path.Dir(filePathToPackagePath(rel)), nil
}
return path.Clean(filePathToPackagePath(rel)), nil
}
}
return "", fmt.Errorf("%w: file '%v' is not in GOPATH '%v'", ErrNotFound, fname, gopath)
}
func filePathToPackagePath(path string) string {
return filepath.ToSlash(path)
}
var (
slashSlash = []byte("//")
moduleStr = []byte("module")
)
// modulePath returns the module path from the gomod file text.
// If it cannot find a module path, it returns an empty string.
// It is tolerant of unrelated problems in the go.mod file.
func modulePath(mod []byte) string {
for len(mod) > 0 {
line := mod
mod = nil
if i := bytes.IndexByte(line, '\n'); i >= 0 {
line, mod = line[:i], line[i+1:]
}
if i := bytes.Index(line, slashSlash); i >= 0 {
line = line[:i]
}
line = bytes.TrimSpace(line)
if !bytes.HasPrefix(line, moduleStr) {
continue
}
line = line[len(moduleStr):]
n := len(line)
line = bytes.TrimSpace(line)
if len(line) == n || len(line) == 0 {
continue
}
if line[0] == '"' || line[0] == '`' {
p, err := strconv.Unquote(string(line))
if err != nil {
return "" // malformed quoted string or multiline module path
}
return p
}
return string(line)
}
return "" // missing module path
}

View File

@@ -0,0 +1,61 @@
package render
import (
"gitoa.ru/go-4devs/config/definition/generate/pkg"
"gitoa.ru/go-4devs/config/definition/generate/view"
)
func NewViewData(render Rendering, view view.View) ViewData {
return ViewData{
Rendering: render,
View: view,
}
}
type ViewData struct {
Rendering
view.View
}
func (d ViewData) StructName() string {
return d.Rendering.StructName(d.View.ParentName() + "_" + d.View.Name())
}
func (d ViewData) FuncName() string {
return d.Rendering.FuncName(d.View.FuncName())
}
func (d ViewData) ParentName() string {
name := d.View.ParentName()
if name == "" {
name = d.Name()
}
return d.Rendering.StructName(name)
}
func (d ViewData) Name() string {
return pkg.AliasName(d.View.Name())
}
func (d ViewData) Type() string {
return Type(d)
}
func (d ViewData) Keys(parent string) string {
return Keys(append(d.View.Keys(), d.Name()), parent)
}
func (d ViewData) Value(name, val string) string {
return Value(name, val, d)
}
func (d ViewData) Default(name string) string {
return Data(d.View.Default(), name, d)
}
type Rendering interface {
StructName(name string) string
FuncName(name string) string
AddType(pkg string) (string, error)
}

View File

@@ -0,0 +1,5 @@
package render
import "errors"
var ErrNotFound = errors.New("not found")

View File

@@ -0,0 +1,34 @@
package render
import (
"strings"
"gitoa.ru/go-4devs/config/definition/generate/pkg"
"gitoa.ru/go-4devs/config/key"
)
func Keys(keys []string, val string) string {
if len(keys) == 0 {
return ""
}
var out strings.Builder
for idx, one := range keys {
if key.IsWild(one) {
out.WriteString(val)
out.WriteString(".")
out.WriteString(pkg.AliasName(one))
} else {
out.WriteString("\"")
out.WriteString(one)
out.WriteString("\"")
}
if len(keys)-1 != idx {
out.WriteString(", ")
}
}
return out.String()
}

View File

@@ -0,0 +1,49 @@
package render
import (
"fmt"
"io"
"reflect"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/generate/view"
"gitoa.ru/go-4devs/config/definition/group"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/definition/proto"
)
type Execute func(w io.Writer, vi view.View, rnd Rendering) error
var randders = map[reflect.Type]Execute{
reflect.TypeFor[*definition.Definition](): Template(defTpl),
reflect.TypeFor[group.Group](): Template(groupTpl),
reflect.TypeFor[option.Option](): Template(optTpl),
reflect.TypeFor[proto.Proto](): Template(protoTpl),
}
func Renders() map[reflect.Type]Execute {
return randders
}
func Add(rt reflect.Type, fn Execute) {
randders[rt] = fn
}
func Render(w io.Writer, view view.View, data Rendering) error {
rnd, ok := randders[view.Kind()]
if !ok {
return fmt.Errorf("%w:%v", ErrNotFound, view.Kind())
}
if err := rnd(w, view, data); err != nil {
return fmt.Errorf("render:%v, err:%w", view.Kind(), err)
}
for _, ch := range view.Views() {
if err := Render(w, ch, data); err != nil {
return fmt.Errorf("render[%v]:%w", ch.Name(), err)
}
}
return nil
}

View File

@@ -0,0 +1,41 @@
package render
import (
"embed"
"fmt"
"io"
"strings"
"text/template"
"gitoa.ru/go-4devs/config/definition/generate/view"
)
//go:embed tpl/*
var tplFS embed.FS
var (
tpls = template.Must(
template.New("tpls").
Funcs(template.FuncMap{
"trim": strings.Trim,
}).
ParseFS(tplFS, "tpl/*.go.tpl"),
)
defTpl = tpls.Lookup("definition.go.tpl")
groupTpl = tpls.Lookup("group.go.tpl")
protoTpl = tpls.Lookup("proto.go.tpl")
optTpl = template.Must(
template.New("opt").ParseFS(tplFS, "tpl/option/option.go.tpl"),
).Lookup("option.go.tpl")
parceTpls = template.Must(template.New("data").ParseFS(tplFS, "tpl/data/*.go.tpl"))
)
func Template(tpl *template.Template) Execute {
return func(w io.Writer, v view.View, rnd Rendering) error {
if err := tpl.Execute(w, NewViewData(rnd, v)); err != nil {
return fmt.Errorf("template[%v]:%w", tpl.Name(), err)
}
return nil
}
}

View File

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

View File

@@ -0,0 +1,8 @@
{{ block "FlagValue" . -}}
pval, perr := {{.ValName}}.ParseString()
if perr != nil {
return {{.Value}}, fmt.Errorf("read [%v]:%w",[]string{ {{- .Keys "i" -}} }, perr)
}
return {{.Value}}, {{.Value}}.Set(pval)
{{- end }}

View File

@@ -0,0 +1,3 @@
{{ block "ScanValue" . -}}
return {{.Value}}, {{.Value}}.Scan({{.ValName}}.Any())
{{- end }}

View File

@@ -0,0 +1,8 @@
{{block "UnmarshalJSON" . -}}
pval, perr := {{.ValName}}.ParseString()
if perr != nil {
return {{.Value}}, fmt.Errorf("read [%v]:%w", []string{ {{- .Keys "i" -}} }, perr)
}
return {{.Value}}, {{.Value}}.UnmarshalJSON([]byte(pval))
{{- end }}

View File

@@ -0,0 +1,8 @@
{{ block "UnmarshalText" . -}}
pval, perr := {{.ValName}}.ParseString()
if perr != nil {
return {{.Value}}, fmt.Errorf("read [%v]:%w", []string{ {{- .Keys "i" -}} }, perr)
}
return {{.Value}}, {{.Value}}.UnmarshalText([]byte(pval))
{{- end }}

View File

@@ -1,19 +1,3 @@
package generate
import "text/template"
//nolint:gochecknoglobals
var (
tpl = template.Must(template.New("tpls").Parse(baseTemplate))
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
@@ -39,5 +23,3 @@ type {{.StructName}} struct {
config.Provider
log func(context.Context, string, ...any)
}
`
)

View File

@@ -0,0 +1,8 @@
type {{.StructName}} struct {
{{.ParentName}}
}
// {{.FuncName}} {{.Description}}.
func (i {{.ParentName}}) {{.FuncName}}() {{.StructName}} {
return {{.StructName}}{i}
}

View File

@@ -0,0 +1,30 @@
// read{{.FuncName}} {{.Description}}.
func (i {{.ParentName}}) read{{.FuncName}}(ctx context.Context) (v {{.Type}},e error) {
val, err := i.Value(ctx, {{ .Keys "i" }})
if err != nil {
{{- if .HasDefault }}
i.log({{ if not .SkipContext }}context.Background(){{else}}ctx{{ end }}, "read [%v]: %v",[]string{ {{- .Keys "i" -}} }, err)
{{ .Default "val" -}}
{{ else }}
return v, fmt.Errorf("read [%v]:%w",[]string{ {{- .Keys "i" -}} }, err)
{{ end }}
}
{{ .Value "val" "v" }}
}
// Read{{.FuncName}} {{.Description}}.
func (i {{.ParentName}}) Read{{.FuncName}}({{if not .SkipContext}} ctx context.Context {{end}}) ({{.Type}}, error) {
return i.read{{.FuncName}}({{if .SkipContext}}context.Background(){{else}}ctx{{end}})
}
// {{.FuncName}} {{.Description}}.
func (i {{.ParentName}}) {{.FuncName}}({{if not .SkipContext}} ctx context.Context {{end}}) {{.Type}} {
val, err := i.read{{.FuncName}}({{ if .SkipContext }}context.Background(){{else}}ctx{{ end }})
if err != nil {
i.log({{ if .SkipContext }}context.Background(){{else}}ctx{{ end }}, "get [%v]: %v",[]string{ {{- .Keys "i" -}} }, err)
}
return val
}

View File

@@ -0,0 +1,9 @@
type {{.StructName}} struct {
{{.ParentName}}
{{ .Name }} string
}
// {{.FuncName}} {{.Description}}.
func (i {{.ParentName}}) {{.FuncName}}(key string) {{.StructName}} {
return {{.StructName}}{i,key}
}

View File

@@ -0,0 +1,266 @@
package render
import (
"bytes"
"database/sql"
"encoding"
"encoding/json"
"flag"
"fmt"
"reflect"
"time"
)
func Value(name, val string, data ViewData) string {
rnd := renderType(data)
res, err := rnd(ValueData{ValName: name, Value: val, ViewData: data})
if err != nil {
return fmt.Sprintf("render value:%v", err)
}
return res
}
func Type(data ViewData) string {
dt := data.View.Type()
rtype := reflect.TypeOf(dt)
slice := ""
if rtype.Kind() == reflect.Slice {
slice = "[]"
rtype = rtype.Elem()
}
short := rtype.Name()
if rtype.PkgPath() != "" {
var err error
short, err = data.AddType(rtype.PkgPath() + "." + rtype.Name())
if err != nil {
return err.Error()
}
}
return slice + short
}
func Data(val any, name string, view ViewData) string {
fn := renderData(view)
data, err := fn(val, ValueData{ValName: name, Value: "", ViewData: view})
if err != nil {
return fmt.Sprintf("render dara:%v", err)
}
return data
}
func renderDataTime(val any, _ ValueData) (string, error) {
data, _ := val.(time.Time)
return fmt.Sprintf("time.Parse(%q,time.RFC3339Nano)", data.Format(time.RFC3339Nano)), nil
}
func renderDataDuration(val any, _ ValueData) (string, error) {
data, _ := val.(time.Duration)
return fmt.Sprintf("time.ParseDuration(%q)", data), nil
}
func renderDataUnmarhal(val any, view ValueData) (string, error) {
res, err := json.Marshal(val)
if err != nil {
return "", fmt.Errorf("render data unmarshal:%w", err)
}
return fmt.Sprintf("return {{.%[1]s}}, {{.%[1]s}}.UnmarshalJSON(%q)", view.ValName, res), nil
}
func renderDataUnmarhalText(val any, view ValueData) (string, error) {
res, err := json.Marshal(val)
if err != nil {
return "", fmt.Errorf("render data unmarshal:%w", err)
}
return fmt.Sprintf("return {{.%[1]s}}, {{.%[1]s}}.UnmarshalText(%s)", view.ValName, res), nil
}
func renderDataFlag(val any, view ValueData) (string, error) {
return fmt.Sprintf("return {{.%[1]s}}, {{.%[1]s}}.Set(%[2]q)", view.ValName, val), nil
}
func renderType(view ViewData) func(data ValueData) (string, error) {
return dataRender(view).Type
}
func renderData(view ViewData) func(in any, data ValueData) (string, error) {
return dataRender(view).Value
}
func dataRender(view ViewData) DataRender {
data := view.View.Type()
vtype := reflect.TypeOf(data)
if vtype.Kind() == reflect.Slice {
return render[reflect.TypeFor[json.Unmarshaler]()]
}
if h, ok := render[vtype]; ok {
return h
}
if vtype.Kind() != reflect.Interface && vtype.Kind() != reflect.Ptr {
vtype = reflect.PointerTo(vtype)
}
for extypes := range render {
if extypes == nil || extypes.Kind() != reflect.Interface {
continue
}
if vtype.Implements(extypes) {
return render[extypes]
}
}
return render[reflect.TypeOf((any)(nil))]
}
//nolint:gochecknoglobals
var render = map[reflect.Type]DataRender{
reflect.TypeFor[encoding.TextUnmarshaler](): NewDataRender(unmarshalTextType, renderDataUnmarhalText),
reflect.TypeFor[json.Unmarshaler](): NewDataRender(unmarshalType, renderDataUnmarhal),
reflect.TypeFor[flag.Value](): NewDataRender(flagType, renderDataFlag),
reflect.TypeFor[sql.Scanner](): NewDataRender(scanType, nil),
reflect.TypeFor[int](): NewDataRender(internalType, nil),
reflect.TypeFor[int64](): NewDataRender(internalType, anyValue),
reflect.TypeFor[bool](): NewDataRender(internalType, anyValue),
reflect.TypeFor[string](): NewDataRender(internalType, anyValue),
reflect.TypeFor[float64](): NewDataRender(internalType, anyValue),
reflect.TypeFor[uint](): NewDataRender(internalType, anyValue),
reflect.TypeFor[int64](): NewDataRender(internalType, anyValue),
reflect.TypeFor[time.Duration](): NewDataRender(durationType, renderDataDuration),
reflect.TypeFor[time.Time](): NewDataRender(timeType, renderDataTime),
reflect.TypeOf((any)(nil)): NewDataRender(anyType, anyValue),
}
func timeType(data ValueData) (string, error) {
return fmt.Sprintf("return %s.ParseTime()", data.ValName), nil
}
func durationType(data ValueData) (string, error) {
return fmt.Sprintf("return %s.ParseDuration()", data.ValName), nil
}
func scanType(data ValueData) (string, error) {
var b bytes.Buffer
err := parceTpls.Lookup("scan_value.go.tpl").Execute(&b, data)
if err != nil {
return "", fmt.Errorf("execute scan value:%w", err)
}
return b.String(), nil
}
func flagType(data ValueData) (string, error) {
var b bytes.Buffer
err := parceTpls.Lookup("flag_value.go.tpl").Execute(&b, data)
if err != nil {
return "", fmt.Errorf("execute flag value:%w", err)
}
return b.String(), nil
}
func anyType(data ValueData) (string, error) {
var b bytes.Buffer
err := parceTpls.ExecuteTemplate(&b, "any.go.tpl", data)
if err != nil {
return "", fmt.Errorf("unmarshal execute any.go.tpl:%w", err)
}
return b.String(), nil
}
func anyValue(data any, _ ValueData) (string, error) {
return fmt.Sprintf("return %#v, nil", data), nil
}
func unmarshalType(data ValueData) (string, error) {
var b bytes.Buffer
err := parceTpls.ExecuteTemplate(&b, "unmarshal_json.go.tpl", data)
if err != nil {
return "", fmt.Errorf("unmarshal execute unmarshal_json.go.tpl:%w", err)
}
return b.String(), nil
}
func unmarshalTextType(data ValueData) (string, error) {
var b bytes.Buffer
err := parceTpls.Lookup("unmarshal_text.go.tpl").Execute(&b, data)
if err != nil {
return "", fmt.Errorf("execute unmarshal text:%w", err)
}
return b.String(), nil
}
func internalType(data ValueData) (string, error) {
var b bytes.Buffer
err := parceTpls.Lookup("parse.go.tpl").Execute(&b, data)
if err != nil {
return "", fmt.Errorf("internal execute parce.go.tpl:%w", err)
}
return b.String(), nil
}
type ValueData struct {
ViewData
ValName string
Value string
}
func (v ValueData) FuncType() string {
name := reflect.TypeOf(v.ViewData.Type()).Name()
return v.Rendering.FuncName(name)
}
type DataRender struct {
renderType func(data ValueData) (string, error)
renderValue func(data any, view ValueData) (string, error)
}
func (d DataRender) Type(data ValueData) (string, error) {
return d.renderType(data)
}
func (d DataRender) Value(data any, view ValueData) (string, error) {
return d.renderValue(data, view)
}
func NewDataRender(rendeType func(data ValueData) (string, error), renderValue func(data any, view ValueData) (string, error)) DataRender {
if rendeType == nil {
rendeType = anyType
}
if renderValue == nil {
renderValue = anyValue
}
return DataRender{
renderType: rendeType,
renderValue: renderValue,
}
}

View File

@@ -0,0 +1,129 @@
package render_test
import (
"fmt"
"strconv"
"testing"
"gitoa.ru/go-4devs/config/definition/generate/render"
"gitoa.ru/go-4devs/config/definition/generate/view"
"gitoa.ru/go-4devs/config/definition/option"
)
type flagValue int
func (f flagValue) String() string {
return strconv.Itoa(int(f))
}
func (f *flagValue) Set(in string) error {
data, err := strconv.Atoi(in)
if err != nil {
return fmt.Errorf("%w", err)
}
*f = flagValue(data)
return nil
}
func TestValue_FlagType(t *testing.T) {
t.Parallel()
const ex = `pval, perr := val.ParseString()
if perr != nil {
return v, fmt.Errorf("read [%v]:%w",[]string{"flagValue"}, perr)
}
return v, v.Set(pval)`
viewData := render.NewViewData(nil, view.NewView(option.New("flag_value", "flag desc", flagValue(0)), nil))
result := render.Value("val", "v", viewData)
if result != ex {
t.Errorf("failed render flag type ex:%s, res:%s", ex, result)
}
}
func TestData_Flag(t *testing.T) {
t.Parallel()
const ex = `return {{.val}}, {{.val}}.Set("42")`
viewData := render.NewViewData(nil, view.NewView(option.New("flag_value", "flag desc", flagValue(0)), nil))
result := render.Data(flagValue(42), "val", viewData)
if result != ex {
t.Errorf("failed render flag value ex:%s, res:%s", ex, result)
}
}
type scanValue int
func (s *scanValue) Scan(src any) error {
res, _ := src.(string)
data, err := strconv.Atoi(res)
if err != nil {
return fmt.Errorf("%w", err)
}
*s = scanValue(data)
return nil
}
func TestValue_Scan(t *testing.T) {
t.Parallel()
const ex = `return v, v.Scan(val.Any())`
viewData := render.NewViewData(nil, view.NewView(option.New("scan_value", "scan desc", scanValue(42)), nil))
result := render.Value("val", "v", viewData)
if result != ex {
t.Errorf("failed render flag value ex:%s, res:%s", ex, result)
}
}
type textData string
func (j *textData) UnmarshalText(in []byte) error {
val := string(in)
*j = textData(val)
return nil
}
func TestData_UnmarshalText(t *testing.T) {
t.Parallel()
const ex = `return {{.val}}, {{.val}}.UnmarshalText("4devs")`
data := textData("4devs")
viewData := render.NewViewData(nil, view.NewView(option.New("tvalue", "unmarshal text desc", textData("")), nil))
result := render.Data(data, "val", viewData)
if result != ex {
t.Errorf("failed render flag value ex:%s, res:%s", ex, result)
}
}
func TestValue_UnmarshalText(t *testing.T) {
t.Parallel()
const ex = `pval, perr := val.ParseString()
if perr != nil {
return v, fmt.Errorf("read [%v]:%w", []string{"tvalue"}, perr)
}
return v, v.UnmarshalText([]byte(pval))`
viewData := render.NewViewData(nil, view.NewView(option.New("tvalue", "unmarshal text desc", textData("")), nil))
result := render.Value("val", "v", viewData)
if result != ex {
t.Errorf("failed render flag value ex:%s, res:%s", ex, result)
}
}

View File

@@ -1,39 +0,0 @@
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
}

View File

@@ -0,0 +1,8 @@
// Code generated gitoa.ru/go-4devs/config DO NOT EDIT.
package {{.Pkg}}
import (
{{range .Imports}}
{{- .Alias }} "{{ .Package }}"
{{end}}
)

View File

@@ -1,63 +0,0 @@
package generate
import (
"fmt"
"io"
"sync"
"gitoa.ru/go-4devs/config/definition"
)
//nolint:gochecknoglobals
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
}
//nolint:forcetypeassert
func get(kind string) Handle {
handler, 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 handler.(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(w io.Writer, handler Handler, opt 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
}

View File

@@ -0,0 +1,191 @@
package view
import (
"fmt"
"reflect"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/param"
)
type key int
const (
viewParamFunctName key = iota + 1
viewParamSkipContext
)
func WithSkipContext(p param.Params) param.Params {
return param.With(p, viewParamSkipContext, true)
}
func WithContext(p param.Params) param.Params {
return param.With(p, viewParamSkipContext, false)
}
func IsSkipContext(p param.Params) bool {
data, has := p.Param(viewParamSkipContext)
if has {
skip, ok := data.(bool)
return ok && skip
}
return false
}
type Option func(*View)
func WithParent(name string) Option {
return func(v *View) {
v.parent = name
}
}
func WithKeys(keys ...string) Option {
return func(v *View) {
v.keys = keys
}
}
func NewViews(name string, get param.Params, option config.Options, opts ...Option) View {
view := newView(name, get, option, opts...)
for _, op := range option.Options() {
view.children = append(view.children, NewView(op, get, WithParent(name)))
}
return view
}
type IOption any
func newView(name string, get param.Params, in any, opts ...Option) View {
vi := View{
kind: reflect.TypeOf(in),
name: name,
Params: get,
dtype: param.Type(get),
children: nil,
keys: nil,
parent: "",
}
for _, opt := range opts {
opt(&vi)
}
return vi
}
func NewView(opt config.Option, get param.Params, opts ...Option) View {
vi := newView(opt.Name(), param.Chain(get, opt), opt, opts...)
if data, ok := opt.(config.Group); ok {
for _, chi := range data.Options() {
vi.children = append(vi.children, NewView(
chi,
param.Chain(vi.Params, chi),
WithParent(vi.ParentName()+"_"+opt.Name()),
WithKeys(append(vi.keys, opt.Name())...),
))
}
}
return vi
}
type View struct {
param.Params
children []View
keys []string
kind reflect.Type
name string
parent string
dtype any
}
func (v View) Types() []any {
types := make([]any, 0)
if v.dtype != nil {
types = append(types, v.dtype)
}
for _, child := range v.children {
types = append(types, child.Types()...)
}
return types
}
func (v View) Kind() reflect.Type {
return v.kind
}
func (v View) Views() []View {
return v.children
}
func (v View) Param(key any) string {
data, ok := v.Params.Param(key)
if !ok {
return ""
}
if res, ok := data.(string); ok {
return res
}
return fmt.Sprintf("%v", data)
}
func (v View) SkipContext() bool {
return IsSkipContext(v.Params)
}
func (v View) Name() string {
return v.name
}
func (v View) Keys() []string {
return v.keys
}
func (v View) Type() any {
return v.dtype
}
func (v View) FuncName() string {
data, ok := v.Params.Param(viewParamFunctName)
name, valid := data.(string)
if !ok || !valid {
return v.name
}
return name
}
func (v View) ParentName() string {
return v.parent
}
func (v View) Description() string {
return option.DataDescription(v.Params)
}
func (v View) Default() any {
data, ok := option.DataDefaut(v.Params)
if !ok {
return nil
}
return data
}
func (v View) HasDefault() bool {
return option.HasDefaut(v.Params)
}

View File

@@ -1,5 +0,0 @@
module gitoa.ru/go-4devs/config/definition
go 1.21
require github.com/iancoleman/strcase v0.3.0

View File

@@ -1,2 +0,0 @@
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=

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

@@ -1,27 +1,34 @@
package group
import (
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/param"
)
const Kind = "group"
var _ config.Group = New("", "")
var _ definition.Option = Group{}
func New(name, desc string, opts ...definition.Option) Group {
return Group{
Name: name,
Description: desc,
Options: opts,
func New(name, desc string, opts ...config.Option) Group {
group := Group{
name: name,
opts: opts,
Params: param.New(option.Description(desc)),
}
return group
}
type Group struct {
Options definition.Options
Name string
Description string
param.Params
name string
opts []config.Option
}
func (o Group) Kind() string {
return Kind
func (g Group) Name() string {
return g.name
}
func (g Group) Options() []config.Option {
return g.opts
}

View File

@@ -1,88 +0,0 @@
package group
import (
"fmt"
"io"
"text/template"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/generate"
)
//nolint:gochecknoinits
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("%w:%T", generate.ErrWrongType, 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 (c ChildData) Keys() []string {
return c.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)
}
//nolint:gochecknoglobals
var (
tpl = template.Must(template.New("tpls").Parse(gpoupTemplate))
gpoupTemplate = `type {{.StructName}} struct {
{{.ParentName}}
}
// {{.FuncName}} {{.Description}}.
func (i {{.ParentName}}) {{.FuncName}}() {{.StructName}} {
return {{.StructName}}{i}
}
`
)

View File

@@ -1,27 +0,0 @@
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
}

View File

@@ -0,0 +1,30 @@
package option
import (
"errors"
"fmt"
)
type Error struct {
Key []string
Err error
}
func (o Error) Error() string {
return fmt.Sprintf("%s: %s", o.Key, o.Err)
}
func (o Error) Is(err error) bool {
return errors.Is(err, o.Err)
}
func (o Error) Unwrap() error {
return o.Err
}
func Err(err error, key []string) Error {
return Error{
Key: key,
Err: err,
}
}

102
definition/option/option.go Executable file → Normal file
View File

@@ -1,100 +1,66 @@
package option
import (
"gitoa.ru/go-4devs/config/definition"
"time"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/param"
)
var _ definition.Option = Option{}
var _ config.Option = New("", "", nil)
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,
func New(name, desc string, vtype any, opts ...param.Option) Option {
opts = append(opts, Description(desc), WithType(vtype))
res := Option{
name: name,
Params: param.New(opts...),
}
for _, opt := range opts {
opt(&option)
}
return option
return res
}
type Option struct {
Name string
Description string
Type any
Default any
Params definition.Params
param.Params
name string
}
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) Name() string {
return o.name
}
func (o Option) Kind() string {
return Kind
func String(name, description string, opts ...param.Option) Option {
return New(name, description, "", opts...)
}
func Time(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeTime, opts...)
func Bool(name, description string, opts ...param.Option) Option {
return New(name, description, false, opts...)
}
func Duration(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeDuration, opts...)
func Duration(name, description string, opts ...param.Option) Option {
return New(name, description, time.Duration(0), opts...)
}
func String(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeString, opts...)
func Float64(name, description string, opts ...param.Option) Option {
return New(name, description, float64(0), opts...)
}
func Int(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeInt, opts...)
func Int(name, description string, opts ...param.Option) Option {
return New(name, description, int(0), opts...)
}
func Int64(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeInt64, opts...)
func Int64(name, description string, opts ...param.Option) Option {
return New(name, description, int64(0), opts...)
}
func Uint(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeUint, opts...)
func Time(name, description string, opts ...param.Option) Option {
return New(name, description, time.Time{}, opts...)
}
func Uint64(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeUint64, opts...)
func Uint(name, description string, opts ...param.Option) Option {
return New(name, description, uint(0), 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...)
func Uint64(name, descriontion string, opts ...param.Option) Option {
return New(name, descriontion, uint64(0), opts...)
}

125
definition/option/params.go Normal file
View File

@@ -0,0 +1,125 @@
package option
import (
"gitoa.ru/go-4devs/config/param"
)
type key int
const (
paramHidden key = iota + 1
paramDefault
paramDesc
paramRequired
paramSlice
paramBool
paramPos
paramShort
)
func Short(in rune) param.Option {
return func(v param.Params) param.Params {
return param.With(v, paramShort, string(in))
}
}
func ParamShort(fn param.Params) (string, bool) {
data, ok := param.String(fn, paramShort)
return data, ok
}
func HasShort(short string) param.Has {
return func(fn param.Params) bool {
data, ok := param.String(fn, paramShort)
return ok && data == short
}
}
func WithType(in any) param.Option {
return func(v param.Params) param.Params {
out := param.WithType(in)(v)
if _, ok := in.(bool); ok {
return param.With(out, paramBool, ok)
}
return out
}
}
func Position(pos uint64) param.Option {
return func(p param.Params) param.Params {
return param.With(p, paramPos, pos)
}
}
func Hidden(v param.Params) param.Params {
return param.With(v, paramHidden, true)
}
func Required(v param.Params) param.Params {
return param.With(v, paramRequired, true)
}
func Slice(v param.Params) param.Params {
return param.With(v, paramSlice, true)
}
func Default(in any) param.Option {
return func(v param.Params) param.Params {
return param.With(v, paramDefault, in)
}
}
func Description(in string) param.Option {
return func(v param.Params) param.Params {
return param.With(v, paramDesc, in)
}
}
func HasDefaut(fn param.Params) bool {
_, ok := fn.Param(paramDefault)
return ok
}
func DataPosition(fn param.Params) (uint64, bool) {
return param.Uint64(paramPos, fn)
}
func DataDefaut(fn param.Params) (any, bool) {
data, ok := fn.Param(paramDefault)
return data, ok
}
func IsSlice(fn param.Params) bool {
data, ok := param.Bool(paramSlice, fn)
return ok && data
}
func IsBool(fn param.Params) bool {
data, ok := param.Bool(paramBool, fn)
return ok && data
}
func IsHidden(fn param.Params) bool {
data, ok := param.Bool(paramHidden, fn)
return ok && data
}
func IsRequired(fn param.Params) bool {
data, ok := param.Bool(paramRequired, fn)
return ok && data
}
func DataDescription(fn param.Params) string {
data, _ := param.String(fn, paramDesc)
return data
}

View File

@@ -1,33 +0,0 @@
// 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
}

View File

@@ -1,8 +0,0 @@
{{block "UnmarshalJSON" . }}
pval, perr := {{.ValName}}.ParseString()
if perr != nil {
return {{.Value}}, fmt.Errorf("read {{.Keys}}:%w", perr)
}
return {{.Value}}, {{.Value}}.UnmarshalJSON([]byte(pval))
{{end}}

View File

@@ -1,8 +0,0 @@
{{block "UnmarshalText" . }}
pval, perr := {{.ValName}}.ParseString()
if perr != nil {
return {{.Value}}, fmt.Errorf("read {{.Keys}}:%w", perr)
}
return {{.Value}}, {{.Value}}.UnmarshalText([]byte(pval))
{{end}}

View File

@@ -1,235 +0,0 @@
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
//nolint:gochecknoglobals
var tpl = template.Must(template.New("tpls").ParseFS(tpls, "tpl/*.tmpl"))
//nolint:gochecknoinits
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 {
name, _ := funcName.(string)
return name
}
return generate.FuncName(v.Name)
}
func (v View) Description() string {
if desc, ok := v.Option.Params.Get(ViewParamDescription); ok {
description, _ := desc.(string)
return description
}
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
}
//nolint:gochecknoglobals,unparam
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 "", fmt.Errorf("execute unmarshal text:%w", 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 "", fmt.Errorf("execute unmarshal json:%w", 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 "", fmt.Errorf("execute parse.tmpl:%w", err)
}
return b.String(), nil
}
//nolint:gochecknoglobals
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
}

View File

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

View File

@@ -1,27 +1,33 @@
package proto
import (
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/key"
"gitoa.ru/go-4devs/config/param"
)
const Kind = "proto"
var _ config.Group = New("", "")
func New(name, desc string, opt definition.Option) Proto {
pr := Proto{
Name: name,
Description: desc,
Option: opt,
func New(name string, desc string, opts ...config.Option) Proto {
return Proto{
name: key.Wild(name),
opts: opts,
Params: param.New(option.Description(desc)),
}
return pr
}
type Proto struct {
Name string
Description string
Option definition.Option
param.Params
opts []config.Option
name string
}
func (p Proto) Kind() string {
return Kind
func (p Proto) Options() []config.Option {
return p.opts
}
func (p Proto) Name() string {
return p.name
}

View File

@@ -1,80 +0,0 @@
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"
)
//nolint:gochecknoinits
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("%w:%T", generate.ErrWrongType, 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("%w:%T", generate.ErrWrongType, opt)
}
//nolint:gochecknoglobals
var (
tpl = template.Must(template.New("tpls").Funcs(template.FuncMap{"join": strings.Join}).Parse(templateOption))
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
}
`
)

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

View File

@@ -1,32 +0,0 @@
package definition
import (
"context"
"time"
"github.com/sirupsen/logrus"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/group"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/definition/proto"
"gitoa.ru/go-4devs/log/level"
)
func Configure(ctx context.Context, def *definition.Definition) error {
def.Add(
option.String("test", "test description", option.Default("defult")),
option.Int("int", "test int description", option.Default(2)),
group.New("group", "group description", option.String("test", "test description")),
option.Time("start", "start at", option.Default(time.Now())),
option.Duration("timer", "timer", option.Default(time.Hour)),
group.New("log", "logger",
option.New("level", "log level", level.Level(0), option.Default(level.Debug)),
option.New("logrus", "logrus level", logrus.Level(0), option.Default(logrus.DebugLevel)),
proto.New("sevice", "cutom service log", option.New("level", "log level", level.Level(0), option.Default(level.Debug))),
),
option.New("erors", "skiped errors", []string{}),
proto.New("proto_errors", "proto errors", option.New("erors", "skiped errors", []string{})),
)
return nil
}

View File

@@ -1,61 +0,0 @@
//go:build ignore
// +build ignore
package main
import (
"context"
"fmt"
"go/format"
"os"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/generate"
"gitoa.ru/go-4devs/config/eample"
)
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stdout, err)
os.Exit(1)
}
}
func run() error {
ctx := context.Background()
def := definition.New()
if err := eample.Configure(ctx, &def); err != nil {
return err
}
f, err := os.Create("eample/defenition_input.go")
if err != nil {
return err
}
gerr := generate.Run(f, "eample", def, generate.ViewOption{
Struct: "Configure",
Suffix: "Input",
Errors: generate.ViewErrors{
Default: []string{
"gitoa.ru/go-4devs/config.ErrVariableNotFound",
},
},
})
if gerr != nil {
return gerr
}
in, err := os.ReadFile(f.Name())
if err != nil {
return err
}
out, err := format.Source(in)
if err != nil {
return err
}
return os.WriteFile(f.Name(), out, 0644)
}

View File

@@ -1,14 +0,0 @@
module gitoa.ru/go-4devs/config/example
go 1.21.5
require (
github.com/sirupsen/logrus v1.9.3
gitoa.ru/go-4devs/config/definition v0.0.0-20240125203435-5586adc4e3d8
gitoa.ru/go-4devs/log v0.5.3
)
require (
github.com/iancoleman/strcase v0.3.0 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
)

View File

@@ -1,21 +0,0 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gitoa.ru/go-4devs/config/definition v0.0.0-20240125203435-5586adc4e3d8 h1:PZ4SE0gq719+lXorGaRPfGSSO2JpfxTWCWOSsna+7Yw=
gitoa.ru/go-4devs/config/definition v0.0.0-20240125203435-5586adc4e3d8/go.mod h1:jV6jF0PsK4ffNC8hrBxMx33PVJtnW5O6S/sGNJDqrd4=
gitoa.ru/go-4devs/log v0.5.3 h1:o/4DcypxbgQ9GEfUWrZ3FVrVfttuJgLs2ptMVPj47sE=
gitoa.ru/go-4devs/log v0.5.3/go.mod h1:tREtjEH2cTHl0p3uCVcH9g5tlqtsVNI/tDQVfq53Ty4=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

98
factory.go Normal file
View File

@@ -0,0 +1,98 @@
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
}

2
go.mod
View File

@@ -1,3 +1,3 @@
module gitoa.ru/go-4devs/config
go 1.21
go 1.23

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

15
key/wild.go Normal file
View File

@@ -0,0 +1,15 @@
package key
const minWildCount = 3
func IsWild(name string) bool {
if len(name) < minWildCount {
return false
}
return name[0] == '{' && name[len(name)-1] == '}'
}
func Wild(name string) string {
return "{" + name + "}"
}

34
param/helper.go Normal file
View File

@@ -0,0 +1,34 @@
package param
func String(fn Params, key any) (string, bool) {
val, ok := fn.Param(key)
if !ok {
return "", false
}
data, ok := val.(string)
return data, ok
}
func Bool(key any, fn Params) (bool, bool) {
val, ok := fn.Param(key)
if !ok {
return false, false
}
data, ok := val.(bool)
return data, ok
}
func Uint64(key any, fn Params) (uint64, bool) {
data, ok := fn.Param(key)
if !ok {
return 0, false
}
res, ok := data.(uint64)
return res, ok
}

30
param/keys.go Normal file
View File

@@ -0,0 +1,30 @@
package param
type key int
const (
paramTimeFormat key = iota + 1
paramType
)
func WithTimeFormat(format string) Option {
return func(p Params) Params {
return With(p, paramTimeFormat, format)
}
}
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
}

74
param/param.go Normal file
View File

@@ -0,0 +1,74 @@
package param
import (
"slices"
)
var emptyParam = empty{}
type (
Option func(p Params) Params
Has func(Params) bool
Params interface {
Param(key any) (any, bool)
}
)
func Chain(vals ...Params) Params {
slices.Reverse(vals)
return chain(vals)
}
func With(parent Params, key, val any) Params {
return value{
Params: parent,
key: key,
val: val,
}
}
func New(opts ...Option) Params {
var parms Params
parms = emptyParam
for _, opt := range opts {
parms = opt(parms)
}
return parms
}
type empty struct{}
func (v empty) Param(_ any) (any, bool) {
return nil, false
}
type value struct {
Params
key, val any
}
func (v value) Param(key any) (any, bool) {
if v.key == key {
return v.val, true
}
return v.Params.Param(key)
}
type chain []Params
func (c chain) Param(key any) (any, bool) {
for _, p := range c {
val, ok := p.Param(key)
if ok {
return val, ok
}
}
return nil, false
}

49
param/param_test.go Normal file
View File

@@ -0,0 +1,49 @@
package param_test
import (
"testing"
"gitoa.ru/go-4devs/config/param"
)
func TestChainReplace(t *testing.T) {
t.Parallel()
const (
replaceParam = "param1"
replaceValue = "replace"
)
params1 := param.With(param.New(), replaceParam, "param1")
params2 := param.With(param.New(), replaceParam, replaceValue)
data, ok := param.String(param.Chain(params1, params2), replaceParam)
if !ok {
t.Errorf("param %v: not found", replaceParam)
}
if data != replaceValue {
t.Errorf("got:%v, expect:%v", data, replaceValue)
}
}
func TestChainExtend(t *testing.T) {
t.Parallel()
const (
extendParam = "param1"
extendValue = "replace"
)
params1 := param.With(param.New(), extendParam, extendValue)
params2 := param.With(param.New(), "new_value", "param2")
data1, ok := param.String(param.Chain(params1, params2), extendParam)
if !ok {
t.Errorf("param %v: not found", extendParam)
}
if data1 != extendValue {
t.Errorf("got:%v, expect:%v", data1, extendParam)
}
}

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
@@ -18,3 +18,33 @@ type WatchProvider interface {
}
type Factory func(ctx context.Context, cfg Provider) (Provider, error)
type Option interface {
Name() string
Param(key any) (any, bool)
}
type Group interface {
Option
Options
}
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)
}

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,50 +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
}
// nolint: cyclop
// return name, value, error.
func (p *Provider) parseOne(arg string) (string, string, error) {
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)
}
data, err := i.Map.Value(ctx, key...)
if err != nil {
return nil, fmt.Errorf("%w", err)
}
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 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)
}
err = i.parseShortOption(arg[1:], def)
default:
err = i.parseArgument(arg, def)
}
if err != nil {
return fmt.Errorf("%w", err)
}
}
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.
func (i *Argv) parseOne(arg string) (string, string, error) {
if arg[0] != '-' {
return "", "", nil
}
@@ -82,55 +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
}
func (p *Provider) Name() string {
return p.name
}
func (p *Provider) Value(_ context.Context, path ...string) (config.Value, error) {
if err := p.parse(); err != nil {
return nil, err
}
name := p.key(path...)
if val, ok := p.args[name]; ok {
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)
}
return value.Decode(func(v interface{}) error {
if err := json.Unmarshal(data, v); err != nil {
return fmt.Errorf("unmarshal:%w", err)
}
return nil
}), nil
}
}
return nil, fmt.Errorf("%s:%w", p.Name(), config.ErrValueNotFound)
}

View File

@@ -65,5 +65,5 @@ func (d *Duration) UnmarshalJSON(in []byte) error {
}
func (d *Duration) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%q", d)), nil
return fmt.Appendf(nil, "%q", d), 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

@@ -26,6 +26,7 @@ func New(namespace, appName string, opts ...Option) *Provider {
return strings.ToUpper(strings.Join(path, "_"))
},
prefix: strings.ToUpper(namespace + "_" + appName + "_"),
name: "",
}
for _, opt := range opts {
@@ -51,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

@@ -1,7 +1,6 @@
package env_test
import (
"os"
"testing"
"gitoa.ru/go-4devs/config/provider/env"
@@ -9,10 +8,8 @@ import (
)
func TestProvider(t *testing.T) {
t.Parallel()
os.Setenv("FDEVS_CONFIG_DSN", test.DSN)
os.Setenv("FDEVS_CONFIG_PORT", "8080")
t.Setenv("FDEVS_CONFIG_DSN", test.DSN)
t.Setenv("FDEVS_CONFIG_PORT", "8080")
provider := env.New("fdevs", "config")

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,10 +1,10 @@
module gitoa.ru/go-4devs/config/provider/etcd
go 1.21
go 1.23
require (
github.com/stretchr/testify v1.8.4
gitoa.ru/go-4devs/config v0.0.2
gitoa.ru/go-4devs/config v0.0.3
go.etcd.io/etcd/api/v3 v3.5.11
go.etcd.io/etcd/client/v3 v3.5.11
)

View File

@@ -31,6 +31,8 @@ gitoa.ru/go-4devs/config v0.0.1 h1:9KrOO09YbIMO8qL8aVn/G74DurGdOIW5y3O02bays4I=
gitoa.ru/go-4devs/config v0.0.1/go.mod h1:xfEC2Al9xnMLJUuekYs3KhJ5BIzWAseNwkMwbN6/xss=
gitoa.ru/go-4devs/config v0.0.2 h1:bkTxW57kDDMf4cj/8W7fxPSN7JCPWEqlhCmL6LP3Vzg=
gitoa.ru/go-4devs/config v0.0.2/go.mod h1:xfEC2Al9xnMLJUuekYs3KhJ5BIzWAseNwkMwbN6/xss=
gitoa.ru/go-4devs/config v0.0.3 h1:+ecwDQj4fneJCh2uLNNAonm4cUJdGmlfxUsFhQRI9Ko=
gitoa.ru/go-4devs/config v0.0.3/go.mod h1:UINWnObZA0nLiJro+TtavUBBvN0cSt17aRHOk20pP74=
go.etcd.io/etcd/api/v3 v3.5.11 h1:B54KwXbWDHyD3XYAwprxNzTe7vlhR69LuBgZnMVvS7E=
go.etcd.io/etcd/api/v3 v3.5.11/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4=
go.etcd.io/etcd/client/pkg/v3 v3.5.11 h1:bT2xVspdiCj2910T0V+/KHcVKjkUrCZVtk8J2JF2z1A=

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,15 +1,10 @@
module gitoa.ru/go-4devs/config/provider/ini
go 1.21
go 1.23
require (
github.com/stretchr/testify v1.8.4
gitoa.ru/go-4devs/config v0.0.1
gitoa.ru/go-4devs/config v0.0.3
gopkg.in/ini.v1 v1.67.0
)
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
)
require github.com/stretchr/testify v1.11.1 // indirect

View File

@@ -2,12 +2,10 @@ 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/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=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gitoa.ru/go-4devs/config v0.0.3 h1:+ecwDQj4fneJCh2uLNNAonm4cUJdGmlfxUsFhQRI9Ko=
gitoa.ru/go-4devs/config v0.0.3/go.mod h1:UINWnObZA0nLiJro+TtavUBBvN0cSt17aRHOk20pP74=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -5,9 +5,9 @@ import (
"testing"
"time"
"github.com/stretchr/testify/require"
"gitoa.ru/go-4devs/config/provider/ini"
"gitoa.ru/go-4devs/config/test"
"gitoa.ru/go-4devs/config/test/require"
lib "gopkg.in/ini.v1"
)

View File

@@ -1,10 +1,10 @@
module gitoa.ru/go-4devs/config/provider/json
go 1.21
go 1.23
require (
github.com/tidwall/gjson v1.17.0
gitoa.ru/go-4devs/config v0.0.1
github.com/tidwall/gjson v1.18.0
gitoa.ru/go-4devs/config v0.0.3
)
require (

View File

@@ -1,10 +1,10 @@
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
gitoa.ru/go-4devs/config v0.0.0-20240125174937-085589a9383a h1:61iwpn1Ec4yIkBSvSz8knNENJuhj6v2rp6bfw1wkG0E=
gitoa.ru/go-4devs/config v0.0.0-20240125174937-085589a9383a/go.mod h1:3g2bwE2OTDyYwm33KN/Cqc8pEdGlWXnB8Ju3PsYNQr0=
gitoa.ru/go-4devs/config v0.0.1 h1:9KrOO09YbIMO8qL8aVn/G74DurGdOIW5y3O02bays4I=
gitoa.ru/go-4devs/config v0.0.1/go.mod h1:xfEC2Al9xnMLJUuekYs3KhJ5BIzWAseNwkMwbN6/xss=
gitoa.ru/go-4devs/config v0.0.3 h1:+ecwDQj4fneJCh2uLNNAonm4cUJdGmlfxUsFhQRI9Ko=
gitoa.ru/go-4devs/config v0.0.3/go.mod h1:UINWnObZA0nLiJro+TtavUBBvN0cSt17aRHOk20pP74=

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,45 @@
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=

Some files were not shown because too many files have changed in this diff Show More