envopts
Provides a code generator to turn structs annotated for the popular env library into functional options. Functional options are a common pattern in Go but a lot of boiler place is required to use them when the values come from environmnet variables. This code generator aims to solve this problem.
Furthermore it also automatically takes into account any default values specified through the "envDefault" tags. And usefull documentation is generated when a comment is specified for each struct field.
Check out the example
directory with variations and options.
example:
Given the following struct for parsing env variables:
//go:generate go run github.com/fxlib/envopts -type=FooEnv
// FooEnv would describe an environment variable struct parsed using: github.com/caarlos0/env
type FooEnv struct {
Home string `env:"HOME"`
Hosts []string `env:"HOSTS" envSeparator:":"`
Duration time.Duration `env:"DURATION"` // Duration of the timeout
Foo, Dar []env.Options `env:"FOO"`
}
Running go generate
will generate everything required for the functional option pattern:
// Option is a functional option to configure FooEnv
type Option func(*FooEnv)
// FromFooEnv takes fully configured FooEnv and returns it as an option. Can be used to parse environment
// variables manually and provide the result in places where an option argument is expected.
func FromFooEnv(v *FooEnv) Option {
return func(c *FooEnv) { *c = *v }
}
// ParseEnv will parse environment variables into a slice of options. Any options for parsing the
// environment can be supplied, for example to parse under a prefix.
func ParseEnv(eo env.Options) (opts []Option, err error) {
var o FooEnv
opts = append(opts, FromFooEnv(&o))
return opts, env.Parse(&o, eo)
}
// ApplyOptions will merge all options into the resulting FooEnv while also ensuring default values are
// always set.
func ApplyOptions(opts ...Option) (res FooEnv) {
env.Parse(&res, env.Options{Environment: make(map[string]string)})
for _, o := range opts {
o(&res)
}
return
}
// WithHome configures FooEnv
func WithHome(v string) Option { return func(o *FooEnv) { o.Home = v } }
// ...
// WithDar configures FooEnv
func WithDar(v []env.Options) Option { return func(o *FooEnv) { o.Dar = v } }
This could then be used to write code that accepts functional options like this:
// FooService behaves differently based on configuration options
type FooService struct {
cfg FooEnv
}
// NewFooService inits the FooService while taking options
func NewFooService(opts ...Option) (s *FooService) {
s = &FooService{
// set default values, overwritten by any explicitely configured options
cfg: ApplyOptions(opts...),
}
return
}
backlog
- We could read the 'required' tag and error when calling ApplyOptions when this is not provided. But it would required to return an extra
err
value and required options should be passed as separate arguments anyway. - Figure out if we need to take care of nested structs of env options
- Instead of depending on
goimport
being present to clean up unused or used imports it would be nice if we could do everything from our own binary - Add comments to the generated code so developers that read it can follow what is happening. Also for godoc
- Clean up the codebase, proper error handing in walking
- Write proper unit tests instead of lazy smoke tests that call the Go command
- Write some more documentation to get the point of this project across. Tell about features:
- Comment handling
- EnvDefautl handling