feat: add basic config loader
This commit is contained in:
108
config.go
Normal file
108
config.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package yourconfig
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ettle/strcase"
|
||||
)
|
||||
|
||||
func MustLoad[T any]() T {
|
||||
output, err := Load[T]()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("must load: %s", err.Error()))
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func Load[T any]() (T, error) {
|
||||
var cfg T
|
||||
|
||||
v := reflect.ValueOf(&cfg).Elem()
|
||||
t := v.Type()
|
||||
|
||||
errs := make([]error, 0)
|
||||
|
||||
OUTER:
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
tagStr := field.Tag.Get("conf")
|
||||
if tagStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
singleValue := make([]string, 0)
|
||||
options := make(map[string]string, 0)
|
||||
|
||||
values := strings.SplitSeq(tagStr, ",")
|
||||
for value := range values {
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
key, val, ok := strings.Cut(strings.TrimSpace(value), ":")
|
||||
if ok {
|
||||
options[key] = val
|
||||
} else {
|
||||
singleValue = append(singleValue, value)
|
||||
}
|
||||
}
|
||||
|
||||
var tag tag
|
||||
if len(singleValue) == 0 {
|
||||
tag.Env = strcase.ToSNAKE(field.Name)
|
||||
} else {
|
||||
tag.Env = singleValue[0] // We always count the first value as the name, if set
|
||||
}
|
||||
|
||||
for _, option := range singleValue {
|
||||
switch option {
|
||||
case "required":
|
||||
tag.Required = true
|
||||
}
|
||||
}
|
||||
|
||||
for key, val := range options {
|
||||
switch key {
|
||||
case "required":
|
||||
required, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("field: %s (key: %s), err: %w", field.Name, key, err))
|
||||
continue OUTER
|
||||
}
|
||||
|
||||
tag.Required = required
|
||||
}
|
||||
}
|
||||
|
||||
valueStr := os.Getenv(tag.Env)
|
||||
if valueStr == "" && tag.Required {
|
||||
errs = append(errs, fmt.Errorf("field: %s (env=%s) is not set and is required", field.Name, tag.Env))
|
||||
continue OUTER
|
||||
}
|
||||
|
||||
fieldValue := v.Field(i)
|
||||
if !fieldValue.CanSet() {
|
||||
errs = append(errs, fmt.Errorf("field: %s is not settable", field.Name))
|
||||
continue OUTER
|
||||
}
|
||||
|
||||
fieldValue.SetString(valueStr)
|
||||
}
|
||||
|
||||
if err := errors.Join(errs...); err != nil {
|
||||
return cfg, fmt.Errorf("config failed: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
type tag struct {
|
||||
Env string
|
||||
Required bool
|
||||
}
|
168
config_test.go
Normal file
168
config_test.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package yourconfig_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kjuulh/yourconfig"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
t.Run("no types", func(t *testing.T) {
|
||||
type Config struct {
|
||||
SomeItem string
|
||||
someOtherItem string
|
||||
someBool bool
|
||||
}
|
||||
|
||||
val, err := yourconfig.Load[Config]()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Zero(t, val)
|
||||
})
|
||||
|
||||
t.Run("default tag, nothing set, no env set", func(t *testing.T) {
|
||||
type Config struct {
|
||||
SomeItem string `conf:""`
|
||||
someOtherItem string
|
||||
someBool bool
|
||||
}
|
||||
|
||||
val, err := yourconfig.Load[Config]()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Zero(t, val)
|
||||
})
|
||||
|
||||
t.Run("default tag (required=true), nothing set, no env set, err", func(t *testing.T) {
|
||||
type Config struct {
|
||||
SomeItem string `conf:"required:true"`
|
||||
someOtherItem string
|
||||
someBool bool
|
||||
}
|
||||
|
||||
val, err := yourconfig.Load[Config]()
|
||||
require.Error(t, err)
|
||||
require.Zero(t, val)
|
||||
|
||||
assert.Equal(t, "config failed: field: SomeItem (env=SOME_ITEM) is not set and is required", err.Error())
|
||||
})
|
||||
|
||||
t.Run("default tag (required=false), nothing set, no env set no error", func(t *testing.T) {
|
||||
type Config struct {
|
||||
SomeItem string `conf:"required:false"`
|
||||
someOtherItem string
|
||||
someBool bool
|
||||
}
|
||||
|
||||
val, err := yourconfig.Load[Config]()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Zero(t, val)
|
||||
})
|
||||
|
||||
t.Run("env tag nothing set, no env set, no error", func(t *testing.T) {
|
||||
type Config struct {
|
||||
SomeItem string `conf:"SOME_ITEM"`
|
||||
someOtherItem string
|
||||
someBool bool
|
||||
}
|
||||
|
||||
val, err := yourconfig.Load[Config]()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Zero(t, val)
|
||||
})
|
||||
|
||||
t.Run("default tag (required=true), nothing set, no env set, err", func(t *testing.T) {
|
||||
type Config struct {
|
||||
SomeItem string `conf:"SOME_ITEM,required:true"`
|
||||
someOtherItem string
|
||||
someBool bool
|
||||
}
|
||||
|
||||
val, err := yourconfig.Load[Config]()
|
||||
require.Error(t, err)
|
||||
require.Zero(t, val)
|
||||
|
||||
assert.Equal(t, "config failed: field: SomeItem (env=SOME_ITEM) is not set and is required", err.Error())
|
||||
})
|
||||
|
||||
t.Run("default tag (required), nothing set, no env set, err", func(t *testing.T) {
|
||||
type Config struct {
|
||||
SomeItem string `conf:"SOME_ITEM,required"`
|
||||
someOtherItem string
|
||||
someBool bool
|
||||
}
|
||||
|
||||
val, err := yourconfig.Load[Config]()
|
||||
require.Error(t, err)
|
||||
require.Zero(t, val)
|
||||
|
||||
assert.Equal(t, "config failed: field: SomeItem (env=SOME_ITEM) is not set and is required", err.Error())
|
||||
})
|
||||
|
||||
t.Run("default tag private, trying to set, err", func(t *testing.T) {
|
||||
type Config struct {
|
||||
SomeItem string
|
||||
someOtherItem string `conf:"required:true"`
|
||||
someBool bool
|
||||
}
|
||||
|
||||
t.Setenv("SOME_OTHER_ITEM", "unsettable")
|
||||
|
||||
val, err := yourconfig.Load[Config]()
|
||||
require.Error(t, err)
|
||||
require.Zero(t, val)
|
||||
|
||||
assert.Equal(t, "config failed: field: someOtherItem is not settable", err.Error())
|
||||
})
|
||||
|
||||
t.Run("env tag and env set, no error", func(t *testing.T) {
|
||||
type Config struct {
|
||||
SomeItem string `conf:"required:true"`
|
||||
someOtherItem string
|
||||
someBool bool
|
||||
}
|
||||
|
||||
t.Setenv("SOME_ITEM", "some-item")
|
||||
|
||||
val, err := yourconfig.Load[Config]()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "some-item", val.SomeItem)
|
||||
})
|
||||
|
||||
t.Run("env tag (different name) and env set, no error", func(t *testing.T) {
|
||||
type Config struct {
|
||||
SomeItem string `conf:"DIFFERENT_NAME,required:true"`
|
||||
someOtherItem string
|
||||
someBool bool
|
||||
}
|
||||
|
||||
t.Setenv("DIFFERENT_NAME", "some-item")
|
||||
|
||||
val, err := yourconfig.Load[Config]()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "some-item", val.SomeItem)
|
||||
})
|
||||
|
||||
t.Run("multiple env tag and env set, no error", func(t *testing.T) {
|
||||
type Config struct {
|
||||
SomeItem string `conf:"required:true"`
|
||||
SomeOtherItem string `conf:"required:true"`
|
||||
someBool bool
|
||||
}
|
||||
|
||||
t.Setenv("SOME_ITEM", "some-item")
|
||||
t.Setenv("SOME_OTHER_ITEM", "some-other-item")
|
||||
|
||||
val, err := yourconfig.Load[Config]()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "some-item", val.SomeItem)
|
||||
assert.Equal(t, "some-other-item", val.SomeOtherItem)
|
||||
})
|
||||
}
|
14
go.mod
Normal file
14
go.mod
Normal file
@@ -0,0 +1,14 @@
|
||||
module github.com/kjuulh/yourconfig
|
||||
|
||||
go 1.24.5
|
||||
|
||||
require (
|
||||
github.com/ettle/strcase v0.2.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
12
go.sum
Normal file
12
go.sum
Normal file
@@ -0,0 +1,12 @@
|
||||
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/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
|
||||
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
|
||||
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.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=
|
Reference in New Issue
Block a user