-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathreaders.go
134 lines (106 loc) · 3.54 KB
/
readers.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
package alligotor
import (
"encoding/json"
"errors"
"io"
"reflect"
"github.com/mitchellh/mapstructure"
"gopkg.in/yaml.v3"
)
const fileKey = "file"
var ErrFileFormatNotSupported = errors.New("file format not supported or malformed content")
// ReadersSource is used to read configuration from any type that implements the io.Reader interface.
// The data in the readers should be in one of the supported file formats (currently yml and json).
// This enables a wide range of usages like for example reading the config from an http endpoint or a file.
//
// The ReadersSource accepts io.Reader to support as many types as possible. To improve the experience with sources
// that need to be closed it will also check if the supplied type implements io.Closer and closes the reader
// if it does.
type ReadersSource struct {
readers []io.Reader
fileMaps []*ciMap
}
// NewReadersSource returns a new ReadersSource that reads from one or more readers.
// If the input reader slice is empty this will be a noop reader.
func NewReadersSource(readers ...io.Reader) *ReadersSource {
return &ReadersSource{
readers: readers,
}
}
// Init initializes the fileMaps property.
// It should be used right before calling the Read method to load the latest config files' states.
func (s *ReadersSource) Init(_ []Field) error {
for _, reader := range s.readers {
if err := func() error {
if closer, ok := reader.(io.Closer); ok {
defer closer.Close()
}
m, err := unmarshal(reader)
if err != nil {
return nil
}
s.fileMaps = append(s.fileMaps, m)
return nil
}(); err != nil {
return err
}
}
return nil
}
// Read reads the saved fileMaps from the Init function and returns the set value for a certain field.
// If not value is set in the flags it returns nil.
func (s *ReadersSource) Read(field *Field) (interface{}, error) {
var finalVal interface{}
for _, m := range s.fileMaps {
val, err := readFileMap(field, m)
if err != nil {
return nil, err
}
finalVal = val
}
return finalVal, nil
}
// unmarshal tries to decode the reader's data into any supported fileType. If it does not work for any file format
// an ErrFileFormatNotSupported is returned.
func unmarshal(r io.Reader) (*ciMap, error) {
m := newCiMap()
if err := yaml.NewDecoder(r).Decode(m); err == nil {
return m, nil
}
if err := json.NewDecoder(r).Decode(m); err == nil {
return m, nil
}
return nil, ErrFileFormatNotSupported
}
// readFileMap reads the value for a given field from the given ciMap.
// It returns the right type if there is no decoding error otherwise it returns a byte slice that could potentially
// be decoded later into the target type.
func readFileMap(f *Field, m *ciMap) (interface{}, error) {
name := extractFileName(f)
valueForField, ok := m.Get(f.BaseNames(extractFileName), name)
if !ok {
return nil, nil
}
fieldTypeNew := reflect.New(f.Type())
if f.Type().Kind() == reflect.Struct {
// if it's a struct, it could be assigned with TextUnmarshaler, otherwise return nil
if valueString, ok := valueForField.(string); ok {
return []byte(valueString), nil
}
return nil, nil
}
if err := mapstructure.Decode(valueForField, fieldTypeNew.Interface()); err != nil {
// if theres a type mismatch check if value is a string so maybe it can be parsed
if valueString, ok := valueForField.(string); ok {
return []byte(valueString), nil
}
return nil, err
}
return fieldTypeNew.Elem().Interface(), nil
}
func extractFileName(f *Field) string {
if f.Configs()[fileKey] != "" {
return f.Configs()[fileKey]
}
return f.Name()
}