Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Milad Abbasi
committed
Jan 1, 2021
1 parent
646bfdf
commit b8aca6b
Showing
11 changed files
with
913 additions
and
357 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,3 @@ | |
# vendor | ||
|
||
.DS_Store | ||
|
||
.env* | ||
!.env.example |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
package gonfig | ||
|
||
import ( | ||
"errors" | ||
"os" | ||
"strings" | ||
|
||
"github.com/joho/godotenv" | ||
) | ||
|
||
// EnvProvider loads values from environment variables to provided struct | ||
type EnvProvider struct { | ||
// Prefix is used when finding values from environment variables, defaults to "" | ||
EnvPrefix string | ||
|
||
// SnakeCase specifies whether to convert field names to snake_case or not, defaults to true | ||
SnakeCase bool | ||
|
||
// UpperCase specifies whether to convert field names to UPPERCASE or not, defaults to true | ||
UpperCase bool | ||
|
||
// FieldSeparator is used to separate field names, defaults to "_" | ||
FieldSeparator string | ||
|
||
// Source is used to retrieve environment variables | ||
// It can be either a path to a file or empty string, if empty OS will be used | ||
Source string | ||
|
||
// Whether to report error if env file is not found, defaults to false | ||
Required bool | ||
} | ||
|
||
// NewEnvProvider creates a new EnvProvider | ||
func NewEnvProvider() *EnvProvider { | ||
return &EnvProvider{ | ||
EnvPrefix: "", | ||
SnakeCase: true, | ||
UpperCase: true, | ||
FieldSeparator: "_", | ||
Source: "", | ||
Required: false, | ||
} | ||
} | ||
|
||
// NewEnvFileProvider creates a new EnvProvider from .env file | ||
func NewEnvFileProvider(path string) *EnvProvider { | ||
return &EnvProvider{ | ||
EnvPrefix: "", | ||
SnakeCase: true, | ||
UpperCase: true, | ||
FieldSeparator: "_", | ||
Source: path, | ||
Required: false, | ||
} | ||
} | ||
|
||
// Name of provider | ||
func (ep *EnvProvider) Name() string { | ||
return "ENV provider" | ||
} | ||
|
||
// Fill takes struct fields and fills their values | ||
func (ep *EnvProvider) Fill(in *Input) error { | ||
content, err := ep.envMap() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
for _, f := range in.Fields { | ||
value, err := ep.provide(content, f.Tags.Config, f.Path) | ||
if err != nil { | ||
if errors.Is(err, ErrKeyNotFound) { | ||
continue | ||
} | ||
|
||
return err | ||
} | ||
|
||
err = in.setValue(f, value) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
f.IsSet = true | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// envMap returns environment variables map from either OS or file specified by source | ||
// Defaults to operating system env variables | ||
func (ep *EnvProvider) envMap() (map[string]string, error) { | ||
envs := envFromOS() | ||
var fileEnvs map[string]string | ||
var err error | ||
|
||
if ep.Source != "" { | ||
fileEnvs, err = envFromFile(ep.Source) | ||
} | ||
if err != nil { | ||
notExistsErr := errors.Is(err, os.ErrNotExist) | ||
if (notExistsErr && ep.Required) || !notExistsErr { | ||
return nil, err | ||
} | ||
} | ||
|
||
if len(envs) == 0 { | ||
if len(fileEnvs) == 0 { | ||
return nil, nil | ||
} | ||
|
||
envs = make(map[string]string) | ||
} | ||
|
||
for k, v := range fileEnvs { | ||
_, exists := envs[k] | ||
if !exists { | ||
envs[k] = v | ||
} | ||
} | ||
|
||
return envs, nil | ||
} | ||
|
||
// returns environment variables map retrieved from operating system | ||
func envFromOS() map[string]string { | ||
envs := os.Environ() | ||
if len(envs) == 0 { | ||
return nil | ||
} | ||
|
||
envMap := make(map[string]string) | ||
|
||
for _, env := range envs { | ||
keyValue := strings.SplitN(env, "=", 2) | ||
if len(keyValue) < 2 { | ||
continue | ||
} | ||
|
||
envMap[keyValue[0]] = keyValue[1] | ||
} | ||
|
||
return envMap | ||
} | ||
|
||
// returns environment variables map retrieved from specified file | ||
func envFromFile(path string) (map[string]string, error) { | ||
m, err := godotenv.Read(path) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return m, nil | ||
} | ||
|
||
// provide find a value from env variables based on specified key and path | ||
func (ep *EnvProvider) provide(content map[string]string, key string, path []string) (string, error) { | ||
k := ep.buildKey(key, path) | ||
value, exists := content[k] | ||
if !exists { | ||
return "", ErrKeyNotFound | ||
} | ||
|
||
return value, nil | ||
} | ||
|
||
// buildKey prefix key with EnvPrefix, if not provided, path slice will be used | ||
func (ep *EnvProvider) buildKey(key string, path []string) string { | ||
if key != "" { | ||
return ep.EnvPrefix + key | ||
} | ||
|
||
k := strings.Join(path, ep.FieldSeparator) | ||
if ep.SnakeCase { | ||
k = toSnakeCase(k) | ||
} | ||
if ep.UpperCase { | ||
k = strings.ToUpper(k) | ||
} | ||
|
||
k = ep.EnvPrefix + k | ||
|
||
return k | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package gonfig | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/BurntSushi/toml" | ||
"gopkg.in/yaml.v2" | ||
) | ||
|
||
// Supported file extensions | ||
const ( | ||
JSON = ".json" | ||
YML = ".yml" | ||
YAML = ".yaml" | ||
ENV = ".env" | ||
TOML = ".toml" | ||
) | ||
|
||
// 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 | ||
} | ||
|
||
// 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 "File provider" | ||
} | ||
|
||
// 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 | ||
} | ||
|
||
_, err := fp.provide(content, key, f.Path) | ||
if 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) { | ||
f, err := os.Open(fp.FilePath) | ||
if err != nil { | ||
if os.IsNotExist(err) && !fp.Required { | ||
return nil | ||
} | ||
|
||
return fmt.Errorf("file provider: %w", 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) | ||
|
||
default: | ||
err = fmt.Errorf(unsupportedFileExtErrFormat, ErrUnsupportedFileExt, fp.FileExt) | ||
} | ||
|
||
if err != nil { | ||
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) { | ||
return traverseMap(content, fp.buildPath(key, path)) | ||
} | ||
|
||
// 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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,9 @@ | ||
module github.com/milad-abbasi/gonfig | ||
|
||
go 1.15 | ||
|
||
require ( | ||
github.com/BurntSushi/toml v0.3.1 | ||
github.com/joho/godotenv v1.3.0 | ||
gopkg.in/yaml.v2 v2.4.0 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= | ||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= | ||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= | ||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= |
Oops, something went wrong.