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
64 changes: 49 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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
Expand All @@ -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
},
}
}
```
51 changes: 42 additions & 9 deletions cmd/configloader/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package main

import (
"encoding/json"
"fmt"
"io"
"os"

cobraconfig "github.com/pastdev/configloader/pkg/cobra"
Expand All @@ -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"},
Expand All @@ -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)
Expand Down
105 changes: 90 additions & 15 deletions pkg/cobra/config.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -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
}
}
4 changes: 2 additions & 2 deletions pkg/cobra/flags.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package config
package cobra

import (
"github.com/pastdev/configloader/pkg/config"
Expand All @@ -7,7 +7,7 @@ import (
)

type flags[T any] struct {
config *Config[T]
config *ConfigLoader[T]
root *cobra.Command
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/config/filesource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/rawsource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down