copre

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jan 4, 2022 License: MIT Imports: 15 Imported by: 0

README

copre

Go Report Card PkgGoDev Github Actions codecov FOSSA Status

copre is a small library for loading configuration from multiple sources with a user-defined precedence and merging them. The sources include pflags, environment-variables and files (bring your own file-format).

While copre can be used standalone with only pflags. It was created to accomodate existing projects also utilizing cobra.

Overview

With copre it is straightforward to express how your configuration should be loaded.

copre provides:

  • One-way to populate a configuration struct
  • Struct-tags to specify options for environment variables and flags
  • Minimal defaults, opt-in to features using options instead (intentionally explicit)
  • Flexible Loader-composition as many passes as required (see example Using options)
  • Easy to extend (see example Custom Loader)

Install

go get github.com/trevex/copre

Quickstart

The main entrypoint to loading configuration is the Load-function. The first argument is the pointer to the struct you want to populate and the rest a variadic list of Loader to process.

A simple example could look like this:

type Config struct {
    Foo string `env:"FOO" flag:"foo" yaml:"foo"`
    Bar string `env:"BAR" yaml:"bar"` // Can only be set by env or file
    Baz string `yaml:"baz"` // In this example, can not be set by env or flag
}

// ...
cfg := Config{ Foo: "myDefaultValue" }
err := copre.Load(&cfg,
    copre.File("./config.yaml", yaml.Unmarshal, copre.IgnoreNotFound()),
    copre.Flag(flags), // assuming flags were setup prior
    copre.Env(copre.WithPrefix("MYAPP")), // by default no prefix, so let's set it explicitly
)

As no advanced options (e.g. ComputeEnvKey) are used, env and flag struct-tags have to be explicitly set, if a field should be populated from those sources. However if an environment variable is not set or a flag with the corresponding name does not exist or has an empty value (e.g. empty string), the field will remain untouched. Therefore if no Loader sets a specific field, a value set prior to loading will remain in place. In the above example the configuration-file to be loaded is optional as copre.IgnoreNotFound() was set.

If you want to learn more about copre, checkout the examples below or the API documentation.

Examples

Using options

This example shows off lots of options and hopefully illustrates how you can use options to make copre the glue that composes your configuration:

package main

import (
	"fmt"
	"net"
	"os"

	"github.com/spf13/pflag"
	"github.com/trevex/copre"
	"gopkg.in/yaml.v3"
)

type Config struct {
	// Copre at least aims to support the same types as pflag for environment variables
	ListenIP   net.IP `flag:"listen-ip" env:"LISTEN_IP" yaml:"listenIP"`
	ListenPort int    `yaml:"listenPort"`
	// The Data field will not use a prefix for its environment variable!
	// So will be set by DATA rather than EXAMPLE_DATA
	Data    []byte `env:"DATA,noprefix,base64" flag:"data" yaml:"data"`
	Default string `env:"DEFAULT" yaml:"default"`
	Special string `superenv:"SPECIAL" flag:"special"`
}

func main() {
	cfg := Config{Default: "default"}

	flags := pflag.NewFlagSet("", pflag.ContinueOnError)
	flags.IP("listen-ip", net.IPv4(127, 0, 0, 1), "")
	flags.Int("listen-port", 8080, "")
	flags.BytesBase64("data", []byte{}, "")
	flags.String("default", "", "")
	flags.String("special", "", "")

	// For this example we provide some input data ourselves:
	err := flags.Parse([]string{"--listen-port=9090", "--special=foo"})
	if err != nil {
		panic(err)
	}
	os.Setenv("DATA", "MQ==")
	os.Setenv("SPECIAL", "bar")

	// Let's load the config
	err = copre.Load(&cfg,
		// Okay, here is a little trick, we want to use the pflag defaults in our struct.
		// So we run our first pass over the flags with IncludeUnchanged and later without.
		copre.FlagSet(flags,
			copre.IncludeUnchanged(),
			// Compute flag names for fields without a "flag"-tag using kebab-case
			copre.ComputeFlagName(copre.KebabCase),
		),
		copre.File( // We need at least one file and the unmarshal function
			"./first.yaml", yaml.Unmarshal,
			// But we can add more files to check
			copre.AppendFilePaths("./second.yaml", "./third.yaml"),
			// By default the first will be unmarshalled, but we can also merge all available files
			copre.MergeFiles(),
			// We can provide the following option if no file is okay as well
			copre.IgnoreNotFound(),
		),
		copre.Env(
			// Prefix all environment variables retrieved with EXAMPLE unless noprefix is set in tag
			copre.WithPrefix("EXAMPLE"),
			// Compute environment variable names for fields without "env"-tag
			copre.ComputeEnvKey(copre.UpperSnakeCase),
		),
		copre.FlagSet(flags, copre.ComputeFlagName(copre.KebabCase)),
		copre.Env(
			// You can also change the tag used, to allow multiple sets of precedences
			// or avoid compatiblity issues with other libraries
			copre.OverrideEnvTag("superenv"), // NOTE: similar functionality exists for flags
		),
	)

	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", cfg)
	// Prints:
	// {ListenIP:127.0.0.1 ListenPort:9090 Data:[49] Default:default Special:bar}
}
Custom Loader

The following example is fairly basic, but should give you an idea how to implement Loader. The example uses kelseyhightower/envconfig rather than the inbuilt Env-loader:

package main

import (
	"fmt"
	"os"

	"github.com/kelseyhightower/envconfig"
	"github.com/trevex/copre"
)

type Config struct {
	Debug bool
	Port  int
}

func main() {
	cfg := Config{}
	// Let's setup our environment
	os.Setenv("MYAPP_DEBUG", "true")
	os.Setenv("MYAPP_PORT", "8080")

	// Load but use a custom loader (for simplicity only one loader)
	err := copre.Load(&cfg, copre.LoaderFunc(func(dst interface{}) error {
		return envconfig.Process("myapp", dst)
	}))

	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", cfg)
	// Prints: {Debug:true Port:8080}
}

Q & A

Why?

Depending on the application domain the precedence of loading configuration can differ. For example a CLI tool might have a precendence such as flags > env > file. However services run in a container might prefer a precendence similar to env > file > flags.

At the end of the day the Go ecosystem had plenty options to load configuration, but not to compose its precendence, so hopefully this library accomodates that.

Validate configuration?

Validation is not in scope of copre. Depending on your use-case it might make sense sense to write code validating your configuration. Alternatively there are libraries that can validate it for you (e.g. go-playground/validator or go-validator/validator).

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CamelCase

func CamelCase(path []string) string

CamelCase will convert the provided struct-field path to camel-case. For example:

CamelCase([]string{"Document","HTMLParser"}) // returns "documentHTMLParser"

func KebabCase

func KebabCase(path []string) string

KebabCase will convert the provided struct-field path to kebab-case, e.g.:

KebabCase([]string{"Document","HTMLParser"}) // returns "document-html-parser"

func Load

func Load(dst interface{}, loaders ...Loader) error

Load is the central function tying all the building blocks together. It allows the composition of the loader precedence. For a given pointer to struct dst, an empty instantiation of the same type is create for each loader. Each loader populates its copy and all copies are merged into dst in the specified order.

See the README for several examples.

func LowerSnakeCase

func LowerSnakeCase(path []string) string

LowerSnakeCase takes the path to a struct-field and returns it in lower-case snake-case. For example:

LowerSnakeCase([]string{"Document","HTMLParser"}) // returns "document_html_parser"

func PascalCase

func PascalCase(path []string) string

PascalCase will convert the provided struct-field path to pascal-case. For example:

PascalCase([]string{"Document","HTMLParser"}) // returns "DocumentHTMLParser"

As you can see above the input is already expected to be field names in pascal-case and the names will be joined.

func SplitPathIntoWords

func SplitPathIntoWords(path []string) []string

SplitPathIntoWords is a utility function used by some of the *Case-functions. It takes a field path and returns the list of individual words. For example:

SplitPathIntoWords([]string{"Document", "HTMLParser"}) // returns []string{"Document", "HTML", "Parser"}

func StructWalk

func StructWalk(dst interface{}, fieldMapper FieldMapper) error

StructWalk walks/visits every field of a struct (including nested) and calls fieldMapper for every field. If an internal error is encountered or an error is returned by fieldMapper it is immediately returned and traversal stopped.

func UpperSnakeCase

func UpperSnakeCase(path []string) string

UpperSnakeCase takes the path to a struct-field and returns it in upper-case snake-case. For example:

UpperSnakeCase([]string{"Document","HTMLParser"}) // returns "DOCUMENT_HTML_PARSER"

Types

type EnvOption

type EnvOption interface {
	// contains filtered or unexported methods
}

EnvOption configures how environment variables are used to populate a given structure.

func ComputeEnvKey

func ComputeEnvKey(keyGetter func([]string) string) EnvOption

ComputeEnvKey will remove the requirement to explicitly specify the env-key with a tag. For all fields not explicitly tagged, the name will be computed based on the path by the provided nameGetter function. For example:

ComputeEnvKey(UpperSnakeCase)

func OverrideEnvTag

func OverrideEnvTag(tag string) EnvOption

OverrideEnvTag will change the struct-tag used to retrieve the env-key and options. The main purpose of this option is to allow interoperability with libraries using the same tag.

However it can also be used to enable edge-cases where multiple sets of environment variables are used to populate the same field.

func WithPrefix

func WithPrefix(prefix string) EnvOption

WithPrefix will prefix environment variable names with the specified value unless noprefix option is set in the tag.

type FieldMapper

type FieldMapper func(path []string, field reflect.StructField) (interface{}, error)

FieldMapper is a function that takes the path of a field in a nested structure and field itself, to return a value or an error.

type FileOption

type FileOption interface {
	// contains filtered or unexported methods
}

FileOption configures how given configuration files are used to populate a given structure.

func AppendFilePaths

func AppendFilePaths(paths ...string) FileOption

AppendFilePaths appends paths to the list of paths used to locate configuration files.

See File for details on how configuration files are located.

func ExpandEnv

func ExpandEnv(f ...bool) FileOption

ExpandEnv expands environment variables in loaded configuration files.

func IgnoreNotFound

func IgnoreNotFound(f ...bool) FileOption

IgnoreNotFound surpresses File from returning fs.ErrNotExist errors effectively making the configuration file optional.

func MergeFiles

func MergeFiles(f ...bool) FileOption

MergeFiles changes the default behavior of using the first file found to load configuration. Instead all files that are available will be loaded and unmarshalled into the configuration struct.

type FlagSetOption

type FlagSetOption interface {
	// contains filtered or unexported methods
}

FlagSetOption configures how a pflag.FlagSet is used to populate a given structure.

func ComputeFlagName

func ComputeFlagName(nameGetter func([]string) string) FlagSetOption

ComputeFlagName will remove the requirement to explicitly specify the flag-name with a tag. For all fields not explicitly tagged, the name will be computed based on the path by the provided nameGetter function. For example:

ComputeFlagName(KebabCase)

func IncludeUnchanged

func IncludeUnchanged(f ...bool) FlagSetOption

IncludeUnchanged will also process the values of unchanged flags. Effectively this means the flag defaults, if non zero, will be set as well.

func OverrideFlagTag

func OverrideFlagTag(tag string) FlagSetOption

OverrideFlagTag will change the struct-tag used to retrieve the flag name. The main purpose of this option is to allow interoperability with libraries using the same tag.

However it can also be used to enable edge-cases where multiple flags are used to populate the same field.

type Loader

type Loader interface {
	Process(dst interface{}) error
}

Loader is the interface that needs to be implemented to be able to load configuration from a configuration source. See Env, File or FlagSet for the implementations provided by this library. They only have to implement a single method Process, which populates the passed-in configuration-struct dst and returns an error if problems occur.

func Env

func Env(opts ...EnvOption) Loader

Env implements a Loader, that uses environment variables to retrieve configuration values.

Standalone usage example:

 cfg := struct{ // Illustrating some ways to load bytes from env
		A []byte `env:"NOPREFIX_A,noprefix"`
		B []byte `env:"MYPREFIX_B,hex"`
		C []byte `env:",base64"`
 }{}
 err := Env(WithPrefix("MYPREFIX"), ComputeEnvKey(UpperSnakeCase)).Process(&cfg)

func File

func File(filePath string, unmarshal UnmarshalFunc, opts ...FileOption) Loader

File implements a Loader, that uses a file or files to retrieve configuration values.

By default the provided filePath is used. However this behaviour can be changed using options. If File should look in multiple locations, additional paths can be appended using AppendFilePaths. File will check the existence of those files one by one and load the first found. If MergeFiles is specified, all files will be loaded and unmarshalled in the order specified by the search paths.

Simple standalone example:

err := File("/etc/myapp/config.json", json.Unmarshal, IgnoreNotFound()).Process(&cfg)

Advanced standalone example:

	err := File("./config.json", json.Unmarshal,
   AppendFilePaths("/etc/myapp/myapp.json", path.Join(userHomeDir, ".config/myapp/myapp.json")),
   MergeFiles(),
 ).Process(&cfg)

func FlagSet

func FlagSet(flags *pflag.FlagSet, opts ...FlagSetOption) Loader

FlagSet implements a Loader, that takes a pflag.FlagSet and uses those to retrieve configuration values.

Standalone usage example:

flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
flags.String("foo-bar", "hello", "")
err := flags.Parse([]string{})
// ...
cfg := struct{ FooBar string }{}
loader := FlagSet(flags, IncludeUnchanged(), ComputeFlagName(KebabCase))
err = l.Process(&cfg) // cfg.FooBar will have the default value of the flag "hello"

type LoaderFunc

type LoaderFunc func(dst interface{}) error

LoaderFunc implements the Loader interface for a individual functions.

func (LoaderFunc) Process

func (fn LoaderFunc) Process(dst interface{}) error

Process calls the LoaderFunc underneath.

type UnmarshalFunc

type UnmarshalFunc func(data []byte, dst interface{}) error

UnmarshalFunc is a function that File can use to unmarshal data into a struct. Compatible with the common signature provided by json.Unmarshal, yaml.Unmarshal and similar.

Jump to

Keyboard shortcuts

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