From 56037c4b0574c39305347c57f9b74f4296ca61ba Mon Sep 17 00:00:00 2001 From: Ryan McGuire Date: Wed, 8 Jan 2025 16:48:34 -0500 Subject: [PATCH] Support custom types --- pkg/app/setup.go | 7 +++ pkg/app/setup_custom.go | 107 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 pkg/app/setup_custom.go diff --git a/pkg/app/setup.go b/pkg/app/setup.go index 8797312..0383aea 100644 --- a/pkg/app/setup.go +++ b/pkg/app/setup.go @@ -23,3 +23,10 @@ func MustSetupConfigAndLogging(ctx context.Context) context.Context { zerolog.Ctx(ctx).Trace().Any("config", *cfg).Send() return ctx } + +// Unmarshal config into a custom type +// Type MUST include *config.AppConfig +// Stored in context as *config.AppConfig but can be asserted back +func MustSetupConfigAndLoggingInto[T any](ctx context.Context, into T) (context.Context, T) { + return ctx, into +} diff --git a/pkg/app/setup_custom.go b/pkg/app/setup_custom.go new file mode 100644 index 0000000..5f8e258 --- /dev/null +++ b/pkg/app/setup_custom.go @@ -0,0 +1,107 @@ +package app + +import ( + "context" + "errors" + "flag" + "fmt" + "os" + "reflect" + + "gopkg.in/yaml.v3" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/config" +) + +// Used to unmarshal config and environment into a custom type +// that overloads *config.AppConfig. Will perform normal env +// substitutions for AppConfig, but env overrides for custom type +// are up to the caller. +func MustLoadConfigInto[T any](ctx context.Context, into T) (context.Context, T) { + // Step 1: Check our custom type for required *config.AppConfig + if err := hasAppConfig(into); err != nil { + panic(err) + } + + // Step 2: Do the normal thing + ctx = MustSetupConfigAndLogging(ctx) + + // Step 3: Extract the config + cfg := config.MustFromCtx(ctx) + + // Step 4: Unmarshal custom config + configPath := flag.Lookup("config") + if configPath != nil && configPath.Value.String() != "" { + file, err := os.Open(configPath.Value.String()) + if err != nil { + panic(fmt.Errorf("could not open config file: %w", err)) + } + defer file.Close() + + decoder := yaml.NewDecoder(file) + if err := decoder.Decode(into); err != nil { + panic(fmt.Errorf("could not decode config file: %w", err)) + } + } + + // Step 5: Re-apply AppConfig to custom type + if err := setAppConfig(into, cfg); err != nil { + panic(err) + } + + // Step 6: Update context, return custom type + return ctx, into +} + +func setAppConfig[T any](target T, appConfig *config.AppConfig) error { + // Ensure target is a pointer to a struct + v := reflect.ValueOf(target) + if v.Kind() != reflect.Ptr || v.IsNil() { + return errors.New("target must be a non-nil pointer to a struct") + } + + v = v.Elem() // Dereference the pointer + if v.Kind() != reflect.Struct { + return errors.New("target must be a pointer to a struct") + } + + // Replace *config.AppConfig + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + if field.Type() == reflect.TypeOf((*config.AppConfig)(nil)) { + if !field.CanSet() { + return fmt.Errorf("field %q cannot be set", v.Type().Field(i).Name) + } + field.Set(reflect.ValueOf(appConfig)) + return nil + } + } + + return errors.New("no *config.AppConfig field found in target struct") +} + +func hasAppConfig[T any](target T) error { + v := reflect.ValueOf(target) + if v.Kind() != reflect.Ptr || v.IsNil() { + return errors.New("target must be a non-nil pointer to a struct") + } + v = v.Elem() + + if v.Kind() != reflect.Struct { + return errors.New("target must be a pointer to a struct") + } + + hasAppConfig := false + for i := 0; i < v.NumField(); i++ { + field := v.Type().Field(i) + if field.Type == reflect.TypeOf((*config.AppConfig)(nil)) { + hasAppConfig = true + break + } + } + if !hasAppConfig { + return errors.New("struct does not contain a *config.AppConfig field") + } + + return nil +}