Skip to content

Commit fedfbe0

Browse files
omeidOmeid Matten
andauthored
New Inteface and Command Support (#19)
* add relative rename support * support slices of encoding..TextUnmarshaler * add support for cli command via flags plugin * allow exposing a pointer * allow exposing an interface type of the underlying value * plugins/flag: support default for command * plugins/flag: work with defaults instead of duplicate * inc required test files * new plugin: file.NewMulti * plugins/flag: move the command last * plugins/file: better report on decoding failure * plugins/flag: support required flags * plugins/flag allow command after boolean flag * plugins/flag: error on extra arguments * new interface with command support * update min go version * update docs --------- Co-authored-by: Omeid Matten <omeid@kemene.com>
1 parent 609ddbc commit fedfbe0

44 files changed

Lines changed: 1529 additions & 544 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- name: Set up Go
2020
uses: actions/setup-go@v4
2121
with:
22-
go-version: '1.20'
22+
go-version: '1.21'
2323

2424
- name: Build
2525
run: go build -v ./...

.golangci.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
version: 2
3+
4+
5+
formatters:
6+
enable:
7+
- gofmt
8+
- gofumpt
9+
- goimports
10+
linters:
11+
enable:
12+
- staticcheck
13+
- errcheck

CHANGELOG.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
# Changelog
22

3+
## v0.7
4+
5+
### Breaking Changes
6+
7+
Field names now can be renamed at struct level instead of globally by prefixing them with a `.`.
8+
9+
So that in the following example, the config Redis.Host will be mapped to Redis.Address
10+
While Port will be mapped to REDIS_SERVICE_PORT.
11+
12+
13+
```go
14+
type Redis struct {
15+
Host string `unconfig:".Address"`
16+
Port string `unconfig:"REDIS_SERVICE_PORT"`
17+
}
18+
19+
20+
type Config struct {
21+
Redis Redis
22+
}
23+
```
24+
25+
26+
27+
28+
## v0.6
29+
30+
Artificial release to retract v1.
31+
32+
333
## v0.5
434

535
### Breaking Changes
@@ -27,7 +57,7 @@ type Files []struct {
2757
}
2858
```
2959

30-
to
60+
to
3161

3262
```go
3363
type Files []struct {

README.md

Lines changed: 63 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
Lightweight, zero-dependency, and extendable configuration management.
55

6-
uConfig is extremely light and extendable configuration management library with zero dependencies. Every aspect of configuration is provided through a _plugin_, which means you can have any combination of flags, environment variables, defaults, secret providers, Kubernetes Downward API, and what you want, and only what you want, through plugins.
6+
uConfig is extremely light and extendable configuration management library with zero dependencies. Every aspect of configuration is provided through a _plugin_, which means you can have any combination of flags, environment variables, defaults, secret providers, Kubernetes Downward API, and any combination of configuration files and formats including json, toml, cue, or just about anything you want, and only what you want, through plugins.
77

88

99
uConfig takes the config schema as a struct decorated with tags, nesting is supported.
@@ -37,48 +37,37 @@ type Config struct {
3737
```go
3838
package main
3939

40-
41-
4240
import (
43-
"encoding/json"
41+
"encoding/json"
42+
"fmt"
43+
"os"
4444

45-
"github.com/omeid/uconfig"
45+
"github.com/omeid/uconfig"
4646

47-
"$PROJECT/redis"
48-
"$PROJECT/database"
47+
"github.com/omeid/uconfig/examples/sample/database"
48+
"github.com/omeid/uconfig/examples/sample/redis"
4949
)
5050

5151
// Config is our application config.
5252
type Config struct {
53+
// yes you can have slices.
54+
Hosts []string `default:"localhost,localhost.local" usage:"the ip or domains to bind to"`
5355

54-
// yes you can have slices.
55-
Hosts []string `default:"localhost,localhost.local" usage:"the ip or domains to bind to"`
56-
57-
Redis redis.Config
58-
Database database.Config
59-
56+
Redis redis.Config
57+
Database database.Config
6058
}
6159

60+
var files = uconfig.Files{
61+
{"config.json", json.Unmarshal, true},
62+
// you can of course add as many files
63+
// as you want, and they will be applied
64+
// in the given order.
65+
}
6266

67+
var conf = uconfig.Classic[Config](files)
6368

6469
func main() {
65-
66-
conf := &Config{}
67-
68-
files := uconfig.Files{
69-
{"config.json", json.Unmarshal, true},
70-
// you can of course add as many files
71-
// as you want, and they will be applied
72-
// in the given order.
73-
}
74-
75-
_, err := uconfig.Classic(&conf, files)
76-
77-
if err != nil {
78-
fmt.Println(err)
79-
os.Exit(1)
80-
}
81-
70+
conf := conf.Run()
8271
// use conf as you please.
8372
// let's pretty print it as JSON for example:
8473
configAsJson, err := json.MarshalIndent(conf, "", " ")
@@ -88,7 +77,6 @@ func main() {
8877
}
8978

9079
fmt.Print(string(configAsJson))
91-
9280
}
9381
```
9482

@@ -140,25 +128,27 @@ Sometimes you might want to use a different env var, or flag name for backwards
140128

141129
1. uconfig tag
142130

143-
You can change the name of a field as seen by unconfig.
144-
This option supports the usual nesting prefixing.
145-
See the port example below.
131+
You can change the name of a field as seen by `uconfig`.
132+
133+
Please not that this flag only works for walker plugins (flags, env, anything flat) and for Visitor plugins (file, stream, et al) you will need
134+
to use encoder specific tags like `json:"field_name"` and so on.
135+
146136

147137
2. Plugin specific tags
148138

149-
Most plugins support controlling the field name as seen by that specific plugin.
139+
Most plugins support controlling the field name as seen by that specific plugin. For example `env:"DB_NAME"`.
150140

151-
This option does not support nesting prefixes.
152-
See the Database field in the example below.
153141

142+
For both type of tags, you can prefix them with `.` to rename the field only at the struct level.
143+
See the `Service.Port` and DB_NAME examples below.
154144

155145
```go
156146
package database
157147

158148
// Config holds the database configurations.
159149
type Database struct {
160150
Address string `default:"localhost"`
161-
Port string `default:"28015" uconfig:"Service.Port"`
151+
Port string `default:"28015" uconfig:".Service.Port"`
162152
Database string `default:"my-project" env:"DB_NAME" flag:"main-db-name"`
163153
}
164154
```
@@ -216,62 +206,56 @@ The secret provider allows you to grab the value of a config from anywhere you w
216206
Unlike most other plugins, secret requires explicit `secret:""` tag, this is because only specific config values like passwords and api keys come from a secret provider, compared to the rest of the config which can be set in various ways.
217207

218208
```go
209+
package main
219210

220211
import (
212+
"encoding/json"
213+
"fmt"
221214

222-
"github.com/omeid/uconfig"
223-
"github.com/omeid/uconfig/plugins/secret"
215+
"github.com/omeid/uconfig"
216+
"github.com/omeid/uconfig/examples/secrets/secretsource"
217+
"github.com/omeid/uconfig/plugins/secret"
224218
)
219+
225220
// Creds is an example of a config struct that uses secret values.
226221
type Creds struct {
227-
// by default, secret plugin will generate a name that is identical
228-
// to env plugin, SCREAM_SNAKE_CASE, so in this case it will be
229-
// APIKEY however, following the standard uConfig nesting rules
230-
// in Config struct below, it becomes CREDS_APIKEY.
231-
APIKey string `secret:""`
232-
// or you can provide your own name, which will not be impacted
233-
// by nesting or the field name.
234-
APIToken string `secret:"API_TOKEN"`
222+
// by default, secret plugin will generate a name that is identical
223+
// to env plugin, SCREAM_SNAKE_CASE, so in this case it will be
224+
// APIKEY however, following the standard uConfig nesting rules
225+
// in Config struct below, it becomes CREDS_APIKEY.
226+
APIKey string `secret:""`
227+
// or you can provide your own name, which will not be impacted
228+
// by nesting or the field name.
229+
APIToken string `secret:"API_TOKEN"`
235230
}
236231

237232
type Config struct {
238-
Redis Redis
239-
Creds Creds
233+
Creds Creds
240234
}
241235

236+
var files = uconfig.Files{
237+
{"config.json", json.Unmarshal, false},
238+
}
242239

243-
func main() {
244-
245-
conf := &Config{}
246-
247-
248-
files := uconfig.Files{
249-
{"config.json", json.Unmarshal, false}
250-
}
251-
252-
// secret.New accepts a function that maps a secret name to it's value.
253-
secretPlugin := secret.New(func(name string) (string, error) {
254-
// you're free to grab the secret based on the name from wherever
255-
// you please, aws secrets-manager, hashicorp vault, or wherever.
256-
value, ok := secretSource.Get(name)
257-
258-
if !ok {
259-
return "", ErrSecretNotFound
260-
}
240+
var secrets = secret.New(func(name string) (string, error) {
241+
// you're free to grab the secret based on the name from wherever
242+
// you please, aws secrets-manager, hashicorp vault, or wherever.
243+
value, ok := secretsource.Get(name)
244+
if !ok {
245+
return "", secret.ErrSecretNotFound
246+
}
261247

262-
return value, nil
263-
})
248+
return value, nil
249+
})
264250

265-
// then you can use the secretPlugin with uConfig like any other plugin.
266-
// Lucky, uconfig.Classic allows passing more plugins, which means
267-
// you can simply do the following for flags, envs, files, and secrets!
268-
_, err := uconfig.Classic(&conf, files, secretPlugin)
269-
if err != nil {
270-
t.Fatal(err)
271-
}
251+
func main() {
252+
// then you can use the secretPlugin with uConfig like any other plugin.
253+
// Lucky, uconfig.Classic allows passing more plugins, which means
254+
// you can simply do the following for flags, envs, files, and secrets!
255+
conf := uconfig.Classic[Config](files, secrets).Run()
272256

257+
fmt.Printf("we got an API Key: %s\n", conf.Creds.APIKey)
273258
}
274-
275259
```
276260

277261

@@ -290,10 +274,8 @@ import (
290274

291275
func TestSomething(t *testing.T) error {
292276

293-
conf := &YourConfigStruct{}
294-
295277
// It will panic on error
296-
uconfig.Must(conf, defaults.New())
278+
conf := uconfig.Must[Conf](defaults.New())
297279

298280
// Use your conf as you please.
299281
}

classic.go

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package uconfig
22

33
import (
4-
"errors"
5-
"os"
6-
74
"github.com/omeid/uconfig/plugins"
85
"github.com/omeid/uconfig/plugins/defaults"
96
"github.com/omeid/uconfig/plugins/env"
@@ -17,32 +14,21 @@ type Files = file.Files
1714
// Classic creates a uconfig manager with defaults,environment variables,
1815
// and flags (in that order) and optionally file loaders based on the provided
1916
// Files map and parses them right away.
20-
func Classic(conf interface{}, files Files, userPlugins ...plugins.Plugin) (Config, error) {
21-
22-
fps := files.Plugins()
23-
24-
ps := make([]plugins.Plugin, 0, len(fps)+3+len(userPlugins))
25-
17+
func Classic[C any](files Files, userPlugins ...plugins.Plugin) Config[C] {
18+
// almost a duplicate of Load, but due to the order of things, not worth abstracting for a few lines.
19+
ps := make([]plugins.Plugin, 0, len(files)+3+len(userPlugins))
2620
// first defaults
2721
ps = append(ps, defaults.New())
2822
// then files
29-
ps = append(ps, fps...)
30-
// followed by env and flags
31-
ps = append(ps, env.New(), flag.Standard())
23+
ps = append(ps, files.Plugins()...)
3224
// then any user pugins, often just _secret_.
3325
ps = append(ps, userPlugins...)
3426

35-
c, err := New(conf, ps...)
36-
37-
if err != nil {
38-
return c, err
39-
}
27+
// followed by envs
28+
ps = append(ps, env.New())
4029

41-
err = c.Parse()
42-
if errors.Is(err, ErrUsage) {
43-
c.Usage()
44-
os.Exit(0)
45-
}
30+
// and lastly flags.
31+
ps = append(ps, flag.Standard())
4632

47-
return c, err
33+
return New[C](ps...)
4834
}

0 commit comments

Comments
 (0)