-
Notifications
You must be signed in to change notification settings - Fork 5
/
config.go
138 lines (113 loc) · 3.39 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package config
import (
"errors"
"fmt"
"os"
"reflect"
"strings"
"github.com/spf13/viper"
)
var (
// TagName defines the struct tag used to specify the env params.
TagName = "env"
// Type specifies the ConfigType used by viper.
Type = "env"
// File specifies the ConfigFile used by viper. If the file does not exist then no error is raised.
File = ".env"
// Resolvers allow custom resolution for type mappings. See Resolver for more info.
Resolvers = ResolverMap{"pg": PgDBResolver}
// ErrInvalidConfigObject is returned when a nil pointer, or non-pointer is provided to Load.
ErrInvalidConfigObject = errors.New("config must be a pointer type")
// ErrInvalidResolver is returned when a struct tag references a resolver that is not found.
ErrInvalidResolver = errors.New("invalid resolver")
// ErrInvalidTag is returned when a struct tag is improperly defined, e.g. `env:","`.
ErrInvalidTag = errors.New("invalid tag")
)
// Resolver is used to map a key to a value. Examples are custom serialization used for Postgres Connection URL
// composition, see PgDBResolver for example.
type Resolver func(key string) (any, error)
// ResolverMap maintains the map of resolver names to Resolver funcs.
type ResolverMap = map[string]Resolver
const (
keyPos = 0
defaultPos = 1
resolverPos = 2
)
// parseTag is responsible for parsing struct tags to env config.
// The struct tag format is:
//
// `env:"ENV_VAR, DEFAULT(opt), RESOLVER(opt)"`
//
// Example Config struct:
//
// type ExampleConfig struct {
// DebugMode bool `env:"DEBUG, false"`
// Port int `env:"PORT, 3000"`
// DB string `env:"DB,localhost,pg"`
// }
func parseTag(tag string) error {
args := strings.Split(tag, ",")
key := strings.TrimSpace(args[keyPos])
if key == "" {
return fmt.Errorf("%w: `%s`", ErrInvalidTag, tag)
}
err := viper.BindEnv(key)
if err != nil {
return fmt.Errorf("binding tag: `%s`: %w", tag, err) // Ignore coverage - unlikely to error
}
if len(args) <= 1 {
return nil
}
def := strings.TrimSpace(args[defaultPos])
if def != "" {
viper.SetDefault(key, def)
}
if len(args) == resolverPos {
return nil
}
resolver := strings.TrimSpace(args[resolverPos])
if resolver != "" {
f, ok := Resolvers[resolver]
if !ok {
return fmt.Errorf("%w: `%v` for field `%v`", ErrInvalidResolver, resolver, key)
}
val, err := f(key)
if err != nil {
return err
}
viper.Set(key, val)
}
return nil
}
// Load uses struct tags (see parseTag) and viper to load the configuration into a config object. The input
// object must be a pointer to a struct. See ExampleLoad for simple example.
func Load(cfg any) error {
v := reflect.ValueOf(cfg)
if v.Kind() != reflect.Ptr || v.IsZero() {
return ErrInvalidConfigObject
}
viper.SetConfigFile(File)
viper.SetConfigType(Type)
viper.AutomaticEnv()
err := viper.ReadInConfig()
var pathError *os.PathError
if err != nil && !errors.As(err, &pathError) {
return fmt.Errorf("error loading config: %w", err)
}
v = reflect.Indirect(v)
for i := 0; i < v.NumField(); i++ {
f := v.Type().Field(i)
tag := f.Tag.Get(TagName)
if tag == "" || tag == "-" {
continue
}
if err = parseTag(tag); err != nil {
return fmt.Errorf("error parsing %s tag on field %s: %w", TagName, f.Name, err)
}
}
err = viper.Unmarshal(cfg, defaultDecoderConfig)
if err != nil {
return fmt.Errorf("error Unmarshaling: %w", err)
}
return nil
}