diff --git a/README.md b/README.md index 7a303c9..f9bbb5f 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,11 @@ import( ## pkg/cobra The [`github.com/pastdev/pkg/cobra`](./pkg/cobra) package provides CLI integration with [cobra](https://github.com/spf13/cobra). -There are 2 mandatory, and 2 optional integration points. -First you need to define your [`Config`](./pkg/cobra/config.go) object: +There is 1 mandatory, and 2 optional integration points. +First you need to define your [`ConfigLoader`](./pkg/cobra/config.go) object: ```go - cfg := cobraconfig.Config[map[any]any]{ + cfgldr := cobraconfig.ConfigLoader[map[any]any]{ DefaultSources: config.Sources[map[any]any]{ config.FileSource[map[any]any]{Path: "/etc/configloader.yml"}, config.DirSource[map[any]any]{Path: "/etc/configloader.d"}, @@ -51,18 +51,6 @@ First you need to define your [`Config`](./pkg/cobra/config.go) object: } ``` -Then you need to `Load()` the configuration object in a the `PersistentPreRunE` function of your root command: - -```go - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - err := cfg.Load() - if err != nil { - return fmt.Errorf("load config: %w", err) - } - return nil - }, -``` - Optionally, you can use flags to allow your user to replace the `DefaultSources`: ```go @@ -81,3 +69,49 @@ And add a `config` subcommand to your root command for printing out the configur ```go cfg.AddSubCommandTo(&root) ``` + +Or a use additional options when adding the subcommand: + +```go + cfgldr.AddSubCommandTo( + &root, + cobraconfig.WithConfigCommandOutput( + "json", + func(w io.Writer, cfg *map[any]any) error { + jsonmap := map[string]any{} + for k, v := range *cfg { + jsonmap[fmt.Sprintf("%s", k)] = v + } + + err := json.NewEncoder(w).Encode(jsonmap) + if err != nil { + return fmt.Errorf("format json: %w", err) + } + return nil + }, + ), + cobraconfig.WithConfigCommandSilenceUsage[map[any]any](true)) +``` + +Then you can pass the the configloader object to any subcommands and simply call the `.Config()` method to load and access the config object: + +```go + root.AddCommand(fooCmd(&cfgldr)) +... + +func fooCmd(cfgldr *cobraconfig.ConfigLoader[map[any]any]) *cobra.Command { + return &cobra.Command{ + Use: "foo", + Short: `An example subcommand for how to use configloader to show the value of foo.`, + RunE: func(_ *cobra.Command, _ []string) error { + cfg, err := cfgldr.Config() + if err != nil { + return fmt.Errorf("get config: %w", err) + } + + fmt.Printf("foo is [%s]", (*cfg)["foo"]) + return nil + }, + } +} +``` diff --git a/cmd/configloader/main.go b/cmd/configloader/main.go index cbdc579..e358b13 100644 --- a/cmd/configloader/main.go +++ b/cmd/configloader/main.go @@ -1,7 +1,9 @@ package main import ( + "encoding/json" "fmt" + "io" "os" cobraconfig "github.com/pastdev/configloader/pkg/cobra" @@ -10,8 +12,24 @@ import ( "github.com/spf13/cobra" ) +func fooCmd(cfgldr *cobraconfig.ConfigLoader[map[any]any]) *cobra.Command { + return &cobra.Command{ + Use: "foo", + Short: `An example subcommand for how to use configloader to show the value of foo.`, + RunE: func(_ *cobra.Command, _ []string) error { + cfg, err := cfgldr.Config() + if err != nil { + return fmt.Errorf("get config: %w", err) + } + + fmt.Printf("foo is [%s]", (*cfg)["foo"]) + return nil + }, + } +} + func main() { - cfg := cobraconfig.Config[map[any]any]{ + cfgldr := cobraconfig.ConfigLoader[map[any]any]{ DefaultSources: config.Sources[map[any]any]{ config.FileSource[map[any]any]{Path: "/etc/configloader.yml"}, config.DirSource[map[any]any]{Path: "/etc/configloader.d"}, @@ -26,29 +44,44 @@ func main() { PersistentPreRunE: func(_ *cobra.Command, _ []string) error { // optionally set a logger for the config lib config.Logger = zerolog.New(os.Stderr).Level(zerolog.TraceLevel).With().Timestamp().Logger() - // load the configuration - err := cfg.Load() - if err != nil { - return fmt.Errorf("load config: %w", err) - } return nil }, } // use the config to add persistent flags to the root command so that they // are available to all subcommands - cfg.PersistentFlags(&root).FileSourceVar( + cfgldr.PersistentFlags(&root).FileSourceVar( config.YamlUnmarshal, "config", "location of one or more config files") - cfg.PersistentFlags(&root).DirSourceVar( + cfgldr.PersistentFlags(&root).DirSourceVar( config.YamlUnmarshal, "config-dir", "location of one or more config directories") // optionally add a `config` subcommand that allows viewing of the resulting // configuration - cfg.AddSubCommandTo(&root) + cfgldr.AddSubCommandTo( + &root, + cobraconfig.WithConfigCommandOutput( + "json", + func(w io.Writer, cfg *map[any]any) error { + jsonmap := map[string]any{} + for k, v := range *cfg { + jsonmap[fmt.Sprintf("%s", k)] = v + } + + err := json.NewEncoder(w).Encode(jsonmap) + if err != nil { + return fmt.Errorf("format json: %w", err) + } + return nil + }, + ), + cobraconfig.WithConfigCommandSilenceUsage[map[any]any](true)) + + // pass the config loader to subcommands so they can access .Config() + root.AddCommand(fooCmd(&cfgldr)) if err := root.Execute(); err != nil { os.Exit(1) diff --git a/pkg/cobra/config.go b/pkg/cobra/config.go index a768ece..8aa9c2d 100644 --- a/pkg/cobra/config.go +++ b/pkg/cobra/config.go @@ -1,34 +1,51 @@ -package config +package cobra import ( "fmt" + "io" "os" + "strings" "github.com/pastdev/configloader/pkg/config" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) -// Config provides utilities to integrate configuration loading into cobra +type ConfigCommandOption[T any] func(*ConfigCommandOptions[T]) + +type ConfigCommandOptions[T any] struct { + Output map[string]func(w io.Writer, cfg *T) error + SilenceUsage bool +} + +// ConfigLoader provides utilities to integrate configuration loading into cobra // CLI commands. -type Config[T any] struct { +type ConfigLoader[T any] struct { config T // DefaultSources are sources that you can configure in the code and allow // for the flags to replace at runtime. DefaultSources config.Sources[T] + loaded bool sources config.Sources[T] } // Config returns the generated configuration object that will be loaded by the // Load method. -func (c *Config[T]) Config() *T { - return &c.config +func (c *ConfigLoader[T]) Config() (*T, error) { + if !c.loaded { + err := c.load() + if err != nil { + return nil, err + } + c.loaded = true + } + return &c.config, nil } // Load loads the configuration. If sources were set using the persistent flags, // then the DefaultSources will be ignored. Otherwise, configurationis loaded // from the DefaultSources. -func (c *Config[T]) Load() error { +func (c *ConfigLoader[T]) load() error { sources := c.sources if len(sources) == 0 { sources = c.DefaultSources @@ -45,7 +62,7 @@ func (c *Config[T]) Load() error { // the supplied root command. // //nolint:revive // want to limit what can be done to the returned object -func (c *Config[T]) PersistentFlags(root *cobra.Command) *flags[T] { +func (c *ConfigLoader[T]) PersistentFlags(root *cobra.Command) *flags[T] { return &flags[T]{ config: c, root: root, @@ -54,17 +71,75 @@ func (c *Config[T]) PersistentFlags(root *cobra.Command) *flags[T] { // AddSubCommandTo will add a config subcommand to the supplied root command. // This subcommand will print out the configuration. -func (c *Config[T]) AddSubCommandTo(root *cobra.Command) { - root.AddCommand( - &cobra.Command{ - Use: "config", - Short: `Print out the config data.`, - RunE: func(_ *cobra.Command, _ []string) error { - err := yaml.NewEncoder(os.Stdout).Encode(c.Config()) +func (c *ConfigLoader[T]) AddSubCommandTo(root *cobra.Command, opts ...ConfigCommandOption[T]) { + options := ConfigCommandOptions[T]{ + Output: map[string]func(w io.Writer, cfg *T) error{ + "yaml": func(w io.Writer, cfg *T) error { + err := yaml.NewEncoder(w).Encode(cfg) if err != nil { return fmt.Errorf("serialize config: %w", err) } return nil }, - }) + }, + } + for _, opt := range opts { + opt(&options) + } + + var output string + + cmd := cobra.Command{ + Use: "config", + Short: `Print out the config data.`, + Args: cobra.NoArgs, + SilenceUsage: options.SilenceUsage, + RunE: func(_ *cobra.Command, _ []string) error { + cfg, err := c.Config() + if err != nil { + return fmt.Errorf("get config: %w", err) + } + + formatter, ok := options.Output[output] + if !ok { + return fmt.Errorf("undefined formatter: %s", output) + } + + err = formatter(os.Stdout, cfg) + if err != nil { + return fmt.Errorf("format config: %w", err) + } + return nil + }, + } + + formatters := make([]string, 0, len(options.Output)) + for formatter := range options.Output { + formatters = append(formatters, formatter) + } + + if len(formatters) > 1 { + cmd.Flags().StringVar( + &output, + "output", + "yaml", + fmt.Sprintf("Format of output, one of: %s", strings.Join(formatters, ", "))) + } + + root.AddCommand(&cmd) +} + +func WithConfigCommandOutput[T any]( + name string, + formatter func(w io.Writer, cfg *T) error, +) ConfigCommandOption[T] { + return func(cco *ConfigCommandOptions[T]) { + cco.Output[name] = formatter + } +} + +func WithConfigCommandSilenceUsage[T any](s bool) ConfigCommandOption[T] { + return func(cco *ConfigCommandOptions[T]) { + cco.SilenceUsage = s + } } diff --git a/pkg/cobra/flags.go b/pkg/cobra/flags.go index 0525c33..54e5e8a 100644 --- a/pkg/cobra/flags.go +++ b/pkg/cobra/flags.go @@ -1,4 +1,4 @@ -package config +package cobra import ( "github.com/pastdev/configloader/pkg/config" @@ -7,7 +7,7 @@ import ( ) type flags[T any] struct { - config *Config[T] + config *ConfigLoader[T] root *cobra.Command } diff --git a/pkg/config/filesource.go b/pkg/config/filesource.go index 9a62123..0326d5d 100644 --- a/pkg/config/filesource.go +++ b/pkg/config/filesource.go @@ -23,7 +23,7 @@ func (s FileSource[T]) Load(cfg *T) error { err = unmarshal(b, cfg, s.Unmarshal) if err != nil { - return fmt.Errorf("load from dir: %w", err) + return fmt.Errorf("load from file: %w", err) } Logger.Debug().Str("file", s.Path).Msg("loaded filesource config") diff --git a/pkg/config/rawsource.go b/pkg/config/rawsource.go index 053a240..10b402c 100644 --- a/pkg/config/rawsource.go +++ b/pkg/config/rawsource.go @@ -14,7 +14,7 @@ type RawSource[T any] struct { func (s RawSource[T]) Load(cfg *T) error { err := unmarshal(s.Data, cfg, s.Unmarshal) if err != nil { - return fmt.Errorf("load from dir: %w", err) + return fmt.Errorf("load from raw: %w", err) } return nil