From 2eec4b5daaf663d616d54199aa86a3056d5833cb Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Mon, 17 May 2021 10:25:09 -0700 Subject: [PATCH] Add regexp.Regexp support It's useful to support validated regular expressions in configuration files. Previously, we could represent these fields as raw pattern strings and compile them in application code. By supporting Regexp as a first-class field type, we get a smaller application code and more immediate error reporting without complicating the API. --- README.md | 7 ++++--- fig.go | 7 +++++++ fig_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c1e5b15..fb80df6 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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"` } } diff --git a/fig.go b/fig.go index 510a4f4..6281dc9 100644 --- a/fig.go +++ b/fig.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "reflect" + "regexp" "strconv" "strings" "time" @@ -289,6 +290,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()) } diff --git a/fig_test.go b/fig_test.go index 35c3826..8d53849 100644 --- a/fig_test.go +++ b/fig_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "reflect" + "regexp" "strings" "testing" "time" @@ -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]"` } @@ -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) @@ -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) @@ -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()