Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/Fill-Struct #4

Merged
merged 7 commits into from
Sep 8, 2024
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
41 changes: 36 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ In Progress

### Getting Values from `bconf.AppConfig`

* `FillStruct(configStruct any) error`
* `GetField(fieldSetKey, fieldKey string) (*bconf.Field, error)`
* `GetString(fieldSetKey, fieldKey string) (string, error)`
* `GetStrings(fieldSetKey, fieldKey string) ([]string, error)`
Expand All @@ -62,6 +63,7 @@ In Progress
* Ability to get a safe map of configuration values from the `bconf.AppConfig` `ConfigMap()` function
* (the configuration map will obfuscate values from fields with `Sensitive` parameter set to `true`)
* Ability to reload field-sets and individual fields via the `bconf.AppConfig`
* Ability to fill configuration structures with values from a `bconf.AppConfig`

### Limitations

Expand Down Expand Up @@ -139,10 +141,24 @@ if errs := configuration.Register(true); len(errs) > 0 {
// (based on the loaders set above).
logLevel, err := configuration.GetString("log", "level")
if err != nil {
// handle retrieval error
// handle error
}

fmt.Printf("log-level: %s", logLevel)
fmt.Printf("log-level: %s\n", logLevel)

type loggerConfig struct {
bconf.ConfigStruct `bconf:"log"`
Level string `bconf:"level"`
Format string `bconf:"format"`
ColorEnabled bool `bconf:"color_enabled"`
}

logConfig := &loggerConfig{}
if err := configuration.FillStruct(logConfig); err != nil {
// handle error
}

fmt.Printf("log config: %v\n", logConfig)
```

```go
Expand Down Expand Up @@ -227,17 +243,32 @@ if errs := configuration.Register(true); len(errs) > 0 {
// (based on the loaders set above).
logLevel, err := configuration.GetString("log", "level")
if err != nil {
// handle retrieval error
// handle error
}

fmt.Printf("log-level: %s\n", logLevel)

type loggerConfig struct {
bconf.ConfigStruct `bconf:"log"`
Level string `bconf:"level"`
Format string `bconf:"format"`
ColorEnabled bool `bconf:"color_enabled"`
}

logConfig := &loggerConfig{}
if err := configuration.FillStruct(logConfig); err != nil {
// handle error
}

fmt.Printf("log-level: %s", logLevel)
fmt.Printf("log config: %v\n", logConfig)
```

In both of the code blocks above, a `bconf.AppConfig` is defined with two field-sets (which group configuration related
to the application and logging in this case), and registered with help flag parsing.

If this code was executed in a `main()` function, it would print the log level picked up by the configuration from the
flags or run-time environment before falling back on the defined default value of "info".
flags or run-time environment before falling back on the defined default value of "info". It would then fill the
`logConfig` struct with multiple values from the log field-set fields, and print those values as well.

If this code was executed inside the `main()` function and passed a `--help` or `-h` flag, it would print the following
output:
Expand Down
83 changes: 83 additions & 0 deletions app_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package bconf
import (
"fmt"
"os"
"reflect"
"sort"
"strings"
"sync"
Expand Down Expand Up @@ -424,6 +425,88 @@ func (c *AppConfig) GetDurations(fieldSetKey, fieldKey string) ([]time.Duration,
return val, nil
}

func (c *AppConfig) FillStruct(configStruct any) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("problem filling struct: %s", r)
}
}()

if reflect.TypeOf(configStruct).Kind() != reflect.Pointer {
return fmt.Errorf("FillStruct expects a pointer to a struct, found '%s'", reflect.TypeOf(configStruct).Kind())
}

configStructValue := reflect.Indirect(reflect.ValueOf(configStruct))
configStructType := configStructValue.Type()

if configStructValue.Kind() != reflect.Struct {
return fmt.Errorf("FillStruct expects a pointer to a struct, found pointer to '%s'", configStructValue.Kind())
}

configStructField := configStructValue.FieldByName("ConfigStruct")
if !configStructField.IsValid() || configStructField.Type().PkgPath() != "github.com/rheisen/bconf" {
return fmt.Errorf("FillStruct expects a struct with a bconf.ConfigStruct field, none found")
}

configStructFieldType, _ := configStructType.FieldByName("ConfigStruct")

baseFieldSet := configStructFieldType.Tag.Get("bconf")

if overrideValue := configStructField.FieldByName("FieldSet"); overrideValue.String() != "" {
baseFieldSet = overrideValue.String()
}

for i := 0; i < configStructValue.NumField(); i++ {
field := configStructType.Field(i)

if field.Name == "ConfigStruct" && field.Type.PkgPath() == "github.com/rheisen/bconf" {
continue
}

fieldTagValue := field.Tag.Get("bconf")
fieldKey := ""
fieldSetKey := baseFieldSet

switch fieldTagValue {
case "":
fieldKey = field.Name
case "-":
continue
default:
fieldTagParams := strings.Split(fieldTagValue, ",")
fieldLocation := strings.Split(fieldTagParams[0], ".")

fieldKey = fieldLocation[0]

// NOTE: error if fieldLocation format isn't <field>.<field-name> ?
if len(fieldLocation) > 1 {
fieldSetKey = fieldLocation[0]
fieldKey = fieldLocation[1]
}
}

if fieldSetKey == "" {
return fmt.Errorf("unidentified field-set for field: %s", fieldKey)
}

appConfigField, err := c.GetField(fieldSetKey, fieldKey)
if err != nil {
return fmt.Errorf("problem getting field '%s.%s': %w", fieldSetKey, fieldKey, err)
}

val, err := appConfigField.getValue()
if err != nil && err.Error() == emptyFieldError {
continue
} else if err != nil {
return fmt.Errorf("problem getting field '%s.%s' value: %w", fieldSetKey, fieldKey, err)
}

configStructValue.Field(i).Set(reflect.ValueOf(val))
}

return nil
}

// -- Private methods --

func (c *AppConfig) addFieldSet(fieldSet *FieldSet, lock bool) []error {
Expand Down
107 changes: 107 additions & 0 deletions app_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,113 @@ func TestAppConfigTimeFieldTypes(t *testing.T) {
}
}

func TestAppConfigFillStruct(t *testing.T) {
//nolint:govet // doesn't need to be optimal for tests
type TestAPIConfig struct {
bconf.ConfigStruct `bconf:"api"`
DBSwitchTime time.Time `bconf:"db_switch_time"`
Host string `bconf:"host"`
ReadTimeout time.Duration `bconf:"read_timeout"`
Port int `bconf:"port"`
DebugMode bool `bconf:"api.debug_mode"`
LogPrefix string `bconf:"log_prefix"`
}

type InvalidAPIConfigStruct struct {
Host string `bconf:"host"`
Port int `bconf:"port"`
}

configStruct := &TestAPIConfig{}

appConfig := createBaseAppConfig()

dbSwitchTime := time.Now().Add(-100 * time.Hour)

errs := appConfig.AddFieldSet(
bconf.FSB().Key("api").Fields(
bconf.FB().Key("host").Type(bconf.String).Default("localhost").Create(),
bconf.FB().Key("port").Type(bconf.Int).Default(8080).Create(),
bconf.FB().Key("read_timeout").Type(bconf.Duration).Default(5*time.Second).Create(),
bconf.FB().Key("db_switch_time").Type(bconf.Time).Default(dbSwitchTime).Create(),
bconf.FB().Key("debug_mode").Type(bconf.Bool).Default(true).Create(),
bconf.FB().Key("log_prefix").Type(bconf.String).Create(),
).Create(),
)

if len(errs) > 0 {
t.Fatalf("problem adding field set to AppConfig: %v\n", errs)
}

errs = appConfig.AddFieldSet(
bconf.FSB().Key("ext_api").Fields(
bconf.FB().Key("host").Type(bconf.String).Default("0.0.0.0").Create(),
bconf.FB().Key("port").Type(bconf.Int).Default(8085).Create(),
bconf.FB().Key("read_timeout").Type(bconf.Duration).Default(10*time.Second).Create(),
bconf.FB().Key("db_switch_time").Type(bconf.Time).Default(dbSwitchTime).Create(),
bconf.FB().Key("debug_mode").Type(bconf.Bool).Default(true).Create(),
bconf.FB().Key("log_prefix").Type(bconf.String).Create(),
).Create(),
)

if len(errs) > 0 {
t.Fatalf("problem adding field set to AppConfig: %v\n", errs)
}

unfillableStruct := TestAPIConfig{}
if err := appConfig.FillStruct(unfillableStruct); err == nil {
t.Fatalf("expected error passing concrete struct\n")
}

notAStruct := 20
if err := appConfig.FillStruct(&notAStruct); err == nil {
t.Fatalf("expected error passing pointer to a non-struct type\n")
}

invalidConfigStruct := &InvalidAPIConfigStruct{}
if err := appConfig.FillStruct(invalidConfigStruct); err == nil {
t.Fatalf("expected error passing struct missing bconf.ConfigStruct field\n")
}

if err := appConfig.FillStruct(configStruct); err != nil {
t.Fatalf("problem setting struct values from AppConfig: %s\n", err)
}

if configStruct.Host != "localhost" {
t.Errorf("unexpected value for configStruct.Host ('%s'), expected: %s\n", configStruct.Host, "localhost")
}

if configStruct.Port != 8080 {
t.Errorf("unexpected value for configStruct.Port ('%d'), expected: %d\n", configStruct.Port, 8080)
}

if configStruct.ReadTimeout != 5*time.Second {
t.Errorf("unexpected value for configStruct.Host ('%s'), expected: %s\n", configStruct.ReadTimeout, 5*time.Second)
}

if configStruct.DBSwitchTime != dbSwitchTime {
t.Errorf("unexpected value for configStruct.Host ('%s'), expected: %s\n", configStruct.DBSwitchTime, dbSwitchTime)
}

if configStruct.DebugMode != true {
t.Errorf("unexpected value for configStruct.Host ('%v'), expected: %v\n", configStruct.DebugMode, true)
}

overrideConfigStruct := &TestAPIConfig{ConfigStruct: bconf.ConfigStruct{FieldSet: "ext_api"}}

if err := appConfig.FillStruct(overrideConfigStruct); err != nil {
t.Fatalf("problem setting override struct values from AppConfig: %s\n", err)
}

if overrideConfigStruct.Host != "0.0.0.0" {
t.Errorf("unexpected value for overrideConfigStruct.Host ('%s'), expected: %s\n", configStruct.Host, "0.0.0.0")
}

if overrideConfigStruct.Port != 8085 {
t.Errorf("unexpected value for overrideConfigStruct.Port ('%d'), expected: %d\n", configStruct.Port, 8085)
}
}

func createBaseAppConfig() *bconf.AppConfig {
appConfig := bconf.NewAppConfig(
"app",
Expand Down
5 changes: 5 additions & 0 deletions config_struct.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package bconf

type ConfigStruct struct {
FieldSet string
}
4 changes: 3 additions & 1 deletion field.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/rheisen/bconf/bconfconst"
)

const emptyFieldError = "empty field value"

// Fields is a slice of Field elements providing context for configuration values
type Fields []*Field

Expand Down Expand Up @@ -279,7 +281,7 @@ func (f *Field) getValue() (any, error) {
return f.generatedDefault, nil
}

return nil, fmt.Errorf("empty field value")
return nil, fmt.Errorf(emptyFieldError)
}

// func (f *Field) getValueFrom(loader string) (any, error) {
Expand Down
5 changes: 5 additions & 0 deletions field_set_struct.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package bconf

type FieldSetStruct interface {
FieldSet() string
}
14 changes: 6 additions & 8 deletions json_file_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ func NewJSONFileLoader() *JSONFileLoader {
}

func NewJSONFileLoaderWithAttributes(decoder JSONUnmarshal, filePaths ...string) *JSONFileLoader {
if decoder == nil {
decoder = json.Unmarshal
}

return &JSONFileLoader{
Decoder: decoder,
FilePaths: filePaths,
Expand Down Expand Up @@ -131,14 +135,8 @@ func (l *JSONFileLoader) fileMaps() []map[string]any {
}

fileMap := map[string]any{}
if l.Decoder != nil {
if err := l.Decoder(fileBytes, &fileMap); err != nil {
continue
}
} else {
if err := json.Unmarshal(fileBytes, &fileMap); err != nil {
continue
}
if err := l.Decoder(fileBytes, &fileMap); err != nil {
continue
}

fileMaps = append(fileMaps, fileMap)
Expand Down
8 changes: 0 additions & 8 deletions json_file_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,6 @@ func TestJSONFileLoaderFunctions(t *testing.T) {

loader = bconf.NewJSONFileLoaderWithAttributes(json.Unmarshal, "./fixtures/json_config_test_fixture_01.json")

if loader == nil {
t.Fatalf("unexpected nil loader")
}

if loader.Decoder == nil {
t.Fatalf("unexpected nil decoder")
}

if len(loader.FilePaths) != 1 {
t.Fatalf("unexpected file-paths length '%d', expected '1'", len(loader.FilePaths))
}
Expand Down
Loading