-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfig.go
139 lines (115 loc) · 3.36 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
139
// Copyright © 2024 Meroxa, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ecdysis
import (
"errors"
"fmt"
"os"
"reflect"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
type Config struct {
EnvPrefix string
Parsed any
DefaultValues any
Path string
}
// setDefaults sets the default values for the configuration. slices and maps are not supported.
func setDefaults(v *viper.Viper, defaults interface{}) {
val := reflect.ValueOf(defaults)
typ := reflect.TypeOf(defaults)
if val.Kind() == reflect.Ptr {
if val.IsNil() {
return
}
val = val.Elem()
typ = typ.Elem()
}
if val.Kind() != reflect.Struct {
return
}
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
fieldName := fieldType.Tag.Get("long")
if fieldName == "" {
fieldName = fieldType.Tag.Get("short")
}
if fieldName == "" {
continue
}
switch field.Kind() { //nolint:exhaustive // no need to handle all cases
case reflect.Struct:
setDefaults(v, field.Interface())
case reflect.Ptr:
if !field.IsNil() {
setDefaults(v, field.Interface())
}
default:
if field.CanInterface() {
v.SetDefault(fieldName, field.Interface())
}
}
}
}
// bindViperConfig parses the configuration (from cfg and cmd) into the viper instance.
func bindViperConfig(v *viper.Viper, cfg Config, cmd *cobra.Command) error {
// Handle env variables
v.SetEnvPrefix(cfg.EnvPrefix)
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Handle config file
v.SetConfigFile(cfg.Path)
if err := v.ReadInConfig(); err != nil {
// we make the existence of the config file optional
if !os.IsNotExist(err) {
return fmt.Errorf("fatal error config file: %w", err)
}
}
var errs []error
// Handle flags
cmd.Flags().VisitAll(func(f *pflag.Flag) {
if err := v.BindPFlag(f.Name, f); err != nil {
errs = append(errs, err)
}
})
if err := errors.Join(errs...); err != nil {
return fmt.Errorf("error binding flags: %w", err)
}
return nil
}
// ParseConfig parses the configuration into cfg.Parsed using viper.
// This is useful for any decorator that needs to parse configuration based on the available flags.
func ParseConfig(cfg Config, cmd *cobra.Command) error {
parsedType := reflect.TypeOf(cfg.Parsed)
// Ensure Parsed is a pointer
if parsedType.Kind() != reflect.Ptr {
return fmt.Errorf("parsed must be a pointer")
}
if parsedType.Elem() != reflect.TypeOf(cfg.DefaultValues) {
return fmt.Errorf("parsed and defaultValues must be the same type")
}
viper := viper.New()
setDefaults(viper, cfg.DefaultValues)
if err := bindViperConfig(viper, cfg, cmd); err != nil {
return fmt.Errorf("error parsing config: %w", err)
}
if err := viper.Unmarshal(cfg.Parsed); err != nil {
return fmt.Errorf("error unmarshalling config: %w", err)
}
return nil
}