-
Notifications
You must be signed in to change notification settings - Fork 1
/
file.go
153 lines (125 loc) · 3.06 KB
/
file.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package gonfig
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/BurntSushi/toml"
"gopkg.in/yaml.v3"
)
// Supported file extensions
const (
JSON = ".json"
YML = ".yml"
YAML = ".yaml"
TOML = ".toml"
ENV = ".env"
)
// FileProvider loads values from file to provided struct
type FileProvider struct {
// Path to file
FilePath string
// File will be decoded based on extension
// .json, .yml(.yaml), .env and .toml file extensions are supported
FileExt string
// Whether to report error if file is not found, defaults to false
Required bool
}
var (
_ Provider = (*FileProvider)(nil)
_ Unmarshaler = (*FileProvider)(nil)
_ Filler = (*FileProvider)(nil)
)
// NewFileProvider creates a new FileProvider from specified path
func NewFileProvider(path string) *FileProvider {
return &FileProvider{
FilePath: path,
FileExt: filepath.Ext(path),
Required: false,
}
}
// Name of provider
func (fp *FileProvider) Name() string {
return fmt.Sprintf("File provider (%v)", fp.FileExt[1:])
}
// UnmarshalStruct takes a struct pointer and loads values from provided file into it
func (fp *FileProvider) UnmarshalStruct(i interface{}) error {
return fp.decode(i)
}
// Fill takes struct fields and and checks if their value is set
func (fp *FileProvider) Fill(in *Input) error {
var content map[string]interface{}
if err := fp.decode(&content); err != nil {
return err
}
for _, f := range in.Fields {
if f.IsSet {
continue
}
var key string
switch fp.FileExt {
case JSON:
key = f.Tags.Json
case YML, YAML:
key = f.Tags.Yaml
case TOML:
key = f.Tags.Toml
}
if _, err := fp.provide(content, key, f.Path); err == nil {
f.IsSet = true
}
}
return nil
}
// decode opens specified file and loads its content to input argument
func (fp *FileProvider) decode(i interface{}) (err error) {
switch fp.FileExt {
case JSON, YML, YAML, TOML:
default:
return fmt.Errorf(unsupportedFileExtErrFormat, ErrUnsupportedFileExt, fp.FileExt)
}
f, err := os.Open(fp.FilePath)
if err != nil {
if os.IsNotExist(err) && !fp.Required {
return nil
}
return err
}
defer func() {
if cErr := f.Close(); cErr != nil && err == nil {
err = cErr
}
}()
switch fp.FileExt {
case JSON:
err = json.NewDecoder(f).Decode(i)
case YML, YAML:
err = yaml.NewDecoder(f).Decode(i)
case TOML:
_, err = toml.DecodeReader(f, i)
}
if err != nil && !errors.Is(err, io.EOF) {
return fmt.Errorf(decodeFailedErrFormat, err)
}
return nil
}
// provide find a value from file content based on specified key and path
func (fp *FileProvider) provide(content map[string]interface{}, key string, path []string) (string, error) {
builtPath := fp.buildPath(key, path)
value, exists := traverseMap(content, builtPath)
if !exists {
return "", ErrKeyNotFound
}
return value, nil
}
// buildPath makes a path from key and path slice
func (fp *FileProvider) buildPath(key string, path []string) []string {
newPath := make([]string, len(path))
copy(newPath, path)
if key != "" {
newPath[len(newPath)-1] = key
}
return newPath
}