From 880da15060f432aacdc2bc0da592a6cc1a5b5392 Mon Sep 17 00:00:00 2001 From: oudwins Date: Sat, 22 Jun 2024 07:34:55 +0200 Subject: [PATCH 1/7] feat: env validation --- validate/env.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 validate/env.go diff --git a/validate/env.go b/validate/env.go new file mode 100644 index 0000000..52d4a36 --- /dev/null +++ b/validate/env.go @@ -0,0 +1,55 @@ +package validate + +import ( + "fmt" + "os" +) + +type EnvValidator struct { + key string + defaultValue string + rules []RuleSet +} + +func Env(key string, rules ...RuleSet) *EnvValidator { + ruleSets := make([]RuleSet, len(rules)) + for i := 0; i < len(ruleSets); i++ { + ruleSets[i] = rules[i] + } + return &EnvValidator{rules: ruleSets, key: key} +} + +func (e *EnvValidator) Default(defaultValue string) *EnvValidator { + e.defaultValue = defaultValue + return e +} + +func (e *EnvValidator) Validate() string { + var fieldName, fieldValue string + val := os.Getenv(e.key) + + fieldName = e.key + fieldValue = val + + for _, set := range e.rules { + set.FieldValue = fieldValue + set.FieldName = fieldName + if !set.ValidateFunc(set) { + msg := set.MessageFunc(set) + if len(set.ErrorMessage) > 0 { + msg = set.ErrorMessage + } + if e.defaultValue == "" { + panic(fmt.Sprintf("Error parsing env %s: %s", e.key, msg)) + } else { + return e.defaultValue + } + } + } + + if val == "" { + return e.defaultValue + } + + return val +} From 6a251cd096037f71517350cd185e9b1038a66b10 Mon Sep 17 00:00:00 2001 From: oudwins Date: Sat, 22 Jun 2024 07:35:10 +0200 Subject: [PATCH 2/7] test: env validation tests --- validate/env_test.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 validate/env_test.go diff --git a/validate/env_test.go b/validate/env_test.go new file mode 100644 index 0000000..3ac0bec --- /dev/null +++ b/validate/env_test.go @@ -0,0 +1,37 @@ +package validate + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEmptyEnv(t *testing.T) { + assert.Panics(t, func() { + _ = Env("TEST", Required).Validate() + }) + + assert.Panics(t, func() { + os.Setenv("TEST", "") + + _ = Env("TEST", Required).Validate() + }) +} + +func TestReturnsValue(t *testing.T) { + + os.Setenv("TEST", "value") + + val := Env("TEST", Required).Validate() + assert.Equal(t, val, "value") +} + +func TestDefault(t *testing.T) { + val := Env("TEST2").Default("hello").Validate() + assert.Equal(t, "hello", val) + + os.Setenv("TEST2", "world") + val = Env("TEST2").Default("hello").Validate() + assert.Equal(t, "world", val) +} From b8e91f8ba0493c69b5f4dd59dc1a6df0d86b44d5 Mon Sep 17 00:00:00 2001 From: oudwins Date: Sat, 22 Jun 2024 07:36:50 +0200 Subject: [PATCH 3/7] feat: example usage --- bootstrap/app/conf/conf.go | 14 ++++++++++++++ bootstrap/cmd/app/main.go | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/bootstrap/app/conf/conf.go b/bootstrap/app/conf/conf.go index bcc5f2b..676fcc4 100644 --- a/bootstrap/app/conf/conf.go +++ b/bootstrap/app/conf/conf.go @@ -1,3 +1,17 @@ package conf +import ( + v "github.com/anthdm/superkit/validate" +) + // Application config + +var Env = struct { + SUPERKIT_ENV string + HTTP_LISTEN_ADDR string + SUPERKIT_SECRET string +}{ + SUPERKIT_ENV: v.Env("SUPERKIT_ENV", v.In([]string{"development", "staging", "production"})).Default("development").Validate(), + HTTP_LISTEN_ADDR: v.Env("HTTP_LISTEN_ADDR", v.Min(3)).Default(":3000").Validate(), + SUPERKIT_SECRET: v.Env("SUPERKIT_SECRET", v.Required, v.Min(32)).Validate(), +} diff --git a/bootstrap/cmd/app/main.go b/bootstrap/cmd/app/main.go index 40b33ab..5918798 100644 --- a/bootstrap/cmd/app/main.go +++ b/bootstrap/cmd/app/main.go @@ -2,6 +2,7 @@ package main import ( "AABBCCDD/app" + "AABBCCDD/app/conf" "AABBCCDD/public" "fmt" "log" @@ -34,7 +35,7 @@ func main() { app.InitializeRoutes(router) app.RegisterEvents() - listenAddr := os.Getenv("HTTP_LISTEN_ADDR") + listenAddr := conf.Env.HTTP_LISTEN_ADDR // In development link the full Templ proxy url. url := "http://localhost:7331" if kit.IsProduction() { From 4248c66b0af5bb1f152707d02086ffad9f8c2973 Mon Sep 17 00:00:00 2001 From: oudwins Date: Sat, 22 Jun 2024 07:37:07 +0200 Subject: [PATCH 4/7] chore: updated go sum --- go.sum | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 go.sum diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1f527fe --- /dev/null +++ b/go.sum @@ -0,0 +1,20 @@ +github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U= +github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg= +github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 01e7522b942dc31d3affc9c43471f135994f2370 Mon Sep 17 00:00:00 2001 From: oudwins Date: Sun, 23 Jun 2024 19:52:42 +0200 Subject: [PATCH 5/7] feat: new implementation that allows for all primitive types to be converted --- validate/env.go | 95 ++++++++++++++++++++++++++++++-------------- validate/env_test.go | 29 +++++++++++--- 2 files changed, 88 insertions(+), 36 deletions(-) diff --git a/validate/env.go b/validate/env.go index 52d4a36..1975f28 100644 --- a/validate/env.go +++ b/validate/env.go @@ -3,53 +3,88 @@ package validate import ( "fmt" "os" + "reflect" + "strconv" ) -type EnvValidator struct { - key string - defaultValue string - rules []RuleSet +type SupportedEnvTypes interface { + ~int | ~float64 | ~bool | ~string } -func Env(key string, rules ...RuleSet) *EnvValidator { - ruleSets := make([]RuleSet, len(rules)) - for i := 0; i < len(ruleSets); i++ { - ruleSets[i] = rules[i] +func coerceString[T SupportedEnvTypes](val string) (T, error) { + var result T + + switch any(result).(type) { + case int: + var tmp int + tmp, err := strconv.Atoi(val) + if err != nil { + return result, err + } + + result = any(tmp).(T) + case float64: + tmp, err := strconv.ParseFloat(val, 64) + if err != nil { + return result, err + } + result = any(tmp).(T) + + case bool: + tmp, err := strconv.ParseBool(val) + if err != nil { + return result, err + } + result = any(tmp).(T) + case string: + result = any(val).(T) + default: + return result, fmt.Errorf("unsupported type: %T", result) } - return &EnvValidator{rules: ruleSets, key: key} + + return result, nil } -func (e *EnvValidator) Default(defaultValue string) *EnvValidator { - e.defaultValue = defaultValue - return e +func isZeroValue(x any) bool { + if x == nil { + return true + } + + v := reflect.ValueOf(x) + if !v.IsValid() { + return true + } + + // Check if the value is the zero value for its type + zeroValue := reflect.Zero(v.Type()) + return reflect.DeepEqual(v.Interface(), zeroValue.Interface()) } -func (e *EnvValidator) Validate() string { - var fieldName, fieldValue string - val := os.Getenv(e.key) +func Env[T SupportedEnvTypes](key string, rulesSets []RuleSet, defaultValue ...T) T { + + str := os.Getenv(key) - fieldName = e.key - fieldValue = val + val, err := coerceString[T](str) - for _, set := range e.rules { + if err != nil || isZeroValue(val) { + if len(defaultValue) > 0 { + return defaultValue[0] + } else { + panic(fmt.Errorf("failed to parse env %s: %v", key, err)) + } + } + + fieldName := key + fieldValue := val + + for _, set := range rulesSets { set.FieldValue = fieldValue set.FieldName = fieldName if !set.ValidateFunc(set) { msg := set.MessageFunc(set) - if len(set.ErrorMessage) > 0 { - msg = set.ErrorMessage - } - if e.defaultValue == "" { - panic(fmt.Sprintf("Error parsing env %s: %s", e.key, msg)) - } else { - return e.defaultValue - } + panic(fmt.Sprintf("Error parsing env %s: %s", key, msg)) } } - if val == "" { - return e.defaultValue - } - return val } diff --git a/validate/env_test.go b/validate/env_test.go index 3ac0bec..f32df1e 100644 --- a/validate/env_test.go +++ b/validate/env_test.go @@ -9,13 +9,12 @@ import ( func TestEmptyEnv(t *testing.T) { assert.Panics(t, func() { - _ = Env("TEST", Required).Validate() + _ = Env[string]("TEST", Rules(Required)) }) assert.Panics(t, func() { os.Setenv("TEST", "") - - _ = Env("TEST", Required).Validate() + _ = Env[string]("TEST", Rules(Required)) }) } @@ -23,15 +22,33 @@ func TestReturnsValue(t *testing.T) { os.Setenv("TEST", "value") - val := Env("TEST", Required).Validate() + val := Env[string]("TEST", Rules(Required)) assert.Equal(t, val, "value") } func TestDefault(t *testing.T) { - val := Env("TEST2").Default("hello").Validate() + val := Env[string]("TEST2", Rules(Required), "hello") assert.Equal(t, "hello", val) os.Setenv("TEST2", "world") - val = Env("TEST2").Default("hello").Validate() + val = Env[string]("TEST2", Rules(Required), "hello") assert.Equal(t, "world", val) } + +func TestInt(t *testing.T) { + os.Setenv("TEST", "1") + val := Env[int]("TEST", Rules(LT(2))) + assert.Equal(t, 1, val) +} + +func TestBool(t *testing.T) { + os.Setenv("TEST", "true") + val := Env[bool]("TEST", Rules(EQ(true))) + assert.Equal(t, true, val) +} + +func TestFloat(t *testing.T) { + os.Setenv("TEST", "1.1") + val := Env[float64]("TEST", Rules(GT(1.0))) + assert.Equal(t, 1.1, val) +} From 3f3ca35ae6f232a0a40933ad367cca30b21fb978 Mon Sep 17 00:00:00 2001 From: oudwins Date: Sun, 23 Jun 2024 19:55:45 +0200 Subject: [PATCH 6/7] test: additional default test --- validate/env_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/validate/env_test.go b/validate/env_test.go index f32df1e..b60feee 100644 --- a/validate/env_test.go +++ b/validate/env_test.go @@ -33,6 +33,11 @@ func TestDefault(t *testing.T) { os.Setenv("TEST2", "world") val = Env[string]("TEST2", Rules(Required), "hello") assert.Equal(t, "world", val) + + assert.Panics(t, func() { + os.Setenv("TEST2", "1") + _ = Env[string]("TEST2", Rules(Min(4))) + }) } func TestInt(t *testing.T) { From 00b500db604da58c22a88efab12d8243916e1bdf Mon Sep 17 00:00:00 2001 From: oudwins Date: Sun, 23 Jun 2024 20:10:33 +0200 Subject: [PATCH 7/7] refactor: updated example config --- bootstrap/app/conf/conf.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bootstrap/app/conf/conf.go b/bootstrap/app/conf/conf.go index 676fcc4..f94105a 100644 --- a/bootstrap/app/conf/conf.go +++ b/bootstrap/app/conf/conf.go @@ -11,7 +11,7 @@ var Env = struct { HTTP_LISTEN_ADDR string SUPERKIT_SECRET string }{ - SUPERKIT_ENV: v.Env("SUPERKIT_ENV", v.In([]string{"development", "staging", "production"})).Default("development").Validate(), - HTTP_LISTEN_ADDR: v.Env("HTTP_LISTEN_ADDR", v.Min(3)).Default(":3000").Validate(), - SUPERKIT_SECRET: v.Env("SUPERKIT_SECRET", v.Required, v.Min(32)).Validate(), + SUPERKIT_ENV: v.Env[string]("SUPERKIT_ENV", v.Rules(v.In([]string{"development", "staging", "production"})), "development"), + HTTP_LISTEN_ADDR: v.Env[string]("HTTP_LISTEN_ADDR", v.Rules(v.Min(3)), ":3000"), + SUPERKIT_SECRET: v.Env[string]("SUPERKIT_SECRET", v.Required, v.Rules(v.Min(32))), }