Skip to content

Commit

Permalink
Release v1.2.0
Browse files Browse the repository at this point in the history
- time support
- logo
- small fixes

Close ilyakaznacheev#10
Close ilyakaznacheev#26
  • Loading branch information
ilyakaznacheev authored and illiafox committed Oct 2, 2022
1 parent e48ddc6 commit ccf78d6
Show file tree
Hide file tree
Showing 4 changed files with 308 additions and 22 deletions.
31 changes: 28 additions & 3 deletions README.md
@@ -1,3 +1,5 @@
![Clean Env](logo.svg)

# Clean Env

Minimalistic configuration reader
Expand Down Expand Up @@ -25,6 +27,7 @@ This is a simple configuration reading tool. It just does the following:
- [Update Environment Variables](#update-environment-variables)
- [Description](#description)
- [Model Format](#model-format)
- [Supported types](#supported-types)
- [Custom Functions](#custom-functions)
- [Custom Value Setter](#custom-value-setter)
- [Custom Value Update](#custom-value-update)
Expand Down Expand Up @@ -170,7 +173,23 @@ Library uses tags to configure model of configuration structure. There are follo
- `env-upd` - flag to mark a field as updatable. Run `UpdateEnv(&cfg)` to refresh updatable variables from environment;
- `env-default="<value>"` - default value. If the field wasn't filled from the environment variable default value will be used instead;
- `env-separator="<value>"` - custom list and map separator. If not set, the default separator `,` will be used;
- `env-description="<value>"` - environment variable description.
- `env-description="<value>"` - environment variable description;
- `env-layout="<value>"` - parsing layout (for types like `time.Time`);

## Supported types

There are following supported types:

- `int` (any kind);
- `float` (any kind);
- `string`;
- `boolean`;
- slices (of any other supported type);
- maps (of any other supported type);
- `time.Duration`;
- `time.Time` (layout by default is RFC3339, may be overridden by `env-layout`);
- any type implementing `cleanenv.Setter` interface.


## Custom Functions

Expand Down Expand Up @@ -241,7 +260,7 @@ var cfg config
fset := flag.NewFlagSet("Example", flag.ContinueOnError)

// get config usage with wrapped flag usage
fset.Usage := cleanenv.FUsage(fset.Output(), &cfg, nil, fset.Usage)
fset.Usage = cleanenv.FUsage(fset.Output(), &cfg, nil, fset.Usage)

fset.Parse(os.Args[1:])
```
Expand Down Expand Up @@ -276,4 +295,10 @@ Any contribution is welcome.

## Thanks

Big thanks to a project [kelseyhightower/envconfig](https://github.com/kelseyhightower/envconfig) for inspiration.
Big thanks to a project [kelseyhightower/envconfig](https://github.com/kelseyhightower/envconfig) for inspiration.

The logo was made by [alexchoffy](https://www.instagram.com/alexchoffy/).

## Blog Posts

[Clean Configuration Management in Golang](https://dev.to/ilyakaznacheev/clean-configuration-management-in-golang-1c89).
58 changes: 40 additions & 18 deletions cleanenv.go
Expand Up @@ -170,6 +170,7 @@ type structMeta struct {
envList []string
fieldValue reflect.Value
defValue *string
layout *string
separator string
description string
updatable bool
Expand Down Expand Up @@ -201,18 +202,21 @@ func readStructMetadata(cfgRoot interface{}) ([]structMeta, error) {

var (
defValue *string
layout *string
separator string
)

// process nested structure
// process nested structure (except of time.Time)
if fld := s.Field(idx); fld.Kind() == reflect.Struct {
// subMeta, err := readStructMetadata(fld.Addr().Interface())
// if err != nil {
// return nil, err
// }
// metas = append(metas, subMeta...)
cfgStack = append(cfgStack, fld.Addr().Interface())
continue
// add structure to parsing stack
if fld.Type() != reflect.TypeOf(time.Time{}) {
cfgStack = append(cfgStack, fld.Addr().Interface())
continue
}
// process time.Time
if l, ok := fType.Tag.Lookup("env-layout"); ok {
layout = &l
}
}

// check is the field value can be changed
Expand Down Expand Up @@ -242,6 +246,7 @@ func readStructMetadata(cfgRoot interface{}) ([]structMeta, error) {
envList: envList,
fieldValue: s.Field(idx),
defValue: defValue,
layout: layout,
separator: separator,
description: fType.Tag.Get("env-description"),
updatable: upd,
Expand Down Expand Up @@ -285,7 +290,7 @@ func readEnvVars(cfg interface{}, update bool) error {
continue
}

if err := parseValue(meta.fieldValue, *rawValue, meta.separator); err != nil {
if err := parseValue(meta.fieldValue, *rawValue, meta.separator, meta.layout); err != nil {
return err
}
}
Expand All @@ -295,7 +300,7 @@ func readEnvVars(cfg interface{}, update bool) error {

// parseValue parses value into the corresponding field.
// In case of maps and slices it uses provided separator to split raw value string
func parseValue(field reflect.Value, value, sep string) error {
func parseValue(field reflect.Value, value, sep string, layout *string) error {
// TODO: simplify recursion

if field.CanInterface() {
Expand Down Expand Up @@ -358,7 +363,7 @@ func parseValue(field reflect.Value, value, sep string) error {

// parse sliced value
case reflect.Slice:
sliceValue, err := parseSlice(valueType, value, sep)
sliceValue, err := parseSlice(valueType, value, sep, layout)
if err != nil {
return err
}
Expand All @@ -367,13 +372,30 @@ func parseValue(field reflect.Value, value, sep string) error {

// parse mapped value
case reflect.Map:
mapValue, err := parseMap(valueType, value, sep)
mapValue, err := parseMap(valueType, value, sep, layout)
if err != nil {
return err
}

field.Set(*mapValue)

case reflect.Struct:
// process time.Time only
if valueType.PkgPath() == "time" && valueType.Name() == "Time" {

var l string
if layout != nil {
l = *layout
} else {
l = time.RFC3339
}
val, err := time.Parse(l, value)
if err != nil {
return err
}
field.Set(reflect.ValueOf(val))
}

default:
return fmt.Errorf("unsupported type %s.%s", valueType.PkgPath(), valueType.Name())
}
Expand All @@ -382,7 +404,7 @@ func parseValue(field reflect.Value, value, sep string) error {
}

// parseSlice parses value into a slice of given type
func parseSlice(valueType reflect.Type, value string, sep string) (*reflect.Value, error) {
func parseSlice(valueType reflect.Type, value string, sep string, layout *string) (*reflect.Value, error) {
sliceValue := reflect.MakeSlice(valueType, 0, 0)
if valueType.Elem().Kind() == reflect.Uint8 {
sliceValue = reflect.ValueOf([]byte(value))
Expand All @@ -391,7 +413,7 @@ func parseSlice(valueType reflect.Type, value string, sep string) (*reflect.Valu
sliceValue = reflect.MakeSlice(valueType, len(values), len(values))

for i, val := range values {
if err := parseValue(sliceValue.Index(i), val, sep); err != nil {
if err := parseValue(sliceValue.Index(i), val, sep, layout); err != nil {
return nil, err
}
}
Expand All @@ -400,22 +422,22 @@ func parseSlice(valueType reflect.Type, value string, sep string) (*reflect.Valu
}

// parseMap parses value into a map of given type
func parseMap(valueType reflect.Type, value string, sep string) (*reflect.Value, error) {
func parseMap(valueType reflect.Type, value string, sep string, layout *string) (*reflect.Value, error) {
mapValue := reflect.MakeMap(valueType)
if len(strings.TrimSpace(value)) != 0 {
pairs := strings.Split(value, sep)
for _, pair := range pairs {
kvPair := strings.Split(pair, ":")
kvPair := strings.SplitN(pair, ":", 2)
if len(kvPair) != 2 {
return nil, fmt.Errorf("invalid map item: %q", pair)
}
k := reflect.New(valueType.Key()).Elem()
err := parseValue(k, kvPair[0], sep)
err := parseValue(k, kvPair[0], sep, layout)
if err != nil {
return nil, err
}
v := reflect.New(valueType.Elem()).Elem()
err = parseValue(v, kvPair[1], sep)
err = parseValue(v, kvPair[1], sep, layout)
if err != nil {
return nil, err
}
Expand Down
104 changes: 103 additions & 1 deletion cleanenv_test.go
Expand Up @@ -22,10 +22,21 @@ func (t *testUpdater) Update() error {

func TestReadEnvVars(t *testing.T) {
durationFunc := func(s string) time.Duration {
d, _ := time.ParseDuration(s)
d, err := time.ParseDuration(s)
if err != nil {
t.Fatal(err)
}
return d
}

timeFunc := func(s, l string) time.Time {
tm, err := time.Parse(l, s)
if err != nil {
t.Fatal(err)
}
return tm
}

ta := &testUpdater{
err: errors.New("test"),
}
Expand All @@ -44,12 +55,23 @@ func TestReadEnvVars(t *testing.T) {
Boolean bool `env:"TEST_BOOLEAN"`
String string `env:"TEST_STRING"`
Duration time.Duration `env:"TEST_DURATION"`
Time time.Time `env:"TEST_TIME"`
ArrayInt []int `env:"TEST_ARRAYINT"`
ArrayString []string `env:"TEST_ARRAYSTRING"`
MapStringInt map[string]int `env:"TEST_MAPSTRINGINT"`
MapStringString map[string]string `env:"TEST_MAPSTRINGSTRING"`
}

type TimeTypes struct {
Time1 time.Time `env:"TEST_TIME1"`
Time2 time.Time `env:"TEST_TIME2" env-layout:"Mon Jan _2 15:04:05 2006"`
Time3 time.Time `env:"TEST_TIME3" env-layout:"Jan _2 15:04:05"`
Time4 time.Time `env:"TEST_TIME4" env-default:"2012-04-23T18:25:43.511Z"`
Time5 time.Time `env:"TEST_TIME5" env-default:"Mon Mar 10 11:11:11 2011" env-layout:"Mon Jan _2 15:04:05 2006"`
Time6 []time.Time `env:"TEST_TIME6" env-separator:"|"`
Time7 map[string]time.Time `env:"TEST_TIME7" env-separator:"|"`
}

tests := []struct {
name string
env map[string]string
Expand Down Expand Up @@ -82,6 +104,7 @@ func TestReadEnvVars(t *testing.T) {
"TEST_BOOLEAN": "true",
"TEST_STRING": "test",
"TEST_DURATION": "1h5m10s",
"TEST_TIME": "2012-04-23T18:25:43.511Z",
"TEST_ARRAYINT": "1,2,3",
"TEST_ARRAYSTRING": "a,b,c",
"TEST_MAPSTRINGINT": "a:1,b:2,c:3",
Expand All @@ -95,6 +118,7 @@ func TestReadEnvVars(t *testing.T) {
Boolean: true,
String: "test",
Duration: durationFunc("1h5m10s"),
Time: timeFunc("2012-04-23T18:25:43.511Z", time.RFC3339),
ArrayInt: []int{1, 2, 3},
ArrayString: []string{"a", "b", "c"},
MapStringInt: map[string]int{
Expand All @@ -111,6 +135,34 @@ func TestReadEnvVars(t *testing.T) {
wantErr: false,
},

{
name: "times",
env: map[string]string{
"TEST_TIME1": "2012-04-23T18:25:43.511Z",
"TEST_TIME2": "Mon Mar 10 11:11:11 2011",
"TEST_TIME3": "Dec 1 11:11:11",
"TEST_TIME6": "2012-04-23T18:25:43.511Z|2012-05-23T18:25:43.511Z",
"TEST_TIME7": "a:2012-04-23T18:25:43.511Z|b:2012-05-23T18:25:43.511Z",
},
cfg: &TimeTypes{},
want: &TimeTypes{
Time1: timeFunc("2012-04-23T18:25:43.511Z", time.RFC3339),
Time2: timeFunc("Mon Mar 10 11:11:11 2011", time.ANSIC),
Time3: timeFunc("Dec 1 11:11:11", time.Stamp),
Time4: timeFunc("2012-04-23T18:25:43.511Z", time.RFC3339),
Time5: timeFunc("Mon Mar 10 11:11:11 2011", time.ANSIC),
Time6: []time.Time{
timeFunc("2012-04-23T18:25:43.511Z", time.RFC3339),
timeFunc("2012-05-23T18:25:43.511Z", time.RFC3339),
},
Time7: map[string]time.Time{
"a": timeFunc("2012-04-23T18:25:43.511Z", time.RFC3339),
"b": timeFunc("2012-05-23T18:25:43.511Z", time.RFC3339),
},
},
wantErr: false,
},

{
name: "wrong types",
env: map[string]string{
Expand Down Expand Up @@ -252,6 +304,56 @@ func TestReadEnvVars(t *testing.T) {
}
}

func TestReadEnvVarsTime(t *testing.T) {
timeFunc := func(s, l string) time.Time {
tm, err := time.Parse(l, s)
if err != nil {
t.Fatal(err)
}
return tm
}

type Timed struct {
Time time.Time `env:"TEST_TIME" env-layout:"Mon Jan _2 15:04:05 2006"`
}

tests := []struct {
name string
env map[string]string
cfg interface{}
want interface{}
wantErr bool
}{
{
name: "time",
env: map[string]string{
"TEST_TIME": "Mon Mar 10 11:11:11 2011",
},
cfg: &Timed{},
want: &Timed{
Time: timeFunc("Mon Mar 10 11:11:11 2011", time.ANSIC),
},
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for env, val := range tt.env {
os.Setenv(env, val)
}
defer os.Clearenv()

if err := readEnvVars(tt.cfg, false); (err != nil) != tt.wantErr {
t.Errorf("wrong error behavior %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(tt.cfg, tt.want) {
t.Errorf("wrong data %v, want %v", tt.cfg, tt.want)
}
})
}
}

type testConfigUpdateFunction struct {
One string
Two string
Expand Down

0 comments on commit ccf78d6

Please sign in to comment.