confetti

package module
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 25, 2025 License: MIT Imports: 17 Imported by: 1

README

Confetti 🎊

License Build and Test Coverage Status Go Report Card Go Reference Socket.dev

An opinionated take on handling Go configs: put your secrets in SSM, put the rest in ENV vars and this package will load them all. And you can also load from JSON because... why not?

Why Confetti?

  • Minimal API, maximal power: One function (Load()), 3 Loader and 6 ways to load the data (ENV vars, SSM var holding a JSON, a local JSON file, a []byte slice, an io.Reader or (preferred over io.Reader) an io.ReadSeeker);
  • Minimal dependencies: Only SSM loader pulls in AWS SDK v2, stdlib for everything else;
  • Composability: Layer environment variables, SSM, and JSON loaders in any order—later loaders override earlier ones;
  • Robust ENV var support: The env variable names are inferred from the struct field name and the passed prefix (if non empty) and can also be overriden on a per-field basis using the struct tag env (e.g. env:"MYAPP_FOO"). Field names in CamelCase are converted to UPPER_SNAKE_CASE for environment variable lookup. Acronyms are handled so that AWSRegion becomes AWS_REGION, and MyID becomes MY_ID.
  • Robust type support: When loading from env it handles primitives, slices, nested structs, booleans (with many/common string forms such as t/f, yes/no, etc.) and time durations out of the box;
  • Testable by example: Code coverage is achieved with concise, real-world examples that double as documentation;
  • Bring Your Own Loader: If builtin loaders don't fit your needs, you can easily implement your own loader by implementing the Loader interface (not at the moment, TBD);
  • Unknown field/var detection: Optionally error if unknown fields/vars are present in the data but not in the target config, but ONLY AFTER the data has been loaded, so you can still use the config and just warn about the unknown fields.

Available Loaders

Loader Source Type Example Usage
WithErrOnUnknown N/A This sets the option to err on unknown fields/vars
WithEnv ENV prefix (string) WithEnv("MYAPP")
WithSSM SSM key (string) WithSSM("/my/key", "us-east-1")
WithJSON file path (string) WithJSON("config.json")
WithJSON []byte WithJSON([]byte(jsonData))
WithJSON io.ReadSeeker WithJSON(bytes.NewReader(data))
WithJSON io.Reader WithJSON(os.Stdin)

Usage

import "github.com/alexaandru/confetti"

var cfg MyConfig

func init() {
  if err := confetti.Load(&cfg, confetti.WithEnv("MYAPP")); err != nil {
    panic(err)
  }
}
Strict Mode
cfg := MyConfig{}
err := confetti.Load(&cfg, confetti.WithErrOnUnknown(), confetti.WithJSON("./config.json"))
if errors.Is(err, confetti.ErrUnknownFields) {
    // handle unknown fields error
}
Default Values

No direct support for default values however, you can provide the cfg pre-populated and you can also Make the zero value useful which together should cover most use cases.

Another option would be to use go:embed to embed a JSON file with the defaults, while using other loaders to override it, i.e.

//go:embed defaults.json
var defaultConfig []byte

func init() {
  if err := confetti.Load(&cfg,
    confetti.WithJSON(defaultConfig),
    confetti.WithJSON(".env.production.json")); err != nil {
    panic(err)
  }
}

For more examples see: ENV, JSON, ENV+JSON and SSM Loader examples.

License

MIT

Documentation

Index

Examples

Constants

View Source
const DefaultAWSRegion = "us-east-1"
View Source
const DefaultSeparator = ","

Variables

View Source
var (
	ErrUnknownFields = errors.New("unknown fields in config")
	ErrNoDataSource  = errors.New("no data source for JSON loader")
)

Functions

func Load

func Load(cfg any, ld Loader, opts ...Loader) (err error)

Load applies one or more loader functions to populate the given config which MUST be a pointer to a struct.

The first argument must be a pointer to a struct. Each loader (such as WithEnv, WithSSM, WithJSON) is applied in order, with later loaders overriding values from earlier ones.

You can optionally pass options. Currently, the only supported option is WithErrOnUnknown, which controls whether to return an error when unknown fields are present in the source but not defined in the target config.

Returns an error if the config pointer is nil, not a struct, or if any loader fails.

Example usage:

cfg := MyConfig{}
err := confetti.Load(&cfg, confetti.WithJSON("./config.json"), confetti.WithEnv("MYAPP"))
if err != nil { panic(err) }
Example (Env)
package main

import (
	"fmt"
	"os"

	"github.com/alexaandru/confetti"
)

type ExampleConfig struct {
	Host   string
	Port   int
	Debug  bool
	Nested struct {
		Value string
		Deep  struct {
			Foo string
		}
	}
	Strs []string
	Ints []int
}

func main() {
	os.Setenv("MYAPP1_HOST", "127.0.0.1")
	os.Setenv("MYAPP1_PORT", "1234")
	os.Setenv("MYAPP1_DEBUG", "true")
	os.Setenv("MYAPP1_NESTED_VALUE", "bar")
	os.Setenv("MYAPP1_NESTED_DEEP_FOO", "baz")
	os.Setenv("MYAPP1_STRS", "a,b,c")
	os.Setenv("MYAPP1_INTS", "1,2,3")

	cfg := &ExampleConfig{}
	if err := confetti.Load(cfg, confetti.WithEnv("MYAPP1")); err != nil {
		panic(err)
	}

	fmt.Printf("Host=%s\n", cfg.Host)
	fmt.Printf("Port=%d\n", cfg.Port)
	fmt.Printf("Debug=%v\n", cfg.Debug)
	fmt.Printf("Nested.Value=%s\n", cfg.Nested.Value)
	fmt.Printf("Nested.Deep.Foo=%s\n", cfg.Nested.Deep.Foo)
	fmt.Printf("Strs=%#v\n", cfg.Strs)
	fmt.Printf("Ints=%#v\n", cfg.Ints)
}
Output:

Host=127.0.0.1
Port=1234
Debug=true
Nested.Value=bar
Nested.Deep.Foo=baz
Strs=[]string{"a", "b", "c"}
Ints=[]int{1, 2, 3}
Example (Env_complex)
package main

import (
	"fmt"
	"os"
	"time"

	"github.com/alexaandru/confetti"
)

type ComplexConfig struct {
	Str    string
	Int    int
	Uint   uint
	Bool   bool
	Float  float64
	Strs   []string
	Ints   []int
	Uints  []uint
	Bools  []bool
	Floats []float64
	Dur    time.Duration
	Nested struct {
		Strs []string
		Deep struct {
			Int int
		}
	}
}

func main() {
	os.Setenv("CPLX_STR", "foo")
	os.Setenv("CPLX_INT", "42")
	os.Setenv("CPLX_UINT", "7")
	os.Setenv("CPLX_BOOL", "true")
	os.Setenv("CPLX_FLOAT", "3.14")
	os.Setenv("CPLX_STRS", "a,b,c")
	os.Setenv("CPLX_INTS", "1,2,3")
	os.Setenv("CPLX_UINTS", "4,5,6")
	os.Setenv("CPLX_FLOATS", "1.1,2.2,3.3")
	os.Setenv("CPLX_DUR", "1h30m")
	os.Setenv("CPLX_NESTED_STRS", "x,y")
	os.Setenv("CPLX_NESTED_DEEP_INT", "99")

	cfg := &ComplexConfig{}
	if err := confetti.Load(cfg, confetti.WithEnv("CPLX")); err != nil {
		panic(err)
	}

	fmt.Printf("Str=%s\n", cfg.Str)
	fmt.Printf("Int=%d\n", cfg.Int)
	fmt.Printf("Uint=%d\n", cfg.Uint)
	fmt.Printf("Bool=%v\n", cfg.Bool)
	fmt.Printf("Float=%.2f\n", cfg.Float)
	fmt.Printf("Strs=%#v\n", cfg.Strs)
	fmt.Printf("Ints=%#v\n", cfg.Ints)
	fmt.Printf("Uints=%#v\n", cfg.Uints)
	fmt.Printf("Bools=%#v\n", cfg.Bools)
	fmt.Printf("Floats=%#v\n", cfg.Floats)
	fmt.Printf("Dur=%s\n", cfg.Dur)
	fmt.Printf("Nested.Strs=%#v\n", cfg.Nested.Strs)
	fmt.Printf("Nested.Deep.Int=%d\n", cfg.Nested.Deep.Int)
}
Output:

Str=foo
Int=42
Uint=7
Bool=true
Float=3.14
Strs=[]string{"a", "b", "c"}
Ints=[]int{1, 2, 3}
Uints=[]uint{0x4, 0x5, 0x6}
Bools=[]bool(nil)
Floats=[]float64{1.1, 2.2, 3.3}
Dur=1h30m0s
Nested.Strs=[]string{"x", "y"}
Nested.Deep.Int=99
Example (Env_error_not_pointer)
package main

import (
	"fmt"
	"time"

	"github.com/alexaandru/confetti"
)

type ComplexConfig struct {
	Str    string
	Int    int
	Uint   uint
	Bool   bool
	Float  float64
	Strs   []string
	Ints   []int
	Uints  []uint
	Bools  []bool
	Floats []float64
	Dur    time.Duration
	Nested struct {
		Strs []string
		Deep struct {
			Int int
		}
	}
}

func main() {
	cfg := ComplexConfig{} // not a pointer
	err := confetti.Load(cfg, confetti.WithEnv("MYAPP2"))

	fmt.Println(err)
}
Output:

config must be a pointer to a struct (got confetti_test.ComplexConfig)
Example (Env_error_not_struct)
package main

import (
	"fmt"

	"github.com/alexaandru/confetti"
)

func main() {
	var x int

	err := confetti.Load(&x, confetti.WithEnv("MYAPP3"))

	fmt.Println(err)
}
Output:

config must be a pointer to a struct (got *int)
Example (Env_error_on_unknown)
package main

import (
	"fmt"
	"os"

	"github.com/alexaandru/confetti"
)

func main() {
	os.Setenv("MYAPP4_DEBUG", "true")
	os.Setenv("MYAPP4_UNUSED", "unknown") // should trigger an error
	os.Setenv("CUSTOM_PORT", "9999")
	os.Setenv("CUSTOM_NESTED_VALUE", "tagged")

	type TaggedConfig struct {
		Port   int `env:"CUSTOM_PORT"`
		Debug  bool
		Nested struct {
			Value string `env:"CUSTOM_NESTED_VALUE"`
		}
	}

	cfg := &TaggedConfig{}
	err := confetti.Load(cfg, confetti.WithErrOnUnknown(), confetti.WithEnv("MYAPP4"))
	fmt.Printf("Port=%d Debug=%v Nested.Value=%s\n", cfg.Port, cfg.Debug, cfg.Nested.Value)
	fmt.Println(err)
}
Output:

Port=9999 Debug=true Nested.Value=tagged
unknown environment variables: [MYAPP4_UNUSED]
Example (Env_error_parse_int)
package main

import (
	"fmt"
	"os"
	"time"

	"github.com/alexaandru/confetti"
)

type ComplexConfig struct {
	Str    string
	Int    int
	Uint   uint
	Bool   bool
	Float  float64
	Strs   []string
	Ints   []int
	Uints  []uint
	Bools  []bool
	Floats []float64
	Dur    time.Duration
	Nested struct {
		Strs []string
		Deep struct {
			Int int
		}
	}
}

func main() {
	os.Setenv("CPLX_INT", "notanint")

	cfg := &ComplexConfig{}
	err := confetti.Load(cfg, confetti.WithEnv("CPLX"))

	fmt.Println(err)
}
Output:

env CPLX_INT: strconv.ParseInt: parsing "notanint": invalid syntax
Example (Env_struct_tag_override)
package main

import (
	"fmt"
	"os"

	"github.com/alexaandru/confetti"
)

func main() {
	os.Setenv("CUSTOM_PORT", "9999")
	os.Setenv("MYAPP4_DEBUG", "true")
	os.Setenv("MYAPP4_UNUSED", "unknown") // should be ignored
	os.Setenv("CUSTOM_NESTED_VALUE", "tagged")

	type TaggedConfig struct {
		Port   int `env:"CUSTOM_PORT"`
		Debug  bool
		Nested struct {
			Value string `env:"CUSTOM_NESTED_VALUE"`
		}
	}

	cfg := &TaggedConfig{}
	err := confetti.Load(cfg, confetti.WithEnv("MYAPP4"))
	fmt.Printf("Port=%d Debug=%v Nested.Value=%s\n", cfg.Port, cfg.Debug, cfg.Nested.Value)
	fmt.Println(err)
}
Output:

Port=9999 Debug=true Nested.Value=tagged
<nil>
Example (Env_unsupported_bools)
package main

import (
	"fmt"
	"os"
	"time"

	"github.com/alexaandru/confetti"
)

type ComplexConfig struct {
	Str    string
	Int    int
	Uint   uint
	Bool   bool
	Float  float64
	Strs   []string
	Ints   []int
	Uints  []uint
	Bools  []bool
	Floats []float64
	Dur    time.Duration
	Nested struct {
		Strs []string
		Deep struct {
			Int int
		}
	}
}

func main() {
	os.Setenv("CPLX_BOOLS", "true,false,yes,0,n")

	cfg := &ComplexConfig{}
	err := confetti.Load(cfg, confetti.WithEnv("CPLX"))
	fmt.Printf("%#v\n", cfg.Bools)
	fmt.Println(err)
}
Output:

[]bool{true, false, true, false, false}
<nil>
Example (Env_with_acronyms)
package main

import (
	"fmt"
	"os"

	"github.com/alexaandru/confetti"
)

func main() {
	os.Setenv("MYAPP5_SQS_QUEUE", "sqs1")
	os.Setenv("MYAPP5_SOME_SNS_TOPIC", "sns1") // should trigger an error

	type TaggedConfig struct {
		SQSQueue     string
		SomeSNSTopic string
	}

	cfg := &TaggedConfig{}
	err := confetti.Load(cfg, confetti.WithEnv("MYAPP5"))
	fmt.Printf("SQS: %s; SNS: %s\n", cfg.SQSQueue, cfg.SomeSNSTopic)
	fmt.Println(err)
}
Output:

SQS: sqs1; SNS: sns1
<nil>
Example (Json_and_env)
// JSON provides Host and Nested.Value, ENV provides Port, Debug, and Nested.Deep.Foo
jsonData := `{"Host":"localhost","Nested":{"Value":"foo"}}`

file := "test_config.json"
if err := os.WriteFile(file, []byte(jsonData), 0o644); err != nil {
	panic("failed to write test file: " + err.Error())
}
defer os.Remove(file)

os.Setenv("MYAPP_PORT", "8080")
os.Setenv("MYAPP_DEBUG", "true")
os.Setenv("MYAPP_NESTED_DEEP_FOO", "baz")

cfg := &ExampleConfig{}
if err := confetti.Load(cfg,
	confetti.WithJSON(file),
	confetti.WithEnv("MYAPP"),
); err != nil {
	panic(err)
}

fmt.Printf("Host=%s\n", cfg.Host)
fmt.Printf("Port=%d\n", cfg.Port)
fmt.Printf("Debug=%v\n", cfg.Debug)
fmt.Printf("Nested.Value=%s\n", cfg.Nested.Value)
fmt.Printf("Nested.Deep.Foo=%s\n", cfg.Nested.Deep.Foo)
Output:

Host=localhost
Port=8080
Debug=true
Nested.Value=foo
Nested.Deep.Foo=baz
Example (Json_bytes)
cfg := &ExampleConfig{}
if err := confetti.Load(cfg, confetti.WithJSON([]byte(jsonData))); err != nil {
	panic("Load failed: " + err.Error())
}

fmt.Printf("Host=%s\n", cfg.Host)
fmt.Printf("Port=%d\n", cfg.Port)
fmt.Printf("Debug=%v\n", cfg.Debug)
fmt.Printf("Nested.Value=%s\n", cfg.Nested.Value)
fmt.Printf("Nested.Deep.Foo=%s\n", cfg.Nested.Deep.Foo)
Output:

Host=localhost
Port=8080
Debug=true
Nested.Value=foo
Nested.Deep.Foo=baz
Example (Json_file)
file := "test_config.json"
if err := os.WriteFile(file, []byte(jsonData), 0o644); err != nil {
	panic("failed to write test file: " + err.Error())
}
defer os.Remove(file)

cfg := &ExampleConfig{}
if err := confetti.Load(cfg, confetti.WithJSON(file)); err != nil {
	panic("Load failed: " + err.Error())
}

fmt.Printf("Host=%s\n", cfg.Host)
fmt.Printf("Port=%d\n", cfg.Port)
fmt.Printf("Debug=%v\n", cfg.Debug)
fmt.Printf("Nested.Value=%s\n", cfg.Nested.Value)
fmt.Printf("Nested.Deep.Foo=%s\n", cfg.Nested.Deep.Foo)
Output:

Host=localhost
Port=8080
Debug=true
Nested.Value=foo
Nested.Deep.Foo=baz
Example (Json_file_not_found)
cfg := &ExampleConfig{}
err := confetti.Load(cfg, confetti.WithJSON("no_such_file.json"))
fmt.Printf("Error: %v\n", err)
Output:

Error: open no_such_file.json: no such file or directory
Example (Json_reader)
cfg := &ExampleConfig{}

r := bytes.NewBufferString(jsonData)
if err := confetti.Load(cfg, confetti.WithJSON(r)); err != nil {
	panic("Load failed: " + err.Error())
}

fmt.Printf("Host=%s\n", cfg.Host)
fmt.Printf("Port=%d\n", cfg.Port)
fmt.Printf("Debug=%v\n", cfg.Debug)
fmt.Printf("Nested.Value=%s\n", cfg.Nested.Value)
fmt.Printf("Nested.Deep.Foo=%s\n", cfg.Nested.Deep.Foo)
Output:

Host=localhost
Port=8080
Debug=true
Nested.Value=foo
Nested.Deep.Foo=baz
Example (Json_readseeker)
cfg := &ExampleConfig{}

r := bytes.NewReader([]byte(jsonData))
if err := confetti.Load(cfg, confetti.WithJSON(r)); err != nil {
	panic("Load failed: " + err.Error())
}

fmt.Printf("Host=%s\n", cfg.Host)
fmt.Printf("Port=%d\n", cfg.Port)
fmt.Printf("Debug=%v\n", cfg.Debug)
fmt.Printf("Nested.Value=%s\n", cfg.Nested.Value)
fmt.Printf("Nested.Deep.Foo=%s\n", cfg.Nested.Deep.Foo)
Output:

Host=localhost
Port=8080
Debug=true
Nested.Value=foo
Nested.Deep.Foo=baz
Example (Json_unknown_fields)
file := "test_config.json"
if err := os.WriteFile(file, []byte(jsonData), 0o644); err != nil {
	panic("failed to write test file: " + err.Error())
}
defer os.Remove(file)

cfg := &ExampleConfig{}
err := confetti.Load(cfg, confetti.WithErrOnUnknown(), confetti.WithJSON(file))

fmt.Printf("Host=%s\n", cfg.Host)
fmt.Printf("Port=%d\n", cfg.Port)
fmt.Printf("Debug=%v\n", cfg.Debug)
fmt.Printf("Nested.Value=%s\n", cfg.Nested.Value)
fmt.Printf("Nested.Deep.Foo=%s\n", cfg.Nested.Deep.Foo)
fmt.Printf("Error=%s\n", err)
Output:

Host=localhost
Port=8080
Debug=true
Nested.Value=foo
Nested.Deep.Foo=baz
Error=unknown fields in config: json: unknown field "Unused"
Example (Json_unsupported_type)
cfg := &ExampleConfig{}
err := confetti.Load(cfg, confetti.WithJSON(123))
fmt.Printf("Error: %v\n", err)
Output:

Error: unsupported type for WithJSON: int
Example (Ssm)
jsonValue := `{"Host":"ssmhost","Port":9000,"Debug":true,"Nested":{"Value":"ssmval","Deep":{"Foo":"ssmdeep", "Unknown":"unknown"}}}`
ssmName := "CONFETTI_TEST"
region := "us-east-1"

cfg := &ExampleConfig{}
err := confetti.Load(cfg,
	confetti.WithErrOnUnknown(),
	confetti.WithMockedSSM(&mockSSM{value: jsonValue}),
	confetti.WithSSM(ssmName, region),
)

fmt.Printf("Host=%s\n", cfg.Host)
fmt.Printf("Port=%d\n", cfg.Port)
fmt.Printf("Debug=%v\n", cfg.Debug)
fmt.Printf("Nested.Value=%s\n", cfg.Nested.Value)
fmt.Printf("Nested.Deep.Foo=%s\n", cfg.Nested.Deep.Foo)
fmt.Printf("Error=%v\n", err)
Output:

Host=ssmhost
Port=9000
Debug=true
Nested.Value=ssmval
Nested.Deep.Foo=ssmdeep
Error=unknown fields in config: json: unknown field "Unknown"
Example (Ssm_error)
cfg := &ExampleConfig{}
err := confetti.Load(cfg,
	confetti.WithMockedSSM(&mockSSM{value: "error: mock SSM error"}),
	confetti.WithSSM("fail", "us-east-1"),
)
fmt.Printf("Error: %v\n", err)
Output:

Error: failed to get SSM parameter fail: mock SSM error
Example (Ssm_param_not_found)
cfg := &ExampleConfig{}
err := confetti.Load(cfg,
	confetti.WithMockedSSM(&mockSSM{value: ""}),
	confetti.WithSSM("missing", "us-east-1"),
)
fmt.Printf("Error: %v\n", err)
Output:

Error: parameter missing not found or has no value

func WithEnv

func WithEnv(prefix string, opts ...string) envLoader

WithEnv returns a loader that populates struct fields from environment variables.

The prefix is prepended to each field name (in UPPER_SNAKE_CASE) to form the env var name.

The optional separator argument sets the delimiter for slice fields (default is ","). Supports primitive types and slices of primitives (string, int, uint, float, bool).

func WithErrOnUnknown added in v1.1.0

func WithErrOnUnknown() optsLoader

WithErrOnUnknown sets whether to return an error if is present in the source but not defined in the config struct. NOTE: It currently only applies to json and ssm loaders.

func WithJSON

func WithJSON(src any) jsonLoader

WithJSON returns a loader that loads the config struct from a JSON source, which can be: a file path (string), []byte, io.ReadSeeker or io.Reader.

func WithMockedSSM added in v1.2.1

func WithMockedSSM(client SSMAPI) optsMockedSSMLoader

WithMockedSSM returns a loader that uses a mocked SSM client for testing.

func WithSSM

func WithSSM(key string, opts ...string) ssmLoader

WithSSM returns a loader that loads the config struct from an AWS SSM parameter.

The key is the SSM parameter name. The optional region and profile arguments override the default AWS region/profile. The SSM parameter value must be a JSON string matching the config struct.

Usage:

confetti.WithSSM("/my/param", "us-west-2", "myprofile")

Types

type Loader

type Loader interface {
	Load(targetConfig any, ownConfig *confetti) error
}

Loader is the interface implemented by all config loaders (env, SSM, JSON). You can implement your own Loader to support custom sources.

type SSMAPI added in v1.2.1

type SSMAPI interface {
	GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error)
}

SSMAPI is the minimal interface for SSM GetParameter used by ssmLoader.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL