Skip to content

Commit

Permalink
Merge pull request #7 from jparise/regexp
Browse files Browse the repository at this point in the history
Add regexp.Regexp support
  • Loading branch information
kkyr authored Aug 4, 2021
2 parents f1d6c5f + a3581e5 commit 9422805
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 11 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ fig is a tiny library for loading an application's config file and its environme
- Define your **configuration**, **validations** and **defaults** in a single location
- Optionally **load from the environment** as well
- Only **3** external dependencies
- Full support for`time.Time` & `time.Duration`
- Full support for`time.Time`, `time.Duration` & `regexp.Regexp`
- Tiny API
- Decoders for `.yaml`, `.json` and `.toml` files

Expand Down Expand Up @@ -64,8 +64,9 @@ type Config struct {
Cleanup time.Duration `fig:"cleanup" default:"30m"`
}
Logger struct {
Level string `fig:"level" default:"info"`
Trace bool `fig:"trace"`
Level string `fig:"level" default:"info"`
Pattern *regexp.Regexp `fig:"pattern" default:".*"`
Trace bool `fig:"trace"`
}
}

Expand All @@ -75,7 +76,7 @@ func main() {
// handle your err

fmt.Printf("%+v\n", cfg)
// Output: {Build:2019-12-25 00:00:00 +0000 UTC Server:{Host:127.0.0.1 Ports:[8080] Cleanup:1h0m0s} Logger:{Level:warn Trace:true}}
// Output: {Build:2019-12-25 00:00:00 +0000 UTC Server:{Host:127.0.0.1 Ports:[8080] Cleanup:1h0m0s} Logger:{Level:warn Pattern:.* Trace:true}}
}
```

Expand Down
10 changes: 6 additions & 4 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,10 @@ See example below to help understand:
I interface{} `validate:"required"`
J interface{} `validate:"required"`
} `validate:"required"`
K *[]bool `validate:"required"`
L []uint `validate:"required"`
M *time.Time `validate:"required"`
K *[]bool `validate:"required"`
L []uint `validate:"required"`
M *time.Time `validate:"required"`
N *regexp.Regexp `validate:"required"`
}
var cfg Config
Expand All @@ -206,7 +207,7 @@ See example below to help understand:
err := fig.Load(&cfg)
fmt.Print(err)
// A: required, B: required, C: required, D: required, E: required, G: required, H.J: required, K: required, M: required
// A: required validation failed, B: required validation failed, C: required validation failed, D: required validation failed, E: required validation failed, G: required validation failed, H.J: required validation failed, K: required validation failed, M: required validation failed, N: required validation failed
Default
Expand All @@ -224,6 +225,7 @@ A default value can be set for the following types:
all basic types except bool and complex
time.Time
time.Duration
*regexp.Regexp
slices (of above types)
Successive elements of slice defaults should be separated by a comma. The entire slice can optionally be enclosed in square brackets:
Expand Down
1 change: 1 addition & 0 deletions examples/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ server:

logger:
level: debug
pattern: '[a-z]+'

certificate:
version: 1
Expand Down
6 changes: 5 additions & 1 deletion examples/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"fmt"
"regexp"
"time"

"github.com/kkyr/fig"
Expand All @@ -18,7 +19,8 @@ type Config struct {
WriteTimeout time.Duration `fig:"write_timeout" default:"30s"`
} `fig:"server"`
Logger struct {
Level string `fig:"level" default:"info"`
Level string `fig:"level" default:"info"`
Pattern *regexp.Regexp `fig:"pattern" default:".*"`
} `fig:"logger"`
Certificate struct {
Version int `fig:"version"`
Expand All @@ -40,6 +42,7 @@ func ExampleLoad() {
fmt.Println(cfg.Server.ReadTimeout)
fmt.Println(cfg.Server.WriteTimeout)
fmt.Println(cfg.Logger.Level)
fmt.Println(cfg.Logger.Pattern)
fmt.Println(cfg.Certificate.Version)
fmt.Println(cfg.Certificate.DNSNames)
fmt.Println(cfg.Certificate.Expiration.Format("2006-01-02"))
Expand All @@ -51,6 +54,7 @@ func ExampleLoad() {
// 1m0s
// 30s
// debug
// [a-z]+
// 1
// [kkyr kkyr.io]
// 2020-12-01
Expand Down
24 changes: 24 additions & 0 deletions fig.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -153,6 +154,7 @@ func (f *fig) decodeMap(m map[string]interface{}, result interface{}) error {
DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToTimeHookFunc(f.timeLayout),
stringToRegexpHookFunc(),
),
})
if err != nil {
Expand All @@ -161,6 +163,22 @@ func (f *fig) decodeMap(m map[string]interface{}, result interface{}) error {
return dec.Decode(m)
}

// stringToRegexpHookFunc returns a DecodeHookFunc that converts strings to regexp.Regexp.
func stringToRegexpHookFunc() mapstructure.DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(&regexp.Regexp{}) {
return data, nil
}
return regexp.Compile(data.(string))
}
}

// processCfg processes a cfg struct after it has been loaded from
// the config file, by validating required fields and setting defaults
// where applicable.
Expand Down Expand Up @@ -289,6 +307,12 @@ func (f *fig) setValue(fv reflect.Value, val string) error {
return err
}
fv.Set(reflect.ValueOf(t))
} else if _, ok := fv.Interface().(regexp.Regexp); ok {
re, err := regexp.Compile(val)
if err != nil {
return err
}
fv.Set(reflect.ValueOf(*re))
} else {
return fmt.Errorf("unsupported type %s", fv.Kind())
}
Expand Down
45 changes: 43 additions & 2 deletions fig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -221,8 +222,9 @@ func Test_fig_Load_Defaults(t *testing.T) {
Host string `fig:"host" default:"127.0.0.1"`
Ports []int `fig:"ports" default:"[80,443]"`
Logger struct {
LogLevel string `fig:"log_level" default:"info"`
Production bool `fig:"production"`
LogLevel string `fig:"log_level" default:"info"`
Pattern *regexp.Regexp `fig:"pattern" default:".*"`
Production bool `fig:"production"`
Metadata struct {
Keys []string `fig:"keys" default:"[ts]"`
}
Expand All @@ -236,6 +238,7 @@ func Test_fig_Load_Defaults(t *testing.T) {
want.Host = "0.0.0.0"
want.Ports = []int{80, 443}
want.Logger.LogLevel = "debug"
want.Logger.Pattern = regexp.MustCompile(".*")
want.Logger.Production = false
want.Logger.Metadata.Keys = []string{"ts"}
want.Application.BuildDate = time.Date(2020, 1, 1, 12, 0, 0, 0, time.UTC)
Expand Down Expand Up @@ -1014,6 +1017,35 @@ func Test_fig_setValue(t *testing.T) {
}
})

t.Run("regexp", func(t *testing.T) {
var re regexp.Regexp
fv := reflect.ValueOf(&re).Elem()

err := fig.setValue(fv, "[a-z]+")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}

want, err := regexp.Compile("[a-z]+")
if err != nil {
t.Fatalf("error parsing time: %v", err)
}

if re.String() != want.String() {
t.Fatalf("want %v, got %v", want, re)
}
})

t.Run("bad regexp", func(t *testing.T) {
var re regexp.Regexp
fv := reflect.ValueOf(&re).Elem()

err := fig.setValue(fv, "[a-")
if err == nil {
t.Fatalf("expected err")
}
})

t.Run("interface returns error", func(t *testing.T) {
var i interface{}
fv := reflect.ValueOf(i)
Expand Down Expand Up @@ -1089,6 +1121,15 @@ func Test_fig_setSlice(t *testing.T) {
},
Val: "[2019-12-25T10:30:30Z,2020-01-01T00:00:00Z]",
},
{
Name: "regexps",
InSlice: &[]*regexp.Regexp{},
WantSlice: &[]*regexp.Regexp{
regexp.MustCompile("[a-z]+"),
regexp.MustCompile(".*"),
},
Val: "[[a-z]+,.*]",
},
} {
t.Run(tc.Val, func(t *testing.T) {
in := reflect.ValueOf(tc.InSlice).Elem()
Expand Down
17 changes: 17 additions & 0 deletions util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fig
import (
"path/filepath"
"reflect"
"regexp"
"testing"
"time"
)
Expand Down Expand Up @@ -137,6 +138,22 @@ func Test_isZero(t *testing.T) {
}
})

t.Run("zero regexp is zero", func(t *testing.T) {
var re *regexp.Regexp

if isZero(reflect.ValueOf(re)) == false {
t.Fatalf("isZero == false")
}
})

t.Run("non-zero regexp is not zero", func(t *testing.T) {
re := regexp.MustCompile(".*")

if isZero(reflect.ValueOf(re)) == true {
t.Fatalf("isZero == true")
}
})

t.Run("reflect invalid is zero", func(t *testing.T) {
var x interface{}

Expand Down

0 comments on commit 9422805

Please sign in to comment.