Skip to content

Commit

Permalink
Add support for encoding.TextUnmarshaler
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyakaznacheev committed Jul 20, 2023
2 parents cb46e5a + a5a43ad commit c35e574
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 15 deletions.
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -192,7 +192,8 @@ There are following supported types:
- maps (of any other supported type);
- `time.Duration`;
- `time.Time` (layout by default is RFC3339, may be overridden by `env-layout`);
- `*time.Location` (time zone parsing [depends](https://pkg.go.dev/time#LoadLocation) on running machine)
- `*time.Location` (time zone parsing [depends](https://pkg.go.dev/time#LoadLocation) on running machine);
- any type that implements `encoding.TextUnmarshaler`;
- any type implementing `cleanenv.Setter` interface.


Expand Down
22 changes: 16 additions & 6 deletions cleanenv.go
@@ -1,6 +1,7 @@
package cleanenv

import (
"encoding"
"encoding/json"
"flag"
"fmt"
Expand Down Expand Up @@ -459,15 +460,29 @@ func readEnvVars(cfg interface{}, update bool) error {
func parseValue(field reflect.Value, value, sep string, layout *string) error {
// TODO: simplify recursion

valueType := field.Type()

// look for supported struct parser
// parsing of struct must be done before checking the implementation `encoding.TextUnmarshaler`
// standard struct types already have the implementation `encoding.TextUnmarshaler` (for example `time.Time`)
if structParser, found := validStructs[valueType]; found {
return structParser(&field, value, layout)
}

if field.CanInterface() {
if ct, ok := field.Interface().(encoding.TextUnmarshaler); ok {
return ct.UnmarshalText([]byte(value))
} else if ctp, ok := field.Addr().Interface().(encoding.TextUnmarshaler); ok {
return ctp.UnmarshalText([]byte(value))
}

if cs, ok := field.Interface().(Setter); ok {
return cs.SetValue(value)
} else if csp, ok := field.Addr().Interface().(Setter); ok {
return csp.SetValue(value)
}
}

valueType := field.Type()
switch valueType.Kind() {

// parse string value
Expand Down Expand Up @@ -542,11 +557,6 @@ func parseValue(field reflect.Value, value, sep string, layout *string) error {
field.Set(*mapValue)

default:
// look for supported struct parser
if structParser, found := validStructs[valueType]; found {
return structParser(&field, value, layout)
}

return fmt.Errorf("unsupported type %s.%s", valueType.PkgPath(), valueType.Name())
}

Expand Down
29 changes: 21 additions & 8 deletions example_test.go
Expand Up @@ -179,36 +179,49 @@ func ExampleReadEnvWithURL() {
//Output: https://images.cdn/
}

// MyField is an example type with a custom setter
type MyField string
// MyField1 is an example type with a custom setter
type MyField1 string

func (f *MyField) SetValue(s string) error {
func (f *MyField1) SetValue(s string) error {
if s == "" {
return fmt.Errorf("field value can't be empty")
}
*f = MyField("my field is: " + s)
*f = MyField1("my field is: " + s)
return nil
}

func (f MyField) String() string {
func (f MyField1) String() string {
return string(f)
}

// MyField2 is an example type with encoding.TextUnmarshaler implementation.
type MyField2 string

func (f *MyField2) UnmarshalText(p []byte) error {
if len(p) == 0 {
return fmt.Errorf("field value can't be empty")
}
*f = MyField2("my field is: " + string(p))
return nil
}

// Example_setter uses type with a custom setter to parse environment variable data
func Example_setter() {
type config struct {
Default string `env:"ONE"`
Custom MyField `env:"TWO"`
Default string `env:"ONE"`
Custom1 MyField1 `env:"TWO"`
Custom2 MyField2 `env:"THREE"`
}

var cfg config

os.Setenv("ONE", "test1")
os.Setenv("TWO", "test2")
os.Setenv("THREE", "test3")

cleanenv.ReadEnv(&cfg)
fmt.Printf("%+v\n", cfg)
//Output: {Default:test1 Custom:my field is: test2}
//Output: {Default:test1 Custom1:my field is: test2 Custom2:my field is: test3}
}

// ConfigUpdate is a type with a custom updater
Expand Down

0 comments on commit c35e574

Please sign in to comment.