77 Commits

Author SHA1 Message Date
bba135f061 Merge pull request 'add providers interface' (#43) from providers into master
All checks were successful
Go Action / goaction (push) Successful in 56s
Reviewed-on: #43
2026-01-05 14:07:11 +03:00
0cf039a3bc add providers interface
All checks were successful
Go Action / goaction (pull_request) Successful in 1m20s
2026-01-05 14:02:56 +03:00
963a2f1b6c Merge pull request 'add atr dump style' (#42) from style into master
All checks were successful
Go Action / goaction (push) Successful in 1m1s
Reviewed-on: #42
2026-01-04 22:18:30 +03:00
46280e5377 add atr dump style
All checks were successful
Go Action / goaction (pull_request) Successful in 1m5s
2026-01-04 22:16:19 +03:00
a23a2d8eb5 Merge pull request 'move option position to param' (#41) from def into master
All checks were successful
Go Action / goaction (push) Successful in 56s
Reviewed-on: #41
2026-01-04 18:56:20 +03:00
d2cc93a97e move option position to param
All checks were successful
Go Action / goaction (pull_request) Successful in 1m15s
2026-01-04 18:53:12 +03:00
07bded1e6a Merge pull request 'move default from option to param' (#40) from def into master
All checks were successful
Go Action / goaction (push) Successful in 1m2s
Reviewed-on: #40
2026-01-04 18:04:32 +03:00
ab536ad876 move default from option to param
All checks were successful
Go Action / goaction (pull_request) Successful in 1m15s
2026-01-04 18:02:19 +03:00
3ec9ee48ab Merge pull request 'dump-reference' (#39) from dump-reference into master
All checks were successful
Go Action / goaction (push) Successful in 1m0s
Reviewed-on: #39
2026-01-04 17:49:02 +03:00
f9f4f90dc8 add dump reference arg provider
All checks were successful
Go Action / goaction (pull_request) Successful in 1m5s
2026-01-04 17:46:38 +03:00
28e03c727b move helper to file 2026-01-04 17:45:58 +03:00
7fcde79266 Merge pull request 'update config options' (#38) from dump-reference into master
All checks were successful
Go Action / goaction (push) Successful in 1m1s
Reviewed-on: #38
2026-01-03 17:32:29 +03:00
23de85f8a1 update create def by vars
All checks were successful
Go Action / goaction (pull_request) Successful in 1m19s
2026-01-03 17:30:34 +03:00
a52e470906 update config options
Some checks failed
Go Action / goaction (pull_request) Failing after 47s
2026-01-03 17:27:37 +03:00
9a189aad47 Merge pull request 'add test arg bind' (#37) from dump-reference into master
All checks were successful
Go Action / goaction (push) Successful in 1m1s
Reviewed-on: #37
2026-01-03 17:26:34 +03:00
8b2f2ea660 add test arg bind
All checks were successful
Go Action / goaction (pull_request) Successful in 1m18s
2026-01-03 17:08:45 +03:00
3054acf0c9 Merge pull request 'dump-reference' (#36) from dump-reference into master
All checks were successful
Go Action / goaction (push) Successful in 56s
Reviewed-on: #36
2026-01-03 14:53:12 +03:00
deca96cf6b add dump refereence env provider
All checks were successful
Go Action / goaction (pull_request) Successful in 1m17s
2026-01-03 14:51:00 +03:00
973aa29ccd set parallel test 2026-01-03 14:50:00 +03:00
39043f1304 move description param from option to param 2026-01-03 14:09:02 +03:00
87c0106ee5 upadate check wild key 2026-01-03 14:08:17 +03:00
f277358d9a Merge pull request 'update string value' (#35) from value into master
All checks were successful
Go Action / goaction (push) Successful in 57s
Reviewed-on: #35
2026-01-03 00:22:47 +03:00
deb258aa42 update string value
All checks were successful
Go Action / goaction (pull_request) Successful in 1m7s
2026-01-03 00:21:06 +03:00
6eabe7c28c Merge pull request 'format' (#34) from format into master
All checks were successful
Go Action / goaction (push) Successful in 54s
Reviewed-on: #34
2026-01-02 23:53:48 +03:00
88bc251577 add json processor
All checks were successful
Go Action / goaction (pull_request) Successful in 56s
2026-01-02 23:52:05 +03:00
74d8cad719 update pointer value 2026-01-02 23:51:35 +03:00
1169e25504 Merge pull request 'provider add format' (#33) from format into master
All checks were successful
Go Action / goaction (push) Successful in 57s
Reviewed-on: #33
2026-01-02 23:21:19 +03:00
a191e82526 provider add format
All checks were successful
Go Action / goaction (pull_request) Successful in 57s
2026-01-02 23:19:41 +03:00
2ce5efdcdd Merge pull request 'processor' (#32) from processor into master
All checks were successful
Go Action / goaction (push) Successful in 53s
Reviewed-on: #32
2026-01-02 22:44:19 +03:00
ef591885eb add provider processor
All checks were successful
Go Action / goaction (pull_request) Successful in 1m9s
2026-01-02 22:42:20 +03:00
d3418959d1 add csv processor 2026-01-02 22:42:02 +03:00
e60c0e25d5 value add helper parse slice string 2026-01-02 22:41:17 +03:00
dac9a87743 param add helper get rune 2026-01-02 22:40:23 +03:00
e7ac06a61c update key map, set priority find by name 2026-01-02 22:39:20 +03:00
b190ccd34e Merge pull request 'add key processor' (#31) from key into master
All checks were successful
Go Action / goaction (push) Successful in 44s
Reviewed-on: #31
2026-01-01 20:40:46 +03:00
01d707ccbf add key processor
All checks were successful
Go Action / goaction (pull_request) Successful in 2m15s
2026-01-01 19:33:45 +03:00
bb03532d83 Merge pull request 'update env processor' (#30) from processor into master
All checks were successful
Go Action / goaction (push) Successful in 50s
Reviewed-on: #30
2025-12-31 23:09:23 +03:00
57c035d72e update env processor
All checks were successful
Go Action / goaction (pull_request) Successful in 1m3s
2025-12-31 23:03:30 +03:00
85618ce0bd Merge pull request 'add env processor' (#29) from processor into master
All checks were successful
Go Action / goaction (push) Successful in 49s
Reviewed-on: #29
2025-12-31 20:32:54 +03:00
81eb902a54 add env processor
All checks were successful
Go Action / goaction (pull_request) Successful in 53s
2025-12-31 20:31:09 +03:00
8d54d7dbae Merge pull request 'update factory provider' (#28) from factory into master
All checks were successful
Go Action / goaction (push) Successful in 45s
Reviewed-on: #28
2025-12-30 22:55:08 +03:00
382bd117c1 update factory provider
All checks were successful
Go Action / goaction (pull_request) Successful in 56s
2025-12-30 22:53:06 +03:00
6e1192772a Merge pull request 'fix typo' (#27) from parse into master
All checks were successful
Go Action / goaction (push) Successful in 42s
Reviewed-on: #27
2025-12-30 21:57:21 +03:00
0054ebf7e6 fix typo
All checks were successful
Go Action / goaction (pull_request) Successful in 51s
2025-12-30 21:55:25 +03:00
783b4dd3b5 Merge pull request 'group' (#26) from group into master
All checks were successful
Go Action / goaction (push) Successful in 36s
Reviewed-on: #26
2025-12-29 22:25:31 +03:00
8d15b51248 add generate heper config
All checks were successful
Go Action / goaction (pull_request) Successful in 4m52s
2025-12-29 22:19:11 +03:00
302af61012 set definition as option 2025-12-29 22:18:13 +03:00
360ee322f2 update definition group 2025-12-29 21:45:47 +03:00
3aa8a30f3f Merge pull request 'update pos argument' (#25) from arg into master
All checks were successful
Go Action / goaction (push) Successful in 51s
Reviewed-on: #25
2025-12-27 19:31:02 +03:00
1fb591bb22 update pos argument
All checks were successful
Go Action / goaction (pull_request) Successful in 52s
2025-12-27 19:29:30 +03:00
4547adab23 Merge pull request 'update dasel provider' (#24) from dasel into master
All checks were successful
Go Action / goaction (push) Successful in 45s
Reviewed-on: #24
2025-12-27 16:31:34 +03:00
7ca860c127 update dasel provider
All checks were successful
Go Action / goaction (pull_request) Successful in 38s
2025-12-27 16:30:24 +03:00
b6aa83b53e Merge pull request 'add dasel provider' (#23) from dasel into master
All checks were successful
Go Action / goaction (push) Successful in 53s
Reviewed-on: #23
2025-12-27 16:12:16 +03:00
e5637b2a49 add dasel provider
All checks were successful
Go Action / goaction (pull_request) Successful in 58s
2025-12-27 16:10:10 +03:00
1be25e67a3 Merge pull request 'update duration jbytes' (#22) from jbytes into master
All checks were successful
Go Action / goaction (push) Successful in 46s
Reviewed-on: #22
2025-12-27 15:05:58 +03:00
c81d6ee010 update duration jbytes
All checks were successful
Go Action / goaction (pull_request) Successful in 58s
2025-12-27 15:04:07 +03:00
5e0e1f5c65 Merge pull request 'update yaml provider' (#21) from yaml into master
All checks were successful
Go Action / goaction (push) Successful in 33s
Reviewed-on: #21
2025-12-27 12:12:22 +03:00
3cdadd465b update yaml provider
All checks were successful
Go Action / goaction (pull_request) Successful in 40s
2025-12-27 12:10:58 +03:00
be629c70f1 Merge pull request 'update workflows' (#20) from workflow into master
All checks were successful
Go Action / goaction (push) Successful in 32s
Reviewed-on: #20
2025-12-27 11:53:55 +03:00
bf901485ce add ignore change workflow provider
All checks were successful
Go Action / goaction (pull_request) Successful in 31s
2025-12-27 11:53:12 +03:00
7990d1b4ff update workflows
All checks were successful
Go Action / goaction (pull_request) Successful in 33s
2025-12-27 11:48:55 +03:00
d5250f2c4e Merge pull request 'update provider vault' (#19) from vault into master
All checks were successful
Go Action / goaction (push) Successful in 36s
Reviewed-on: #19
2025-12-27 11:47:01 +03:00
3fc7f6259b update provider vault
All checks were successful
Go Action / goaction (push) Successful in 29s
Go Action / goaction (pull_request) Successful in 1m21s
2025-12-27 11:38:43 +03:00
de35df477b Merge pull request 'update toml' (#18) from toml into master
All checks were successful
Go Action / goaction (push) Successful in 29s
Reviewed-on: #18
2025-12-27 11:25:57 +03:00
0e7f303c2d update toml workflows
All checks were successful
Go Action / goaction (push) Successful in 26s
Go Action / goaction (pull_request) Successful in 35s
2025-12-27 11:21:52 +03:00
90f6f65c5f update toml
All checks were successful
Go Action / goaction (push) Successful in 28s
Go Action / goaction (pull_request) Successful in 26s
2025-12-27 10:50:32 +03:00
cc252cc858 Merge pull request 'update test' (#17) from toml into master
All checks were successful
Go Action / goaction (push) Successful in 28s
Reviewed-on: #17
2025-12-27 10:44:11 +03:00
1e0f4490ba update test
All checks were successful
Go Action / goaction (push) Successful in 27s
Go Action / goaction (pull_request) Successful in 28s
2025-12-27 10:43:31 +03:00
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
177 changed files with 7684 additions and 1699 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

View File

@@ -0,0 +1,38 @@
name: Go Action
on:
push:
branches:
- master
paths:
- 'provider/dasel/**'
- '.gitea/workflows/dasel.yml'
pull_request:
paths:
- 'provider/dasel/**'
- '.gitea/workflows/dasel.yml'
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/dasel
- name: Run go test
run: go test ./...
working-directory: ./provider/dasel

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

@@ -0,0 +1,50 @@
name: Go Action
on:
push:
branches:
- master
paths:
- 'provider/etcd/**'
- '.gitea/workflows/etcd.yml'
pull_request:
paths:
- 'provider/etcd/**'
- '.gitea/workflows/etcd.yml'
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,47 @@
name: Go Action
on:
push:
branches:
- master
pull_request:
paths-ignore:
- 'provider/etcd/**'
- '.gitea/workflows/etcd.yml'
- 'provider/ini/**'
- '.gitea/workflows/ini.yml'
- 'provider/json/**'
- '.gitea/workflows/json.yml'
- 'provider/toml/**'
- '.gitea/workflows/toml.yml'
- 'provider/vault/**'
- '.gitea/workflows/vault.yml'
- 'provider/yaml/**'
- '.gitea/workflows/yaml.yml'
- 'provider/dasel/**'
- '.gitea/workflows/dasel.yml'
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 ./...

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

@@ -0,0 +1,38 @@
name: Go Action
on:
push:
branches:
- master
paths:
- 'provider/ini/**'
- '.gitea/workflows/ini.yml'
pull_request:
paths:
- 'provider/ini/**'
- '.gitea/workflows/ini.yml'
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

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

@@ -0,0 +1,38 @@
name: Go Action
on:
push:
branches:
- master
paths:
- 'provider/json/**'
- '.gitea/workflows/json.yml'
pull_request:
paths:
- 'provider/json/**'
- '.gitea/workflows/json.yml'
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

38
.gitea/workflows/toml.yml Normal file
View File

@@ -0,0 +1,38 @@
name: Go Action
on:
push:
branches:
- master
paths:
- 'provider/toml/**'
- '.gitea/workflows/toml.yml'
pull_request:
paths:
- 'provider/toml/**'
- '.gitea/workflows/toml.yml'
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/toml
- name: Run go test
run: go test ./...
working-directory: ./provider/toml

View File

@@ -0,0 +1,46 @@
name: Go Action
on:
push:
branches:
- master
paths:
- 'provider/vault/**'
- '.gitea/workflows/vault.yml'
pull_request:
paths:
- 'provider/vault/**'
- '.gitea/workflows/vault.yml'
jobs:
goaction:
services:
vault-server:
image: vault:1.13.3
env:
VAULT_DEV_ROOT_TOKEN_ID: "dev"
env:
VAULT_DEV_LISTEN_ADDRESS: 'http://vault-server:8200'
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/vault
- name: Run go test
run: go test ./...
working-directory: ./provider/vault

38
.gitea/workflows/yaml.yml Normal file
View File

@@ -0,0 +1,38 @@
name: Go Action
on:
push:
branches:
- master
paths:
- 'provider/yaml/**'
- '.gitea/workflows/yaml.yml'
pull_request:
paths:
- 'provider/yaml/**'
- '.gitea/workflows/yaml.yml'
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/yaml
- name: Run go test
run: go test ./...
working-directory: ./provider/yaml

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)

127
client.go
View File

@@ -4,11 +4,11 @@ import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
)
func Must(providers ...interface{}) *Client {
var _ Providers = (*Client)(nil)
func Must(providers ...any) *Client {
client, err := New(providers...)
if err != nil {
panic(err)
@@ -17,85 +17,40 @@ 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)),
name: make(map[string]int),
chain: make([]Providers, 0, len(providers)),
}
for idx, prov := range providers {
var name string
switch current := prov.(type) {
case Provider:
client.providers[idx] = current
name = current.Name()
case Factory:
client.providers[idx] = &provider{
factory: func(ctx context.Context) (Provider, error) {
return current(ctx, client)
},
}
client.providers[idx] = WrapFactory(current, client)
name = current.Name()
default:
return nil, fmt.Errorf("provier[%d]: %w %T", idx, ErrUnknowType, prov)
}
client.name[name] = idx
if current, ok := prov.(Providers); ok {
client.chain = append(client.chain, current)
}
}
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
name map[string]int
chain []Providers
}
func (c *Client) Name() string {
@@ -111,7 +66,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 +87,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 +97,45 @@ 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
}
func (c *Client) Provider(name string) (Provider, error) {
if idx, ok := c.name[name]; ok {
return c.providers[idx], nil
}
for _, prov := range c.chain {
if cprov, err := prov.Provider(name); err == nil {
return cprov, nil
}
}
return nil, fmt.Errorf("provider[%v]:%w", c.Name(), ErrNotFound)
}
func (c *Client) Names() []string {
names := make([]string, 0, len(c.providers))
for name := range c.name {
names = append(names, name)
}
for _, prov := range c.chain {
names = append(names, prov.Names()...)
}
return names
}

View File

@@ -11,6 +11,7 @@ import (
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/config/provider/env"
"gitoa.ru/go-4devs/config/provider/factory"
"gitoa.ru/go-4devs/config/provider/watcher"
"gitoa.ru/go-4devs/config/test"
)
@@ -42,14 +43,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 +80,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 +94,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()
@@ -127,7 +128,7 @@ func ExampleClient_Value_factory() {
config, err := config.New(
arg.New(),
config.Factory(func(ctx context.Context, cfg config.Provider) (config.Provider, error) {
factory.New("factory:env", func(ctx context.Context, cfg config.Provider) (config.Provider, error) {
val, err := cfg.Value(ctx, "env")
if err != nil {
return nil, fmt.Errorf("failed read config file:%w", err)

32
cmd/config/main.go Normal file
View File

@@ -0,0 +1,32 @@
package main
import (
"context"
"os"
"os/signal"
"syscall"
"gitoa.ru/go-4devs/config/definition/generate/command"
"gitoa.ru/go-4devs/console"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
ch := make(chan os.Signal, 1)
defer close(ch)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-ch
cancel()
}()
console.
New().
Add(
command.Command(),
).
Execute(ctx)
}

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"

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

@@ -1,29 +1,40 @@
package definition
import (
"fmt"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/param"
)
func New() Definition {
return Definition{}
var _ config.Options = (*Definition)(nil)
func New(opts ...config.Option) *Definition {
return &Definition{
options: opts,
Params: param.New(),
}
}
type Definition struct {
options Options
param.Params
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)
}
func (d *Definition) Options() []config.Option {
return d.options
}
func (d *Definition) With(params param.Params) *Definition {
def := &Definition{
options: make([]config.Option, len(d.options)),
Params: param.Chain(params, d.Params),
}
return nil
copy(def.options, d.options)
return def
}

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
*pkg.Packages
Configure []string
}
func (b Boot) Pkg() string {
return pkg.Pkg(b.FullPkg())
}
type Config interface {
File() string
Methods() []string
SkipContext() bool
Prefix() string
Suffix() string
FullPkg() string
}
func Bootstrap(ctx context.Context, cfg Config) (string, error) {
fInfo, err := os.Stat(cfg.File())
if err != nil {
return "", fmt.Errorf("stat[%v]:%w", cfg.File(), 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",
"gitoa.ru/go-4devs/config/definition/generate/view",
"gitoa.ru/go-4devs/config/param",
"gitoa.ru/go-4devs/config/definition/generate",
"os",
"io",
"fmt",
pkgPath,
)
data := Boot{
Packages: imports,
Configure: cfg.Methods(),
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,43 @@
//go:build ignore
// +build ignore
package main
import (
{{range .Imports}}
{{- .Alias }}"{{ .Package }}"
{{end}}
)
func main() {
if err := run(os.Stdout); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(f io.Writer) error {
ctx := context.Background()
defs:=make([]config.Options,0)
{{ range .Configure }}
params{{.}} := param.New(
{{- if $.SkipContext }}view.WithSkipContext,{{ end }}
view.WithStructName("{{$.Prefix}}_{{.}}_{{$.Suffix}}"),
view.WithStructPrefix("{{$.Prefix}}"),
view.WithStructSuffix("{{$.Suffix}}"),
)
def{{.}} := definition.New().With(params{{.}})
if err := {{$.Pkg}}.{{.}}(ctx, def{{.}}); err != nil {
return err
}
defs = append(defs,def{{.}})
{{ end }}
if gerr := generate.Run(ctx,"{{.FullPkg}}",f, defs...);gerr != nil {
return gerr
}
return nil
}

View File

@@ -0,0 +1,68 @@
package command
import (
"context"
"fmt"
"os"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/generate"
"gitoa.ru/go-4devs/config/provider/chain"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/output"
)
const Name = "config:generate"
func Handle(ctx context.Context, in config.Provider, out output.Output, next console.Action) error {
var name string
value, err := in.Value(ctx, generate.OptionFile)
if err == nil {
name = value.String()
}
if name == "" {
name = os.Getenv("GOFILE")
}
parser, err := generate.Parse(ctx, name)
if err != nil {
return fmt.Errorf("parse:%w", err)
}
mem, merr := generate.NewMemoryProvider(name,
generate.WithOutName(parser.OutName()),
generate.WithFullPkg(parser.FullPkg()),
generate.WithMethods(parser.Methods()...),
)
if merr != nil {
return fmt.Errorf("mem provider:%w", merr)
}
return next(ctx, chain.New(in, mem), out)
}
func Execute(ctx context.Context, in config.Provider, _ output.Output) error {
cfg := generate.NewConfigure(ctx, in)
if err := generate.Generate(ctx, cfg); err != nil {
return fmt.Errorf("%w", err)
}
return nil
}
func Command() *console.Command {
return &console.Command{
Description: "",
Help: "",
Version: "v0.0.1",
Hidden: false,
Prepare: nil,
Handle: Handle,
Name: Name,
Execute: Execute,
Configure: generate.Configure,
}
}

View File

@@ -0,0 +1,134 @@
package generate
import (
"context"
"fmt"
"strconv"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/provider/memory"
)
const (
OptionFile = "file"
optionPrefix = "prefix"
optionSuffix = "suffix"
optionSkipContext = "skip-context"
optionBuildTags = "build-tags"
optionOutName = "out-name"
optionMethod = "method"
optionFullPkg = "full-pkg"
)
func WithPrefix(in string) func(*memory.Map) error {
return func(m *memory.Map) error {
err := m.AppendOption(in, optionPrefix)
if err != nil {
return fmt.Errorf("append %v:%w", optionPrefix, err)
}
return nil
}
}
func WithSuffix(in string) func(*memory.Map) error {
return func(m *memory.Map) error {
err := m.AppendOption(in, optionSuffix)
if err != nil {
return fmt.Errorf("append %v:%w", optionSuffix, err)
}
return nil
}
}
func WithSkipContext(in bool) func(*memory.Map) error {
return func(m *memory.Map) error {
err := m.AppendOption(strconv.FormatBool(in), optionSkipContext)
if err != nil {
return fmt.Errorf("append %v:%w", optionSkipContext, err)
}
return nil
}
}
func WithBuildTags(in string) func(*memory.Map) error {
return func(m *memory.Map) error {
err := m.AppendOption(in, optionBuildTags)
if err != nil {
return fmt.Errorf("append %v:%w", optionBuildTags, err)
}
return nil
}
}
func WithOutName(in string) func(*memory.Map) error {
return func(m *memory.Map) error {
err := m.AppendOption(in, optionOutName)
if err != nil {
return fmt.Errorf("append %v:%w", optionOutName, err)
}
return nil
}
}
func WithMethods(methods ...string) func(*memory.Map) error {
return func(m *memory.Map) error {
for _, method := range methods {
err := m.AppendOption(method, optionMethod)
if err != nil {
return fmt.Errorf("append %v:%w", optionMethod, err)
}
}
return nil
}
}
func WithFullPkg(in string) func(*memory.Map) error {
return func(m *memory.Map) error {
err := m.AppendOption(in, optionFullPkg)
if err != nil {
return fmt.Errorf("append %v:%w", optionFullPkg, err)
}
return nil
}
}
func NewMemoryProvider(file string, opts ...func(*memory.Map) error) (*memory.Map, error) {
cfg := new(memory.Map)
err := cfg.AppendOption(file, OptionFile)
if err != nil {
return nil, fmt.Errorf("append %v:%w", OptionFile, err)
}
for idx, opt := range opts {
err = opt(cfg)
if err != nil {
return nil, fmt.Errorf("opt[%d]:%w", idx, err)
}
}
return cfg, nil
}
func Configure(_ context.Context, def config.Definition) error {
def.Add(
option.String(OptionFile, "set file", option.Required),
option.String(optionPrefix, "struct prefix"),
option.String(optionSuffix, "struct suffix", option.Default("Config")),
option.Bool(optionSkipContext, "skip contect to method"),
option.String(optionBuildTags, "add build tags"),
option.String(optionOutName, "set out name"),
option.String(optionMethod, "set method", option.Slice),
option.String(optionFullPkg, "set full pkg"),
)
return nil
}

View File

@@ -0,0 +1,231 @@
package generate
import (
"context"
"errors"
"fmt"
"log"
"gitoa.ru/go-4devs/config"
)
func NewConfigure(ctx context.Context, prov config.Provider) ConfigureConfig {
return ConfigureConfig{
Provider: prov,
ctx: ctx,
handle: func(err error) {
if !errors.Is(err, config.ErrNotFound) {
log.Print(err)
}
},
}
}
type ConfigureConfig struct {
config.Provider
ctx context.Context //nolint:containedctx
handle func(err error)
}
func (i ConfigureConfig) ReadBuildTags() (string, error) {
val, verr := i.Value(i.ctx, optionBuildTags)
if verr != nil {
return "", fmt.Errorf("get %v:%w", optionBuildTags, verr)
}
data, err := val.ParseString()
if err != nil {
return "", fmt.Errorf("parse %v:%w", optionBuildTags, err)
}
return data, nil
}
func (i ConfigureConfig) BuildTags() string {
data, err := i.ReadBuildTags()
if err != nil {
i.handle(err)
return ""
}
return data
}
func (i ConfigureConfig) ReadOutName() (string, error) {
val, err := i.Value(i.ctx, optionOutName)
if err != nil {
return "", fmt.Errorf("get %v:%w", optionOutName, err)
}
data, derr := val.ParseString()
if derr != nil {
return "", fmt.Errorf("parse %v:%w", optionOutName, derr)
}
return data, nil
}
func (i ConfigureConfig) OutName() string {
data, err := i.ReadOutName()
if err != nil {
i.handle(err)
return ""
}
return data
}
func (i ConfigureConfig) ReadFile() (string, error) {
val, err := i.Value(i.ctx, OptionFile)
if err != nil {
return "", fmt.Errorf("get %v:%w", OptionFile, err)
}
data, derr := val.ParseString()
if derr != nil {
return "", fmt.Errorf("parse %v:%w", OptionFile, derr)
}
return data, nil
}
func (i ConfigureConfig) File() string {
data, err := i.ReadFile()
if err != nil {
i.handle(err)
return ""
}
return data
}
func (i ConfigureConfig) ReadMethods() ([]string, error) {
val, err := i.Value(i.ctx, optionMethod)
if err != nil {
return nil, fmt.Errorf("get %v:%w", optionMethod, err)
}
var data []string
perr := val.Unmarshal(&data)
if perr != nil {
return nil, fmt.Errorf("unmarshal %v:%w", optionMethod, perr)
}
return data, nil
}
func (i ConfigureConfig) Methods() []string {
data, err := i.ReadMethods()
if err != nil {
i.handle(err)
return nil
}
return data
}
func (i ConfigureConfig) ReadSkipContext() (bool, error) {
val, err := i.Value(i.ctx, optionSkipContext)
if err != nil {
return false, fmt.Errorf("get %v:%w", optionSkipContext, err)
}
data, derr := val.ParseBool()
if derr != nil {
return false, fmt.Errorf("parse %v:%w", optionSkipContext, derr)
}
return data, nil
}
func (i ConfigureConfig) SkipContext() bool {
data, err := i.ReadSkipContext()
if err != nil {
i.handle(err)
return false
}
return data
}
func (i ConfigureConfig) ReadPrefix() (string, error) {
val, err := i.Value(i.ctx, optionPrefix)
if err != nil {
return "", fmt.Errorf("get %v: %w", optionPrefix, err)
}
data, derr := val.ParseString()
if derr != nil {
return "", fmt.Errorf("parse %v:%w", optionPrefix, derr)
}
return data, nil
}
func (i ConfigureConfig) Prefix() string {
val, err := i.ReadPrefix()
if err != nil {
i.handle(err)
return ""
}
return val
}
func (i ConfigureConfig) ReadSuffix() (string, error) {
val, err := i.Value(i.ctx, optionSuffix)
if err != nil {
return "", fmt.Errorf("get %v:%w", optionSuffix, err)
}
data, derr := val.ParseString()
if derr != nil {
return "", fmt.Errorf("parse %v:%w", optionSuffix, derr)
}
return data, nil
}
func (i ConfigureConfig) Suffix() string {
data, err := i.ReadSuffix()
if err != nil {
i.handle(err)
return ""
}
return data
}
func (i ConfigureConfig) ReadFullPkg() (string, error) {
val, err := i.Value(i.ctx, optionFullPkg)
if err != nil {
return "", fmt.Errorf("get %v:%w", optionFullPkg, err)
}
data, derr := val.ParseString()
if derr != nil {
return "", fmt.Errorf("parse %v:%w", optionFullPkg, derr)
}
return data, nil
}
func (i ConfigureConfig) FullPkg() string {
data, err := i.ReadFullPkg()
if err != nil {
i.handle(err)
return ""
}
return data
}

View File

@@ -0,0 +1,39 @@
package example
import (
"context"
configs "gitoa.ru/go-4devs/config"
"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"
)
//go:generate go run ../../../cmd/config/main.go config:generate
type Level string
func (l *Level) UnmarshalText(in []byte) error {
data := string(in)
*l = Level(data)
return nil
}
func Example(_ context.Context, def configs.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"),
).With(view.WithContext),
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,153 @@
// Code generated gitoa.ru/go-4devs/config DO NOT EDIT.
package example
import (
"context"
"fmt"
"gitoa.ru/go-4devs/config"
)
func WithExampleConfigHandle(fn func(context.Context, error)) func(*ExampleConfig) {
return func(ci *ExampleConfig) {
ci.handle = fn
}
}
func NewExampleConfig(ctx context.Context, prov config.Provider, opts ...func(*ExampleConfig)) ExampleConfig {
i := ExampleConfig{
Provider: prov,
handle: func(_ context.Context, err error) {
fmt.Printf("ExampleConfig:%v", err)
},
ctx: ctx,
}
for _, opt := range opts {
opt(&i)
}
return i
}
type ExampleConfig struct {
config.Provider
handle func(context.Context, error)
ctx context.Context
}
// readTest test string.
func (i ExampleConfig) 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 ExampleConfig) ReadTest() (string, error) {
return i.readTest(i.ctx)
}
// Test test string.
func (i ExampleConfig) Test() string {
val, err := i.readTest(i.ctx)
if err != nil {
i.handle(i.ctx, err)
}
return val
}
type UserConfig struct {
ExampleConfig
}
// User configure user.
func (i ExampleConfig) User() UserConfig {
return UserConfig{i}
}
type LogConfig struct {
ExampleConfig
}
// Log configure logger.
func (i ExampleConfig) Log() LogConfig {
return LogConfig{i}
}
// readLevel log level.
func (i LogConfig) 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("parse [%v]:%w", []string{"log", "level"}, perr)
}
return v, v.UnmarshalText([]byte(pval))
}
// ReadLevel log level.
func (i LogConfig) ReadLevel(ctx context.Context) (Level, error) {
return i.readLevel(ctx)
}
// Level log level.
func (i LogConfig) Level(ctx context.Context) Level {
val, err := i.readLevel(ctx)
if err != nil {
i.handle(ctx, err)
}
return val
}
type LogServiceConfig struct {
LogConfig
service string
}
// Service servise logger.
func (i LogConfig) Service(key string) LogServiceConfig {
return LogServiceConfig{i, key}
}
// readLevel log level.
func (i LogServiceConfig) 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("parse [%v]:%w", []string{"log", i.service, "level"}, perr)
}
return v, v.UnmarshalText([]byte(pval))
}
// ReadLevel log level.
func (i LogServiceConfig) ReadLevel(ctx context.Context) (Level, error) {
return i.readLevel(ctx)
}
// Level log level.
func (i LogServiceConfig) Level(ctx context.Context) Level {
val, err := i.readLevel(ctx)
if err != nil {
i.handle(ctx, err)
}
return val
}

View File

@@ -0,0 +1,70 @@
package generate
import (
"context"
"fmt"
"go/format"
"os"
"os/exec"
"path/filepath"
"gitoa.ru/go-4devs/config/definition/generate/bootstrap"
)
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,84 @@
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"
)
//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, fullPkg string, w io.Writer, defs ...config.Options) error {
data := Data{
Packages: pkg.NewImports(fullPkg).
Adds("fmt", "context", "gitoa.ru/go-4devs/config"),
}
var buff bytes.Buffer
for _, in := range defs {
vi := view.NewViews(in)
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 {
*pkg.Packages
}
func (f Data) StructName(name string) string {
return FuncName(name)
}
func (f Data) FuncName(in string) string {
return FuncName(in)
}
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()
}

View File

@@ -0,0 +1,88 @@
package generate_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/view"
"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/config/test/require"
)
type LogLevel string
func (l *LogLevel) UnmarshalText(in []byte) error {
data := string(in)
*l = LogLevel(data)
return nil
}
func Configure(_ 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", LogLevel("")),
proto.New("service", "servise logger", option.New("level", "log level", LogLevel(""))),
),
)
return nil
}
func TestGenerate_Bootstrap(t *testing.T) {
t.SkipNow()
t.Parallel()
ctx := context.Background()
options := definition.New()
err := Configure(ctx, options)
require.NoError(t, err)
cfg, _ := generate.NewMemoryProvider("generate_test.go",
generate.WithMethods("Config"),
generate.WithFullPkg("gitoa.ru/go-4devs/config/definition/generate_test"),
)
path, err := bootstrap.Bootstrap(ctx, generate.NewConfigure(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()
err := Configure(ctx, options)
require.NoError(t, err)
cfg, _ := generate.NewMemoryProvider("generate_test.go",
generate.WithMethods("Config"),
generate.WithFullPkg("gitoa.ru/go-4devs/config/definition/generate_test"),
)
err = generate.Generate(ctx, generate.NewConfigure(ctx, cfg))
require.NoError(t, err)
t.FailNow()
}

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,152 @@
package generate
import (
"context"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"reflect"
"slices"
"strings"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/generate/pkg"
)
const NameSuffix = "_config.go"
func Parse(ctx context.Context, name string) (Parser, error) {
var parse Parser
parse.file = name
stats, err := os.Stat(name)
if err != nil {
return parse, fmt.Errorf("stats:%w", err)
}
parse.fullPkg, err = pkg.ByPath(ctx, name, stats.IsDir())
if err != nil {
return parse, fmt.Errorf("get pkg:%w", err)
}
parse.methods, err = NewParseMethods(
name,
[]reflect.Type{
reflect.TypeFor[context.Context](),
reflect.TypeFor[config.Definition](),
},
[]reflect.Type{reflect.TypeFor[error]()},
)
if err != nil {
return parse, fmt.Errorf("parse methods:%w", err)
}
return parse, nil
}
func NewParseMethods(file string, params []reflect.Type, results []reflect.Type) ([]string, error) {
pfile, err := parser.ParseFile(token.NewFileSet(), file, nil, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("parse:%w", err)
}
resultAlias := importAlias(pfile, results)
paramsAlias := importAlias(pfile, params)
var methods []string
ast.Inspect(pfile, func(anode ast.Node) bool {
if fn, ok := anode.(*ast.FuncDecl); ok &&
fn.Recv == nil &&
fn.Type != nil &&
(fn.Type.Params != nil && len(params) == len(fn.Type.Params.List) || len(params) == 0 && fn.Type.Params == nil) &&
(fn.Type.Results != nil && len(results) == len(fn.Type.Results.List) || len(results) == 0 && fn.Type.Results == nil) {
if hasFields(fn.Type.Params, params, paramsAlias) && hasFields(fn.Type.Results, results, resultAlias) {
methods = append(methods, fn.Name.String())
}
}
return true
})
return methods, nil
}
func importAlias(file *ast.File, params []reflect.Type) map[int][]string {
paramsAlias := make(map[int][]string, len(params))
for idx := range params {
name := params[idx].Name()
if pkgPath := params[idx].PkgPath(); pkgPath != "" {
name = pkg.Pkg(pkgPath)
}
paramsAlias[idx] = append(paramsAlias[idx], name)
}
ast.Inspect(file, func(anode ast.Node) bool {
if exp, ok := anode.(*ast.ImportSpec); ok {
pathName := strings.Trim(exp.Path.Value, "\"")
pname := pkg.Pkg(pathName)
if exp.Name != nil {
pname = exp.Name.String()
}
for idx, param := range params {
if pathName == param.PkgPath() {
paramsAlias[idx] = append(paramsAlias[idx], pname)
}
}
}
return true
})
return paramsAlias
}
func hasFields(fields *ast.FieldList, params []reflect.Type, alias map[int][]string) bool {
for idx, one := range fields.List {
iparam := params[idx]
if ident, iok := one.Type.(*ast.Ident); iok && iparam.String() == ident.String() {
return true
}
selector, sok := one.Type.(*ast.SelectorExpr)
if !sok {
return false
}
if iparam.Name() != selector.Sel.String() {
return false
}
salias, saok := selector.X.(*ast.Ident)
if iparam.PkgPath() != "" && saok && !slices.Contains(alias[idx], salias.String()) {
return false
}
}
return true
}
type Parser struct {
file string
fullPkg string
methods []string
}
func (p Parser) OutName() string {
return strings.ReplaceAll(p.file, ".go", NameSuffix)
}
func (p Parser) FullPkg() string {
return p.fullPkg
}
func (p Parser) Methods() []string {
return p.methods
}

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) *Packages {
imp := Packages{
data: make(map[string]string),
pkg: pkg,
}
return &imp
}
type Imports struct {
type Packages struct {
data map[string]string
pkg string
}
func (i Imports) Imports() []Import {
func (i *Packages) Imports() []Import {
imports := make([]Import, 0, len(i.data))
for name, alias := range i.data {
imports = append(imports, Import{
@@ -28,7 +32,7 @@ func (i Imports) Imports() []Import {
return imports
}
func (i *Imports) Short(fullType string) (string, error) {
func (i *Packages) Short(fullType string) (string, error) {
idx := strings.LastIndexByte(fullType, '.')
if idx == -1 {
return "", fmt.Errorf("%w: expect package.Type", ErrWrongFormat)
@@ -41,28 +45,41 @@ func (i *Imports) Short(fullType string) (string, error) {
return "", fmt.Errorf("%w alias for pkg %v", ErrNotFound, fullType[:idx])
}
func (i *Imports) AddType(fullType string) (string, error) {
func (i *Packages) 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 *Packages) Adds(pkgs ...string) *Packages {
for _, pkg := range pkgs {
i.Add(pkg)
}
return i
}
func (i *Imports) Add(pkg string) Import {
func (i *Packages) 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 {
@@ -83,7 +100,31 @@ func (i *Imports) Add(pkg string) Import {
}
}
func (i *Packages) Pkg() string {
return Pkg(i.pkg)
}
type Import struct {
Alias string
Package string
}
func (i Import) Pkg() string {
return Pkg(i.Package)
}
func (i Import) String() string {
if i.Alias == i.Pkg() {
return fmt.Sprintf("%q", i.Package)
}
return fmt.Sprintf("%s %q", i.Alias, i.Package)
}
func Pkg(fullPkg string) string {
if idx := strings.LastIndex(fullPkg, "/"); idx != -1 {
return fullPkg[idx+1:]
}
return fullPkg
}

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.SplitSeq(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.StructName())
}
func (d ViewData) FuncName() string {
return d.Rendering.FuncName(d.View.FuncName())
}
func (d ViewData) ParentStruct() string {
name := d.View.ParentStruct()
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(d.View.Keys(), 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("parse [%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("parse [%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("parse [%v]:%w", []string{ {{- .Keys "i" -}} }, perr)
}
return {{.Value}}, {{.Value}}.UnmarshalText([]byte(pval))
{{- end -}}

View File

@@ -0,0 +1,27 @@
func With{{.StructName}}Handle(fn func(context.Context, error)) func(*{{.StructName}}) {
return func(ci *{{.StructName}}) {
ci.handle = fn
}
}
func New{{.StructName}}({{if or .SkipContext .ClildSkipContext }} ctx context.Context,{{end}}prov config.Provider, opts ...func(*{{.StructName}})) {{.StructName}} {
i := {{.StructName}}{
Provider: prov,
handle: func(_ context.Context, err error) {
fmt.Printf("{{.StructName}}:%v",err)
},
{{if or .SkipContext .ClildSkipContext }} ctx: ctx, {{end}}
}
for _, opt := range opts {
opt(&i)
}
return i
}
type {{.StructName}} struct {
config.Provider
handle func(context.Context, error)
{{if or .SkipContext .ClildSkipContext}}ctx context.Context {{end}}
}

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
type {{.StructName}} struct {
{{.ParentStruct}}
{{ .Name }} string
}
// {{.FuncName}} {{.Description}}.
func (i {{.ParentStruct}}) {{.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.Pointer {
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("parse [%v]:%w",[]string{"flag_value"}, perr)
}
return v, v.Set(pval)`
viewData := render.NewViewData(nil, view.NewView(option.New("flag_value", "flag desc", flagValue(0))))
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))))
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))))
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(""))))
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("parse [%v]:%w", []string{"tvalue"}, perr)
}
return v, v.UnmarshalText([]byte(pval))`
viewData := render.NewViewData(nil, view.NewView(option.New("tvalue", "unmarshal text desc", textData(""))))
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

@@ -1,43 +0,0 @@
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
}
}
func New{{.StructName}}(prov config.Provider, opts ...func(*{{.StructName}})) {{.StructName}} {
i := {{.StructName}}{
Provider: prov,
log: func(_ context.Context, format string, args ...any) {
fmt.Printf(format, args...)
},
}
for _, opt := range opts {
opt(&i)
}
return i
}
type {{.StructName}} struct {
config.Provider
log func(context.Context, string, ...any)
}
`
)

View File

@@ -0,0 +1,8 @@
// Code generated gitoa.ru/go-4devs/config DO NOT EDIT.
package {{.Pkg}}
import (
{{range .Imports}}
{{- . }}
{{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,250 @@
package view
import (
"fmt"
"reflect"
"strings"
"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
viewParamStructName
viewParamStructPrefix
viewParamStructSuffix
)
func WithStructName(name string) param.Option {
return func(p param.Params) param.Params {
return param.With(p, viewParamStructName, name)
}
}
func WithStructPrefix(prefix string) param.Option {
return func(p param.Params) param.Params {
return param.With(p, viewParamStructPrefix, prefix)
}
}
func WithStructSuffix(suffix string) param.Option {
return func(p param.Params) param.Params {
return param.With(p, viewParamStructSuffix, suffix)
}
}
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(in *View) Option {
return func(v *View) {
v.parent = in
}
}
func NewViews(option config.Options, opts ...Option) View {
view := newView(option, opts...)
for _, op := range option.Options() {
view.children = append(view.children, NewView(op, WithParent(&view)))
}
return view
}
func newView(params param.Params, opts ...Option) View {
vi := View{
Params: params,
parent: nil,
children: nil,
}
for _, opt := range opts {
opt(&vi)
}
return vi
}
func NewView(opt config.Option, opts ...Option) View {
vi := newView(opt, opts...)
if data, ok := opt.(config.Group); ok {
for _, chi := range data.Options() {
vi.children = append(vi.children, NewView(chi, WithParent(&vi)))
}
}
return vi
}
type View struct {
param.Params
children []View
parent *View
}
func (v View) Types() []any {
types := make([]any, 0)
if v.Type() != "" {
types = append(types, v.Type())
}
for _, child := range v.children {
types = append(types, child.Types()...)
}
return types
}
func (v View) Kind() reflect.Type {
return reflect.TypeOf(v.Params)
}
func (v View) Views() []View {
return v.children
}
func (v View) Param(key any) string {
data, has := v.Params.Param(key)
if has {
return fmt.Sprintf("%v", data)
}
if v.parent != nil {
return v.parent.Param(key)
}
return ""
}
func (v View) ClildSkipContext() bool {
for _, child := range v.children {
if child.SkipContext() {
return true
}
}
return false
}
func (v View) SkipContext() bool {
if IsSkipContext(v.Params) {
return true
}
if v.parent != nil {
return v.parent.SkipContext()
}
return false
}
func (v View) Name() string {
if data, ok := v.Params.(interface{ Name() string }); ok {
return data.Name()
}
return ""
}
func (v View) Keys() []string {
keys := make([]string, 0, 1)
if v.parent != nil {
keys = append(keys, v.parent.Keys()...)
}
if name := v.Name(); name != "" {
keys = append(keys, name)
}
return keys
}
func (v View) Type() any {
return param.Type(v.Params)
}
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) StructName() string {
name, ok := param.String(v.Params, viewParamStructName)
if ok {
return name
}
keys := make([]string, 0, len(v.Keys())+2) //nolint:mnd
prefix := v.Param(viewParamStructPrefix)
if prefix != "" {
keys = append(keys, prefix)
}
keys = append(keys, v.Keys()...)
suffix := v.Param(viewParamStructSuffix)
if suffix != "" {
keys = append(keys, suffix)
}
return strings.Join(keys, "_")
}
func (v View) ParentStruct() string {
if v.parent == nil {
return ""
}
return v.parent.StructName()
}
func (v View) Description() string {
return param.Description(v.Params)
}
func (v View) Default() any {
data, ok := param.Default(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=

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

@@ -1,27 +1,44 @@
package group
import (
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config"
"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(param.WithDescription(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
}
func (g *Group) Add(opts ...config.Option) {
g.opts = append(g.opts, opts...)
}
func (g *Group) With(opts ...param.Option) *Group {
group := New(g.Name(), "")
group.Params = param.Chain(g.Params, param.New(opts...))
return group
}

View File

@@ -0,0 +1,29 @@
package group_test
import (
"testing"
"gitoa.ru/go-4devs/config/definition/group"
"gitoa.ru/go-4devs/config/param"
"gitoa.ru/go-4devs/config/test/require"
)
func TestGroupAdd(t *testing.T) {
t.Parallel()
var gr group.Group
gr.Add(group.New("test", "test"))
require.Truef(t, len(gr.Options()) == 1, "len(%v) != 1", len(gr.Options()))
}
func TestGroupWith(t *testing.T) {
t.Parallel()
const descrition = "group description"
gr := group.New("test", "test desc")
gr = gr.With(param.WithDescription(descrition))
require.Equal(t, descrition, param.Description(gr))
}

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, param.WithDescription(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...)
}

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

@@ -0,0 +1,118 @@
package option
import (
"gitoa.ru/go-4devs/config/param"
)
type key int
const (
paramHidden key = iota + 1
paramRequired
paramSlice
paramBool
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 param.WithPostition(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 param.WithDefault(in)
}
// Deprecated: use param.WithDescription.
func Description(in string) param.Option {
return param.WithDescription(in)
}
func HasDefaut(fn param.Params) bool {
_, ok := param.Default(fn)
return ok
}
// Deprecated: use param.Position.
func DataPosition(fn param.Params) (uint64, bool) {
pos := param.Position(fn)
return pos, pos != 0
}
// Deprecated: use param.Default.
func DataDefaut(fn param.Params) (any, bool) {
return param.Default(fn)
}
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
}
// Deprecated: use param.Description.
func DataDescription(fn param.Params) string {
return param.Description(fn)
}

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,32 @@
package proto
import (
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config"
"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(param.WithDescription(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=

96
factory.go Normal file
View File

@@ -0,0 +1,96 @@
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.Create(ctx, prov)
},
mu: sync.Mutex{},
done: 0,
provider: nil,
name: fn.Name(),
}
}
type WrapProvider struct {
mu sync.Mutex
done uint32
provider Provider
factory func(ctx context.Context) (Provider, error)
name string
}
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 {
return p.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
}

4
go.mod
View File

@@ -1,3 +1,5 @@
module gitoa.ru/go-4devs/config
go 1.21
go 1.24.0
require gitoa.ru/go-4devs/console v0.2.0

2
go.sum
View File

@@ -0,0 +1,2 @@
gitoa.ru/go-4devs/console v0.2.0 h1:6lsbArs99GA8vGdnwNDThZNKjFNctNtTlSCUjhgwIpU=
gitoa.ru/go-4devs/console v0.2.0/go.mod h1:xi4Svw7T+lylckAQiJQS/2qwDwF4YbIanlhcbQrBAiI=

125
key/map.go Normal file
View File

@@ -0,0 +1,125 @@
package key
import (
"strings"
)
const (
prefixByPath = "byPath"
wrongIDx = -1
)
func newMap() *Map {
return &Map{
idx: wrongIDx,
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) {
if len(path) == 0 {
return m, m.isValid()
}
for name := range m.children {
if after, ok := strings.CutPrefix(path, name); ok {
data := m.children[name]
if len(after) == 0 || len(after) == len(sep) {
return data, data.isValid()
}
return data.byPath(after[len(sep):], sep)
}
}
if m.wild == nil {
return m, m.isValid()
}
if idx := strings.Index(path, sep); idx != -1 {
return m.wild.byPath(path[idx+1:], sep)
}
return m, m.isValid()
}
func (m *Map) find(path []string) (*Map, bool) {
name := path[0]
last := len(path) == 1
if !last {
path = path[1:]
}
data, ok := m.children[name]
if !ok && m.wild != nil {
return m.wild.find(path)
}
if !ok {
return data, false
}
if last {
return data, data.isValid()
}
return data.find(path)
}
func (m *Map) isValid() bool {
return m.idx != wrongIDx
}

101
key/map_test.go Normal file
View File

@@ -0,0 +1,101 @@
package key_test
import (
"testing"
"gitoa.ru/go-4devs/config/key"
)
func TestMap_ByPath(t *testing.T) {
t.Parallel()
const (
expID int = 1
othID int = 0
newID int = 42
grpID int = 27
)
data := key.Map{}
data.Add(expID, []string{"test", "data", "three"})
data.Add(othID, []string{"test", "other"})
data.Add(newID, []string{"new", "{data}", "test"})
data.Add(grpID, []string{"new", "group"})
idx, ok := data.Index(key.ByPath("test-other", "-"))
if !ok {
t.Error("key not found")
}
if idx != othID {
t.Errorf("idx exp:%v got:%v", othID, idx)
}
if nidx, nok := data.Index(key.ByPath("new-service-test", "-")); !nok && nidx != newID {
t.Errorf("idx exp:%v got:%v", newID, nidx)
}
if gidx, gok := data.Index(key.ByPath("new-group", "-")); !gok && gidx != grpID {
t.Errorf("idx %v exp:%v got:%v", gok, grpID, gidx)
}
}
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
grpID int = 27
newID int = 42
)
data := key.Map{}
data.Add(expID, []string{"test", "{data}", "id"})
data.Add(grpID, []string{"test", "group"})
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)
}
gidx, gok := data.Index([]string{"test", "group"})
if !gok {
t.Error("key not found")
}
if gidx != grpID {
t.Errorf("idx exp:%v got:%v", grpID, idx)
}
}

21
key/wild.go Normal file
View File

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

16
key/wild_test.go Normal file
View File

@@ -0,0 +1,16 @@
package key_test
import (
"testing"
"gitoa.ru/go-4devs/config/key"
"gitoa.ru/go-4devs/config/test/require"
)
func TestWild(t *testing.T) {
t.Parallel()
require.True(t, key.IsWild(key.Wild("test")))
require.True(t, !key.IsWild("test"))
require.True(t, key.IsWild("test", key.Wild("test"), "key"))
}

45
param/helper.go Normal file
View File

@@ -0,0 +1,45 @@
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 Rune(fn Params, key any) (rune, bool) {
val, ok := fn.Param(key)
if !ok {
return '0', false
}
data, dok := val.(rune)
return data, dok
}
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
}

69
param/keys.go Normal file
View File

@@ -0,0 +1,69 @@
package param
type key int
const (
paramTimeFormat key = iota + 1
paramType
paramDescription
paramDefault
paramPosition
)
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
}
func WithDescription(in string) Option {
return func(p Params) Params {
return With(p, paramDescription, in)
}
}
func Description(fn Params) string {
data, _ := String(fn, paramDescription)
return data
}
func WithDefault(in any) Option {
return func(p Params) Params {
return With(p, paramDefault, in)
}
}
func Default(p Params) (any, bool) {
data, ok := p.Param(paramDefault)
return data, ok
}
func WithPostition(in uint64) Option {
return func(p Params) Params {
return With(p, paramPosition, in)
}
}
func Position(in Params) uint64 {
pos, _ := Uint64(paramPosition, in)
return pos
}

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

123
processor/csv/processor.go Normal file
View File

@@ -0,0 +1,123 @@
package csv
import (
"bytes"
"context"
"encoding/csv"
"fmt"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/param"
"gitoa.ru/go-4devs/config/value"
)
type pkey int
const (
paramDelimiter pkey = iota + 1
paramParse
)
const defaultDelimiter = ','
func WithDelimiter(in rune) param.Option {
return func(p param.Params) param.Params {
return param.With(p, paramDelimiter, in)
}
}
func WithInt(p param.Params) param.Params {
return param.With(p, paramParse, func(data []string) (config.Value, error) {
return value.ParseSlice(data, value.Atoi)
})
}
func WithInt64(p param.Params) param.Params {
return param.With(p, paramParse, func(data []string) (config.Value, error) {
return value.ParseSlice(data, value.ParseInt64)
})
}
func WithFloat(p param.Params) param.Params {
return param.With(p, paramParse, func(data []string) (config.Value, error) {
return value.ParseSlice(data, value.ParseFloat)
})
}
func WithBool(p param.Params) param.Params {
return param.With(p, paramParse, func(data []string) (config.Value, error) {
return value.ParseSlice(data, value.ParseBool)
})
}
func WithUint(p param.Params) param.Params {
return param.With(p, paramParse, func(data []string) (config.Value, error) {
return value.ParseSlice(data, value.ParseUint)
})
}
func WithUint64(p param.Params) param.Params {
return param.With(p, paramParse, func(data []string) (config.Value, error) {
return value.ParseSlice(data, value.ParseUint64)
})
}
func WithDuration(p param.Params) param.Params {
return param.With(p, paramParse, func(data []string) (config.Value, error) {
return value.ParseSlice(data, value.ParseDuration)
})
}
func WithTime(p param.Params) param.Params {
return param.With(p, paramParse, func(data []string) (config.Value, error) {
return value.ParseSlice(data, value.ParseTime)
})
}
func WithParse(fn func(data []string) config.Value) param.Option {
return func(p param.Params) param.Params {
return param.With(p, paramParse, fn)
}
}
func Csv(_ context.Context, in config.Value, opts ...param.Option) (config.Value, error) {
sval, serr := in.ParseString()
if serr != nil {
return in, nil //nolint:nilerr
}
params := param.New(opts...)
reader := csv.NewReader(bytes.NewBufferString(sval))
reader.Comma = getDelimiter(params)
data, rerr := reader.Read()
if rerr != nil {
return nil, fmt.Errorf("read csv:%w", rerr)
}
return csvValue(params, data)
}
func csvValue(params param.Params, data []string) (config.Value, error) {
fn, ok := params.Param(paramParse)
if !ok {
return stringsValue(data)
}
parse, _ := fn.(func([]string) (config.Value, error))
return parse(data)
}
func stringsValue(data []string) (config.Value, error) {
return value.New(data), nil
}
func getDelimiter(params param.Params) rune {
if name, ok := param.Rune(params, paramDelimiter); ok {
return name
}
return defaultDelimiter
}

View File

@@ -0,0 +1,54 @@
package csv_test
import (
"context"
"testing"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/processor/csv"
"gitoa.ru/go-4devs/config/test/require"
"gitoa.ru/go-4devs/config/value"
)
func TestCsv(t *testing.T) {
t.Parallel()
ctx := context.Background()
val := value.String("test2,test3,other")
data, derr := csv.Csv(ctx, val)
require.NoError(t, derr)
var resString []string
require.NoError(t, data.Unmarshal(&resString))
require.Equal(t, []string{"test2", "test3", "other"}, resString)
}
func TestCsv_int(t *testing.T) {
t.Parallel()
ctx := context.Background()
val := value.String("42,0,1")
data, derr := csv.Csv(ctx, val, csv.WithInt)
require.NoError(t, derr)
var resInt []int
require.NoError(t, data.Unmarshal(&resInt))
require.Equal(t, []int{42, 0, 1}, resInt)
}
func TestCsv_invalidValue(t *testing.T) {
t.Parallel()
ctx := context.Background()
val := value.String("42,0.1,1")
data, derr := csv.Csv(ctx, val, csv.WithInt)
require.ErrorIs(t, derr, config.ErrInvalidValue)
require.Equal(t, nil, data)
}

27
processor/env/processor.go vendored Normal file
View File

@@ -0,0 +1,27 @@
package env
import (
"context"
"fmt"
"os"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/param"
"gitoa.ru/go-4devs/config/value"
)
var _ config.ProcessFunc = Env
func Env(_ context.Context, in config.Value, _ ...param.Option) (config.Value, error) {
key, err := in.ParseString()
if err != nil {
return in, fmt.Errorf("process[env]:%w", err)
}
res, ok := os.LookupEnv(key)
if !ok {
return nil, fmt.Errorf("%w", config.ErrNotFound)
}
return value.String(res), nil
}

View File

@@ -0,0 +1,19 @@
package json //nolint:revive
import (
"context"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/param"
"gitoa.ru/go-4devs/config/value"
)
//nolint:revive
func Json(_ context.Context, in config.Value, _ ...param.Option) (config.Value, error) {
data, err := in.ParseString()
if err != nil {
return in, nil //nolint:nilerr
}
return value.JString(data), nil
}

View File

@@ -0,0 +1,48 @@
package json_test
import (
"context"
"testing"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/processor/json"
"gitoa.ru/go-4devs/config/test/require"
"gitoa.ru/go-4devs/config/value"
)
func TestJson(t *testing.T) {
t.Parallel()
ctx := context.Background()
val, err := json.Json(ctx, value.String("42"))
require.NoError(t, err)
var res int
require.NoError(t, val.Unmarshal(&res))
require.Equal(t, 42, res)
sval, serr := json.Json(ctx, value.String("\"test data\""))
require.NoError(t, serr)
var sres string
require.NoError(t, sval.Unmarshal(&sres))
require.Equal(t, "test data", sres)
slval, slerr := json.Json(ctx, value.String("[\"test\",\"test2 data\",\"test3\"]"))
require.NoError(t, slerr)
var slres []string
require.NoError(t, slval.Unmarshal(&slres))
require.Equal(t, []string{"test", "test2 data", "test3"}, slres)
}
func TestJson_invalidValue(t *testing.T) {
t.Parallel()
ctx := context.Background()
val, err := json.Json(ctx, value.New("42"))
require.NoError(t, err)
var data string
require.ErrorIs(t, val.Unmarshal(&data), config.ErrInvalidValue)
}

View File

@@ -0,0 +1,49 @@
package key
import (
"context"
"fmt"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/param"
"gitoa.ru/go-4devs/config/value"
)
type pkey int
const paramKey pkey = iota
func WithKey(in string) param.Option {
return func(p param.Params) param.Params {
return param.With(p, paramKey, in)
}
}
func Key(_ context.Context, in config.Value, opts ...param.Option) (config.Value, error) {
data := make(map[string]any, 0)
if err := in.Unmarshal(&data); err != nil {
return nil, fmt.Errorf("unmarshal:%w", err)
}
key, ok := getKey(opts...)
if !ok {
return nil, fmt.Errorf("key is %w", config.ErrRequired)
}
val, vok := data[key]
if !vok {
return nil, fmt.Errorf("value by key[%v]: %w", key, config.ErrNotFound)
}
return value.New(val), nil
}
func getKey(opts ...param.Option) (string, bool) {
params := param.New(opts...)
if name, ok := param.String(params, paramKey); ok {
return name, ok
}
return "", false
}

View File

@@ -0,0 +1,58 @@
package key_test
import (
"context"
"encoding/json"
"testing"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/processor/key"
"gitoa.ru/go-4devs/config/test/require"
"gitoa.ru/go-4devs/config/value"
)
func TestKey(t *testing.T) {
t.Parallel()
ctx := context.Background()
res, rerr := key.Key(ctx, keyData(t), key.WithKey("key"))
require.NoError(t, rerr)
require.Equal(t, "value", res.String())
}
func TestKey_required(t *testing.T) {
t.Parallel()
ctx := context.Background()
res, rerr := key.Key(ctx, keyData(t))
require.ErrorIs(t, rerr, config.ErrRequired)
require.Equal(t, res, nil)
}
func TestKey_notFound(t *testing.T) {
t.Parallel()
ctx := context.Background()
res, rerr := key.Key(ctx, keyData(t), key.WithKey("wrong"))
require.ErrorIs(t, rerr, config.ErrNotFound)
require.Equal(t, res, nil)
}
func keyData(t *testing.T) config.Value {
t.Helper()
data := map[string]string{
"key": "value",
}
jdata, err := json.Marshal(data)
require.NoError(t, err)
return value.JBytes(jdata)
}

View File

@@ -1,14 +1,15 @@
package config
import "context"
import (
"context"
"io"
"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
@@ -17,4 +18,61 @@ type WatchProvider interface {
Watch(ctx context.Context, callback WatchCallback, path ...string) error
}
type Factory func(ctx context.Context, cfg Provider) (Provider, error)
type Factory interface {
Name() string
Create(ctx context.Context, prov Provider) (Provider, error)
}
type Option interface {
Name() string
Param(key any) (any, bool)
}
type Group interface {
Option
Options
}
type Options interface {
Options() []Option
param.Params
}
type BindProvider interface {
Provider
Bind(ctx context.Context, data Variables) error
}
type DunpProvider interface {
Provider
DumpRefernce(ctx context.Context, w io.Writer, opts Options) error
}
type Providers interface {
Provider
Provider(name string) (Provider, error)
Names() []string
}
type Variables interface {
ByName(name ...string) (Variable, error)
ByParam(filter param.Has) (Variable, error)
Variables() []Variable
}
type Definition interface {
Add(opts ...Option)
}
type ProcessFunc func(ctx context.Context, in Value, opts ...param.Option) (Value, error)
func (o ProcessFunc) Process(ctx context.Context, in Value, opts ...param.Option) (Value, error) {
return o(ctx, in, opts...)
}
type Processor interface {
Process(ctx context.Context, in Value, opts ...param.Option) (Value, error)
}

419
provider/arg/dump.go Normal file
View File

@@ -0,0 +1,419 @@
package arg
import (
"bytes"
"encoding"
"encoding/json"
"errors"
"fmt"
"io"
"sort"
"strings"
"time"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/param"
)
const (
defaultSpace = 2
)
func ResolveStyle(params param.Params) ViewStyle {
var vs ViewStyle
data, ok := params.Param(paramDumpReferenceView)
if ok {
vs, _ = data.(ViewStyle)
}
return vs
}
func WithStyle(comment, info Style) param.Option {
return func(p param.Params) param.Params {
return param.With(p, paramDumpReferenceView, ViewStyle{
Comment: comment,
Info: info,
MLen: 0,
})
}
}
type ViewStyle struct {
Comment Style
Info Style
MLen int
}
func (v ViewStyle) ILen() int {
return v.Info.Len() + v.MLen
}
type Style struct {
Start string
End string
}
func (s Style) Len() int {
return len(s.End) + len(s.Start)
}
func NewDump() Dump {
return Dump{
sep: dash,
space: defaultSpace,
}
}
type Dump struct {
sep string
space int
}
func (d Dump) Reference(w io.Writer, opt config.Options) error {
views := NewViews(opt, nil)
style := ResolveStyle(opt)
style.MLen = d.keyMaxLen(views...)
if args := views.Arguments(); len(args) > 0 {
if aerr := d.writeArguments(w, style, args...); aerr != nil {
return fmt.Errorf("write arguments:%w", aerr)
}
}
if opts := views.Options(); len(opts) > 0 {
if oerr := d.writeOptions(w, style, opts...); oerr != nil {
return fmt.Errorf("write option:%w", oerr)
}
}
return nil
}
//nolint:mnd
func (d Dump) keyMaxLen(views ...View) int {
var maxLen int
for _, vi := range views {
vlen := len(vi.Name(d.sep)) + d.space
if !vi.IsArgument() {
if !vi.IsBool() {
vlen = vlen*2 + 1
}
if def := vi.Default(); def != "" {
vlen += d.space
}
vlen += 4 + d.space
}
if vlen > maxLen {
maxLen = vlen
}
}
return maxLen
}
func (d Dump) writeArguments(w io.Writer, style ViewStyle, args ...View) error {
_, err := fmt.Fprintf(w, "\n%sArguments:%s\n",
style.Comment.Start,
style.Comment.End,
)
for _, arg := range args {
alen, ierr := fmt.Fprintf(w, "%s%s%s%s",
strings.Repeat(" ", d.space),
style.Info.Start,
arg.Name(d.sep),
style.Info.End,
)
if ierr != nil {
err = errors.Join(err, ierr)
}
_, ierr = fmt.Fprint(w, strings.Repeat(" ", style.ILen()+d.space-alen))
if ierr != nil {
err = errors.Join(err, ierr)
}
_, ierr = fmt.Fprint(w, arg.Description())
if ierr != nil {
err = errors.Join(err, ierr)
}
if def := arg.Default(); def != "" {
ierr := d.writeDefault(w, style, def)
if ierr != nil {
err = errors.Join(err, ierr)
}
}
_, ierr = fmt.Fprint(w, "\n")
if ierr != nil {
err = errors.Join(err, ierr)
}
}
if err != nil {
return fmt.Errorf("%w", err)
}
return nil
}
//nolint:gocognit,gocyclo,cyclop
func (d Dump) writeOptions(w io.Writer, style ViewStyle, opts ...View) error {
_, err := fmt.Fprintf(w, "\n%sOptions:%s\n",
style.Comment.Start,
style.Comment.End,
)
for _, opt := range opts {
if opt.IsHidden() {
continue
}
var op bytes.Buffer
_, oerr := fmt.Fprintf(&op, "%s%s", strings.Repeat(" ", d.space), style.Info.Start)
if oerr != nil {
err = errors.Join(err, oerr)
}
if short := opt.Short(); short != "" {
op.WriteString("-")
op.WriteString(short)
op.WriteString(", ")
} else {
op.WriteString(" ")
}
op.WriteString("--")
op.WriteString(opt.Name(d.sep))
if !opt.IsBool() {
if !opt.IsRequired() {
op.WriteString("[")
}
op.WriteString("=")
op.WriteString(strings.ToUpper(opt.Name(d.sep)))
if !opt.IsRequired() {
op.WriteString("]")
}
}
_, oerr = fmt.Fprintf(&op, "%s", style.Info.End)
if oerr != nil {
err = errors.Join(err, oerr)
}
olen, oerr := w.Write(op.Bytes())
if oerr != nil {
err = errors.Join(err, oerr)
}
_, oerr = fmt.Fprintf(w, "%s%s",
strings.Repeat(" ", style.ILen()+d.space-olen),
opt.Description(),
)
if oerr != nil {
err = errors.Join(err, oerr)
}
if def := opt.Default(); def != "" {
oerr = d.writeDefault(w, style, def)
if oerr != nil {
err = errors.Join(err, oerr)
}
}
if opt.IsSlice() {
_, oerr = fmt.Fprintf(w, "%s (multiple values allowed)%s", style.Comment.Start, style.Comment.End)
if oerr != nil {
err = errors.Join(err, oerr)
}
}
_, oerr = fmt.Fprint(w, "\n")
if oerr != nil {
err = errors.Join(err, oerr)
}
}
if err != nil {
return fmt.Errorf("write options:%w", err)
}
return nil
}
func (d Dump) writeDefault(w io.Writer, style ViewStyle, data string) error {
_, err := fmt.Fprintf(w, " %s[default:%s]%s",
style.Comment.Start,
data,
style.Comment.End,
)
if err != nil {
return fmt.Errorf("default:%w", err)
}
return nil
}
func NewViews(opts config.Options, parent *View) Views {
views := make(Views, 0, len(opts.Options()))
for _, opt := range opts.Options() {
views = append(views, newViews(opt, parent)...)
}
return views
}
func newViews(opt config.Option, parent *View) []View {
view := NewView(opt, parent)
switch one := opt.(type) {
case config.Group:
return NewViews(one, &view)
default:
return []View{view}
}
}
type Views []View
func (v Views) Arguments() []View {
args := make([]View, 0, len(v))
for _, view := range v {
if view.IsArgument() {
args = append(args, view)
}
}
sort.Slice(args, func(i, j int) bool {
return args[i].Pos() < args[j].Pos()
})
return args
}
func (v Views) Options() []View {
opts := make([]View, 0, len(v))
for _, view := range v {
if !view.IsArgument() {
opts = append(opts, view)
}
}
sort.Slice(opts, func(i, j int) bool {
return opts[i].Pos() < opts[j].Pos()
})
return opts
}
func NewView(params config.Option, parent *View) View {
pos, ok := ParamArgument(params)
keys := make([]string, 0)
if parent != nil {
keys = append(keys, parent.Keys()...)
}
if !ok {
pos = param.Position(params)
}
if name := params.Name(); name != "" {
keys = append(keys, name)
}
return View{
pos: pos,
isArgument: ok,
keys: keys,
parent: parent,
Params: params,
}
}
type View struct {
param.Params
keys []string
pos uint64
isArgument bool
parent *View
}
func (v View) Name(delimiter string) string {
return strings.Join(v.keys, delimiter)
}
func (v View) IsArgument() bool {
return v.isArgument
}
func (v View) Keys() []string {
return v.keys
}
func (v View) Pos() uint64 {
return v.pos
}
func (v View) Default() string {
data, ok := param.Default(v.Params)
if !ok {
return ""
}
switch dt := data.(type) {
case time.Time:
return dt.Format(time.RFC3339)
case encoding.TextMarshaler:
if res, err := dt.MarshalText(); err == nil {
return string(res)
}
case json.Marshaler:
if res, err := dt.MarshalJSON(); err == nil {
return string(res)
}
}
return fmt.Sprintf("%v", data)
}
func (v View) Description() string {
return param.Description(v.Params)
}
func (v View) IsHidden() bool {
return option.IsHidden(v.Params)
}
func (v View) Short() string {
short, _ := option.ParamShort(v.Params)
return short
}
func (v View) IsRequired() bool {
return option.IsHidden(v.Params)
}
func (v View) IsBool() bool {
return option.IsBool(v.Params)
}
func (v View) IsSlice() bool {
return option.IsSlice(v.Params)
}

35
provider/arg/dump_test.go Normal file
View File

@@ -0,0 +1,35 @@
package arg_test
import (
"bytes"
"testing"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/config/test/require"
)
func TestDumpReference(t *testing.T) {
t.Parallel()
//nolint:dupword
const expect = `
Arguments:
config config [default:config.hcl]
user-name username
Options:
-l, --listen[=LISTEN] listen [default:8080]
-p, --user-password[=USER-PASSWORD] user pass
-u, --url[=URL] url (multiple values allowed)
-t, --timeout[=TIMEOUT] timeout (multiple values allowed)
--start-at[=START-AT] start at [default:2010-01-02T15:04:05Z]
--end-after[=END-AFTER] after (multiple values allowed)
--end-{service}-after[=END-{SERVICE}-AFTER] after
`
dump := arg.NewDump()
var buff bytes.Buffer
require.NoError(t, dump.Reference(&buff, testOptions(t)))
require.Equal(t, expect, buff.String())
}

View File

@@ -0,0 +1,38 @@
package arg_test
import (
"testing"
"gitoa.ru/go-4devs/config"
"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/config/provider/arg"
"gitoa.ru/go-4devs/config/test"
)
func testOptions(t *testing.T) config.Options {
t.Helper()
def := definition.New()
def.Add(
option.Int("listen", "listen", option.Short('l'), option.Default(8080)),
option.String("config", "config", arg.Argument, option.Default("config.hcl")),
group.New("user", "configure user",
option.String("name", "username", arg.Argument),
option.String("password", "user pass", option.Short('p')),
),
option.String("url", "url", option.Short('u'), option.Slice),
option.Duration("timeout", "timeout", option.Short('t'), option.Slice),
option.Time("start-at", "start at", option.Default(test.Time("2010-01-02T15:04:05Z"))),
group.New("end", "end at",
option.Time("after", "after", option.Slice),
proto.New("service", "service after",
option.Time("after", "after"),
),
),
)
return def
}

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

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

@@ -0,0 +1,39 @@
package arg
import (
"sync/atomic"
"gitoa.ru/go-4devs/config/param"
)
type keyParam int
const (
paramArgument keyParam = iota + 1
paramDumpReferenceView
)
//nolint:gochecknoglobals
var argNum uint64
func Argument(v param.Params) param.Params {
return param.With(v, paramArgument, atomic.AddUint64(&argNum, 1)-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
}

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