diff --git a/bootstrap/app/conf/conf.go b/bootstrap/app/conf/conf.go index bcc5f2b..f94105a 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[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))), +} 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() { 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= diff --git a/validate/env.go b/validate/env.go new file mode 100644 index 0000000..1975f28 --- /dev/null +++ b/validate/env.go @@ -0,0 +1,90 @@ +package validate + +import ( + "fmt" + "os" + "reflect" + "strconv" +) + +type SupportedEnvTypes interface { + ~int | ~float64 | ~bool | ~string +} + +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 result, nil +} + +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 Env[T SupportedEnvTypes](key string, rulesSets []RuleSet, defaultValue ...T) T { + + str := os.Getenv(key) + + val, err := coerceString[T](str) + + 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) + panic(fmt.Sprintf("Error parsing env %s: %s", key, msg)) + } + } + + return val +} diff --git a/validate/env_test.go b/validate/env_test.go new file mode 100644 index 0000000..b60feee --- /dev/null +++ b/validate/env_test.go @@ -0,0 +1,59 @@ +package validate + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEmptyEnv(t *testing.T) { + assert.Panics(t, func() { + _ = Env[string]("TEST", Rules(Required)) + }) + + assert.Panics(t, func() { + os.Setenv("TEST", "") + _ = Env[string]("TEST", Rules(Required)) + }) +} + +func TestReturnsValue(t *testing.T) { + + os.Setenv("TEST", "value") + + val := Env[string]("TEST", Rules(Required)) + assert.Equal(t, val, "value") +} + +func TestDefault(t *testing.T) { + val := Env[string]("TEST2", Rules(Required), "hello") + assert.Equal(t, "hello", val) + + 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) { + 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) +}