Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@

6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
except as required for reasonable and customary use in helpribing the
origin of the Work and reproducing the content of the NOTICE file.

7. Disclaimer of Warranty. Unless required by applicable law or
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ import (

// Database config struct.
type DBConfig struct {
Host string `validate:"required"` // this field is required
Host string `required:""` // this field is required
Port int `default:"5432" short:"p"` // specify "short" flag
User string `default:"postgres"`
Password string `default:"postgres"`
Expand All @@ -102,17 +102,17 @@ type DBConfig struct {
type ServerConfig struct {
ServerName string `name:"hostname"` // rename this field in the config
ReadTimeout int // no struct tags are required
ListenIP net.IP `dec:"IP address on which to listen" default:"127.0.0.1"`
ListenIP net.IP `help:"IP address on which to listen" default:"127.0.0.1"`
ListenPort uint `default:"8080"`
}

type Config struct {
ServerConfig // Embedded struct
DB DBConfig // Sub-config in `DB` struct
Theme theme.Config // Sub-config from "theme" package
CalculatedField string `ignore:"true"` // ignore this field
CalculatedField string `ignore:""` // ignore this field
LogLevel string `default:"info" enum:"debug,info,warn,error"` // enum field
Conf co.ConfigFile `desc:"Configuration file"` // config file
Conf co.ConfigFile `help:"Configuration file"` // config file
}

func main() {
Expand Down
3 changes: 1 addition & 2 deletions config_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
"reflect"
"strings"

"github.com/fatih/structtag"
"github.com/iancoleman/strcase"
"github.com/spf13/pflag"
"gopkg.in/yaml.v3"
Expand All @@ -36,7 +35,7 @@ import (
// setConfigFile checks for a field of type File in the config struct and sets
// the configFile.Value pointer to its address
func (c *configurer) setConfigFile() {
c.visitFields(c.config, func(f reflect.StructField, _ *structtag.Tags, v reflect.Value, _ []string) (stop bool) {
c.visitFields(c.config, func(f reflect.StructField, _ *reflect.StructTag, v reflect.Value, _ []string) (stop bool) {
if v.Elem().Type() == configFileType {
if c.configFile.Value != nil {
panic("ConfigFile already set to " + *c.configFile.Value)
Expand Down
61 changes: 22 additions & 39 deletions configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"reflect"
"strings"

"github.com/fatih/structtag"
"github.com/iancoleman/strcase"
"github.com/spf13/pflag"
)
Expand Down Expand Up @@ -142,7 +141,7 @@ func Configure[T any](opts *Options) *T {
// setFromEnv sets configuration values from environment
func (c *configurer) setFromEnv(s any, fs *pflag.FlagSet) {

c.visitFields(s, func(f reflect.StructField, tags *structtag.Tags, v reflect.Value, ancestors []string) (stop bool) {
c.visitFields(s, func(f reflect.StructField, tags *reflect.StructTag, v reflect.Value, ancestors []string) (stop bool) {
fName := fieldNameToConfigName(f.Name, tags, ancestors)
envVal := os.Getenv(
fmt.Sprintf("%s%s", c.opts.EnvPrefix, strcase.ToScreamingSnake(fName)),
Expand All @@ -162,45 +161,32 @@ func (c *configurer) loadFlags(s any, fl *pflag.FlagSet) []func() {

setters := []func(){}

c.visitFields(s, func(f reflect.StructField, tags *structtag.Tags, v reflect.Value, ancestors []string) (stop bool) {
c.visitFields(s, func(f reflect.StructField, tags *reflect.StructTag, v reflect.Value, ancestors []string) (stop bool) {

fName := fieldNameToConfigName(f.Name, tags, ancestors)
descTag, err := tags.Get("desc")
if err != nil {
descTag = &structtag.Tag{
Key: "desc",
Name: strings.ReplaceAll(
fieldNameToConfigName(f.Name, tags, ancestors), "_", " ",
),
}
}
shortTag, _ := tags.Get("short")
if shortTag == nil {
shortTag = &structtag.Tag{}
}
noDefault := false
defaultTag, _ := tags.Get("default")
if defaultTag == nil {
noDefault = true
defaultTag = &structtag.Tag{}
helpTag, ok := tags.Lookup("help")
if !ok {
helpTag = strings.ReplaceAll(fieldNameToConfigName(f.Name, tags, ancestors), "_", " ")
}
shortTag := tags.Get("short")
defaultTag, ok := tags.Lookup("default")
noDefault := !ok

// Special case for ConfigFile field
if v.Elem().Type() == configFileType {
c.configFile.Flag = fName
c.configFile.Short = shortTag.Value()
c.configFile.Short = shortTag
}

desc := descTag.Value()
enumProvided := false
if enums, _ := tags.Get("enum"); enums != nil && enums.Value() != "" {
desc += fmt.Sprintf(" (%s)", strings.Replace(enums.Value(), ",", "|", -1))
if enums := tags.Get("enum"); enums != "" {
helpTag += fmt.Sprintf(" (%s)", strings.Replace(enums, ",", "|", -1))
enumProvided = true
}
addToFlagSet(v.Type(), enumProvided, fl, fName, shortTag.Value(), defaultTag.Value(), desc)
addToFlagSet(v.Type(), enumProvided, fl, fName, shortTag, defaultTag, helpTag)

// Hide hidden flags
if _, err := tags.Get("hidden"); err == nil {
if _, ok := tags.Lookup("hidden"); ok {
fl.MarkHidden(fName)
}

Expand All @@ -224,7 +210,7 @@ func (c *configurer) loadFlags(s any, fl *pflag.FlagSet) []func() {

// visitFields visits the fields of the config struct and calls the
// provided function on each field.
func (c *configurer) visitFields(s any, f func(reflect.StructField, *structtag.Tags, reflect.Value, []string) bool, ancestors []string) bool {
func (c *configurer) visitFields(s any, f func(reflect.StructField, *reflect.StructTag, reflect.Value, []string) bool, ancestors []string) bool {
v := reflect.ValueOf(s).Elem()
t := v.Type()

Expand All @@ -235,13 +221,10 @@ func (c *configurer) visitFields(s any, f func(reflect.StructField, *structtag.T
}

// Parse tags
tags, err := structtag.Parse(string(t.Field(i).Tag))
if err != nil {
panic(fmt.Sprintf("error parsing field %s tags: %s", t.Field(i).Name, err.Error()))
}
tags := t.Field(i).Tag

// Skip any fields tagged with ignore:""
if _, err := tags.Get("ignore"); err == nil {
if _, ok := tags.Lookup("ignore"); ok {
continue
}

Expand All @@ -258,8 +241,8 @@ func (c *configurer) visitFields(s any, f func(reflect.StructField, *structtag.T
if t.Field(i).Type.Kind() == reflect.Struct {
fld := v.Field(i).Addr().Interface()
fName := t.Field(i).Name
if name, err := tags.Get("name"); err == nil {
fName = name.Value()
if name, ok := tags.Lookup("name"); ok {
fName = name
}

var newAncestors []string
Expand All @@ -275,7 +258,7 @@ func (c *configurer) visitFields(s any, f func(reflect.StructField, *structtag.T
}

// Call function on field and stop if it returns true
if f(t.Field(i), tags, v.Field(i).Addr(), ancestors) {
if f(t.Field(i), &tags, v.Field(i).Addr(), ancestors) {
return true
}
}
Expand All @@ -284,9 +267,9 @@ func (c *configurer) visitFields(s any, f func(reflect.StructField, *structtag.T

// fieldNameToConfigName converts a struct field name and its ancestor path to
// its flag name
func fieldNameToConfigName(name string, tags *structtag.Tags, ancestors []string) string {
if nm, err := tags.Get("name"); err == nil && nm.Value() != "" {
name = nm.Value()
func fieldNameToConfigName(name string, tags *reflect.StructTag, ancestors []string) string {
if nm, ok := tags.Lookup("name"); ok && nm != "" {
name = nm
}
return strings.Join(append(ancestors, strcase.ToSnake(name)), "_")
}
Expand Down
76 changes: 38 additions & 38 deletions configure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,44 +32,44 @@ import (
)

type SubConfig struct {
StateFile string `desc:"File in which to store lock state" short:"s"`
DefaultLockTimeout time.Duration `desc:"Lock timeout to use when loading locks from state file on startup" default:"10m" short:"d"`
NoClearOnDisconnect bool `desc:"Do not clear locks on client disconnect" default:"false" short:"c"`
ReqInt int `desc:"Required int"`
FooSeconds uint `desc:"Something" default:"10" short:"f"`
FooInt uint32 `desc:"Something" default:"100" short:"o"`
FooInts []uint `desc:"Something list of ints" default:"100,200,30"`
StateFile string `help:"File in which to store lock state" short:"s"`
DefaultLockTimeout time.Duration `help:"Lock timeout to use when loading locks from state file on startup" default:"10m" short:"d"`
NoClearOnDisconnect bool `help:"Do not clear locks on client disconnect" default:"false" short:"c"`
ReqInt int `help:"Required int"`
FooSeconds uint `help:"Something" default:"10" short:"f"`
FooInt uint32 `help:"Something" default:"100" short:"o"`
FooInts []uint `help:"Something list of ints" default:"100,200,30"`
}

type OtherSubConfig struct {
SubFooString string `desc:"Something" default:"here"`
SubFooString string `help:"Something" default:"here"`
}

type TestConfig struct {
SubConfig
IgnoredField string `ignore:""`
Bool bool `desc:"Bool thing" default:"false"`
KeepaliveInterval time.Duration `desc:"Interval at which to send keepalive pings to client" default:"60s" short:"k"`
KeepaliveTimeout time.Duration `desc:"Wait this duration for the ping ack before assuming the connection is dead" default:"5s" short:"t"`
LockGcInterval time.Duration `desc:"Interval at which to garbage collect unused locks." default:"30m" short:"g"`
OtherSubConfig
LockGcMinIdle time.Duration `desc:"Minimum time a lock has to be idle (no unlocks or locks) before being considered for garbage collection" default:"5m" short:"m"`
ListenAddress string `desc:"Address (host:port) at which to listen" default:"localhost:3144" short:"l"`
LogLevel slog.Level `desc:"Log level" default:"info" short:"v"`
IgnoredField string `ignore:""`
Bool bool `help:"Bool thing" default:"false"`
KeepaliveInterval time.Duration `help:"Interval at which to send keepalive pings to client" default:"60s" short:"k"`
KeepaliveTimeout time.Duration `help:"Wait this duration for the ping ack before assuming the connection is dead" default:"5s" short:"t"`
LockGcInterval time.Duration `help:"Interval at which to garbage collect unused locks." default:"30m" short:"g"`
LockGcMinIdle time.Duration `help:"Minimum time a lock has to be idle (no unlocks or locks) before being considered for garbage collection" default:"5m" short:"m"`
ListenAddress string `help:"Address (host:port) at which to listen" default:"localhost:3144" short:"l"`
LogLevel slog.Level `help:"Log level" default:"info" short:"v"`
}

type TestConfigFileStruct struct {
CoolFile co.ConfigFile `desc:"Configuration file"`
CoolFile co.ConfigFile `help:"Configuration file"`
TestConfig
}

type TestNestedConfig struct {
CoolFile co.ConfigFile `desc:"Configuration file"`
SSlice []string `desc:"Slice of strings" default:"a,b,c"`
MyMap map[string]string `desc:"Map of strings"`
NameAgeMap map[string]int `desc:"Map of ages"`
HiddenFlag string `desc:"hidden flag" default:"hidden" hidden:"true"`
MyEnum string `desc:"My enum" enum:"a,b,c" default:"a"`
CoolFile co.ConfigFile `help:"Configuration file"`
SSlice []string `help:"Slice of strings" default:"a,b,c"`
MyMap map[string]string `help:"Map of strings"`
NameAgeMap map[string]int `help:"Map of ages"`
HiddenFlag string `help:"hidden flag" default:"hidden" hidden:"true"`
MyEnum string `help:"My enum" enum:"a,b,c" default:"a"`
OS OtherSubConfig
Sub SubConfig
}
Expand Down Expand Up @@ -564,13 +564,13 @@ func TestSubConfig_GivenNameEnv(t *testing.T) {

func TestNilPtrs_True(t *testing.T) {
type TConf struct {
PString *string `desc:"Pointer to string"`
PLogLevel *slog.Level `desc:"Pointer to log level" default:"debug"`
PInt *int `desc:"Pointer to int"`
PInts *[]int `desc:"Pointer to int slice"`
PIntsDef *[]int `desc:"Pointer to int slice" default:"1,3,4"`
PStrings *[]string `desc:"Pointer to string slice"`
PStringsDef *[]string `desc:"Pointer to string slice" default:"a,b,c"`
PString *string `help:"Pointer to string"`
PLogLevel *slog.Level `help:"Pointer to log level" default:"debug"`
PInt *int `help:"Pointer to int"`
PInts *[]int `help:"Pointer to int slice"`
PIntsDef *[]int `help:"Pointer to int slice" default:"1,3,4"`
PStrings *[]string `help:"Pointer to string slice"`
PStringsDef *[]string `help:"Pointer to string slice" default:"a,b,c"`
}

var conf *TConf
Expand Down Expand Up @@ -603,14 +603,14 @@ func TestNilPtrs_True(t *testing.T) {

func TestNilPtrs_False(t *testing.T) {
type TConf struct {
PString *string `desc:"Pointer to string"`
PLogLevel *slog.Level `desc:"Pointer to log level" default:"debug"`
PInt *int `desc:"Pointer to int"`
PString2 *string `desc:"Pointer to another string"`
PInts *[]int `desc:"Pointer to int slice"`
PIntsDef *[]int `desc:"Pointer to int slice" default:"1,3,4"`
PStrings *[]string `desc:"Pointer to string slice"`
PStringsDef *[]string `desc:"Pointer to string slice" default:"a,b,c"`
PString *string `help:"Pointer to string"`
PLogLevel *slog.Level `help:"Pointer to log level" default:"debug"`
PInt *int `help:"Pointer to int"`
PString2 *string `help:"Pointer to another string"`
PInts *[]int `help:"Pointer to int slice"`
PIntsDef *[]int `help:"Pointer to int slice" default:"1,3,4"`
PStrings *[]string `help:"Pointer to string slice"`
PStringsDef *[]string `help:"Pointer to string slice" default:"a,b,c"`
}

var conf *TConf
Expand Down
24 changes: 12 additions & 12 deletions get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,28 +51,28 @@ func TestGet_DisableCache(t *testing.T) {

func TestGet_VeryNested(t *testing.T) {
type T6 struct {
SubFooString string `desc:"Something" default:"t6there"`
SubFooString string `help:"Something" default:"t6there"`
}
type T5 struct {
T5Str string `desc:"t5" default:"t5"`
T5Str string `help:"t5" default:"t5"`
Not T6
}
type T4 struct {
T4Str string `desc:"t4" default:"t4"`
SubFooString string `desc:"T4 Something" default:"t4there"`
T4Str string `help:"t4" default:"t4"`
SubFooString string `help:"T4 Something" default:"t4there"`
Thing T5
}
type T3 struct {
T3Str string `desc:"t3" default:"t3"`
T3Str string `help:"t3" default:"t3"`
Other T4
}
type T2 struct {
T2Str string `desc:"t2" default:"t2"`
SubFooString string `desc:"T2 Something" default:"t2there"`
T2Str string `help:"t2" default:"t2"`
SubFooString string `help:"T2 Something" default:"t2there"`
Something T3
}
type T1 struct {
T1Str string `desc:"t1" default:"t1"`
T1Str string `help:"t1" default:"t1"`
Sub T2
}

Expand All @@ -91,15 +91,15 @@ func TestGet_VeryNested(t *testing.T) {
func TestGet_Anonymous(t *testing.T) {

type T3 struct {
T3Str string `desc:"t3" default:"t3"`
T3Str string `help:"t3" default:"t3"`
}
type T2 struct {
T2Str string `desc:"t2" default:"t2"`
SubFooString string `desc:"T2 Something" default:"t2there"`
T2Str string `help:"t2" default:"t2"`
SubFooString string `help:"T2 Something" default:"t2there"`
T3
}
type T1 struct {
T1Str string `desc:"t1" default:"t1"`
T1Str string `help:"t1" default:"t1"`
T2
}

Expand Down
16 changes: 3 additions & 13 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
module github.com/imoore76/configurature

go 1.22.1
go 1.23.4

require (
github.com/fatih/structtag v1.2.0
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.22.1
github.com/iancoleman/strcase v0.3.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
)
Loading