zconfig

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Dec 26, 2018 License: MIT Imports: 14 Imported by: 0

README

zConfig Build Status Godoc license

zConfig is a Golang, extensible, reflection-based configuration and dependency injection tool.

Usage

zconfig primary feature is an extensible configuration repository. To use it, simply define a configuration struture and feed it to the zconfig.Configure method. You can use the key, descriptionand default tags to define which key to use.

type Configuration struct {
	Addr string `key:"addr" description:"address the server should bind to" default:":80"`
}

func main() {
	var c Configuration
	err := zconfig.Configure(&c)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	//...
	err := http.ListenAndServe(c.Addr, nil)
	//...
}

Once compiled, the special flag help can be passed to the binary to display a list of the available configuration keys, in their cli and env form, as well as their description and default values.

$ ./a.out --help
Keys:
addr	ADDR	address the server should bind to	(:80)

Configurations can be nested into structs to improve usability, and the keys of the final parameters are prefixed by the keys of all parents.

type Configuration struct {
	Server struct{
		Addr string `key:"addr" description:"address the server should bind to" default:":80"`
	} `key:"server"`
}

func main() {
	var c Configuration
	err := zconfig.Configure(&c)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	//...
	err := http.ListenAndServe(c.Server.Addr, nil)
	//...
}
$ ./a.out --help
Keys:
server.addr	SERVER_ADDR	address the server should bind to	(:80)
Initialization

zconfig does handle dependency initialization. Any reachable field of your configuration struct (whatever the nesting level) that implements the zconfig.Initializable interface will be Inited during the configuration process.

Here is an example with our internal Redis wrapper.

package zredis

import (
	"time"
	"github.com/go-redis/redis"
)

type Client struct {
	*redis.Client
	Address         string        `key:"address" description:"address and port of the redis"`
	ConnMaxLifetime time.Duration `key:"connection-max-lifetime" description:"maximum duration of open connection" default:"30s"`
	MaxOpenConns    int           `key:"max-open-connections" description:"maximum of concurrent open connections" default:"10"`

}

func (c *Client) Init() (err error) {
	c.Client = redis.NewClient(&redis.Options{
		Network:     "tcp",
		Addr:        c.Address,
		IdleTimeout: c.ConnMaxLifetime,
		PoolSize:    c.MaxOpenConns,
	})

	_, err = c.Ping()
	return err
}

Now, whenever we need to use a Redis database in our services, we can simply declare the dependency in the configuration struct and go on without worrying about initializing it, liberating your service from pesky initialization code.

package main

import (
	"zredis"
	"github.com/synthesio/zconfig"
)

type Service struct {
	Redis *zredis.Client `key:"redis"`
}

func main() {
	var s Service
	err := zconfig.Configure(&s)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	res, err := s.Redis.Get("foo").Result()
	// ...
}
Injection

The zconfig processor understands a set of tags used for injecting one field into another, thus sharing resources. The inject-as tag defines an injection source and the key used to identify it, while the inject tag defines an injection target and the key of the source to use.

Any type can be injected as long as the source is assignable to the target. This is especially useful to allow sharing common configuration fields or even whole structs like a database handle.

type Service struct {
	Foo struct{
		Root string `inject:"bar-root"`
	}
	Bar struct{
		Root string `inject-as:"bar-root"`
	}
}

Note that the injection system isn't tied to the configuration one: you don't need your injection source or target to be part of a chain of keyed structs.

Also, zconfig will return an error if given a struct with a cycle in it, the same way the compiler will refuse compiling a type definition with cycles.

Repository

The configuration of the fields by the processor is deleguated to a configuration repository. A repository is a list of providers and parsers than respectively retrieve configuration key's raw values and transform them into the expected value. The configuration itself is done via a hook added to the default provider.

Providers

The repository is based around a slice of structs implementing the zconfig.Provider interface. Each provider has a name and priority and they are requested in priority order when a key is needed.

The args and env providers are registered on the default provider, and instances are exposed as zconfig.Args and zconfig.Env for convenience so you can use them for other purposes, like getting the path for a YAMLProvider file for example.

You can use the zconfig.Repository.AddProviders(...zconfig.Provider) method to add a new provider to a given repository, or the zconfig.AddProviders(...zconfig.Provider) shortcut to add one to the default repository.

Parsers

Parsing the raw string values into the actual variables for the configuration struct is handled by the configuration repository. The repository holds a slice of zconfig.Parser called in order until one of them is able to handle the type of the destination field.

The following types actually have parsers in the default repository:

  • encoding.TextUnmarshaller
  • encoding.BinaryUnmarshaller
  • (u)?int(32|64)?
  • float(32|64)
  • string
  • []string
  • bool
  • time.Duration
  • regexp.Regexp

You can use the zconfig.Repository.AddParsers(...zconfig.Parser) method to add a new parser to a given repository, or the zconfig.AddParsers(...zconfig.Parser) shortcut to add to the default repository.

For convenience, the various parsers of the default repository are available as zconfig.DefaultParsers.

Hooks

The standard way to extend the behavior of zconfig is to implement a new hook. The default Processor uses two of them: the zconfig.Repository.Hook and the zconfig.Initialize hook.

Once the configuration struct is parsed and verified, the processor runs each hook registered on the fields of the struct, allowing custom behavior to be implemented for your systems. The interface for a hook is really simple:

type Hook func(field *Field) error

Each encountered field will be passed to the hook, lowest dependencies first, meaning that you can assume that the hook was executed on any child of the current field before the field itself. The fields that have a configuration key can be distinguished by the Configurable bool attribute being true. Note that the hooks aren't executed on the injection targets to avoid processing them twice.

You can use the zconfig.Processor.AddHooks(...zconfig.Hook) method to add a new hook to a given processor, or the zconfig.AddHooks(...zconfig.Hook) shortcut to add one to the default processor.

Documentation

Index

Constants

View Source
const (
	TagInjectAs    = "inject-as"
	TagInject      = "inject"
	TagKey         = "key"
	TagDefault     = "default"
	TagDescription = "description"
)

Variables

View Source
var (
	Args = NewArgsProvider()
	Env  = NewEnvProvider()
)
View Source
var DefaultParsers = []Parser{
	ParserFunc{reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem(), parseTextUnmarshaler},
	ParserFunc{reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem(), parseBinaryUnmarshaler},
	ParserFunc{reflect.TypeOf([]string(nil)), parseStringSlice},
	ParserFunc{reflect.TypeOf(string("")), parseString},
	ParserFunc{reflect.TypeOf(bool(false)), parseBool},
	ParserFunc{reflect.TypeOf(float32(0)), parseFloat},
	ParserFunc{reflect.TypeOf(float64(0)), parseFloat},
	ParserFunc{reflect.TypeOf(uint(0)), parseUint},
	ParserFunc{reflect.TypeOf(uint8(0)), parseUint},
	ParserFunc{reflect.TypeOf(uint16(0)), parseUint},
	ParserFunc{reflect.TypeOf(uint32(0)), parseUint},
	ParserFunc{reflect.TypeOf(uint64(0)), parseUint},
	ParserFunc{reflect.TypeOf(int(0)), parseInt},
	ParserFunc{reflect.TypeOf(int8(0)), parseInt},
	ParserFunc{reflect.TypeOf(int16(0)), parseInt},
	ParserFunc{reflect.TypeOf(int32(0)), parseInt},
	ParserFunc{reflect.TypeOf(int64(0)), parseInt},
	ParserFunc{reflect.TypeOf(time.Duration(0)), parseDuration},
	ParserFunc{reflect.TypeOf(new(regexp.Regexp)), parseRegexp},
}

Functions

func AddHooks

func AddHooks(hooks ...Hook)

Add a hook to the default repository.

func AddParsers

func AddParsers(parsers ...Parser)

Add a parser to the default repository.

func AddProviders

func AddProviders(providers ...Provider)

Add a provider to the default repository.

func Configure

func Configure(s interface{}) error

Configure a service using the default processor.

func Initialize

func Initialize(field *Field) error

Types

type ArgsProvider

type ArgsProvider struct {
	Args map[string]string
}

A Provider that implements the repository.Provider interface.

func NewArgsProvider

func NewArgsProvider() (p *ArgsProvider)

Init fetch all keys available in the command-line and initialize the provider internal storage.

func (ArgsProvider) Name

func (ArgsProvider) Name() string

Name of the provider.

func (ArgsProvider) Priority

func (ArgsProvider) Priority() int

Priority of the provider.

func (*ArgsProvider) Retrieve

func (p *ArgsProvider) Retrieve(key string) (value string, found bool, err error)

Retrieve will return the value from the parsed command-line arguments. Arguments are parsed the first time the method is called. Arguments are expected to be in the form `--key=value` exclusively (for now).

type EnvProvider

type EnvProvider struct {
	Env map[string]string
}

A Provider that implements the repository.Provider interface.

func NewEnvProvider

func NewEnvProvider() (p *EnvProvider)

Init fetch all keys available in the command-line and initialize the provider internal storage.

func (EnvProvider) FormatKey

func (EnvProvider) FormatKey(key string) (env string)

func (EnvProvider) Name

func (EnvProvider) Name() string

Name of the provider.

func (EnvProvider) Priority

func (EnvProvider) Priority() int

Priority of the provider.

func (*EnvProvider) Retrieve

func (p *EnvProvider) Retrieve(key string) (value string, found bool, err error)

Retrieve will return the value from the parsed environment variables. Variables are parsed the first time the method is called.

type Field

type Field struct {
	StructField *reflect.StructField
	Value       reflect.Value
	Path        string

	Parent   *Field
	Children []*Field

	Tags             *structtag.Tags
	Key              string
	Configurable     bool
	ConfigurationKey string
}

func (*Field) FullTag

func (f *Field) FullTag(name string) (string, bool)

func (*Field) Inject

func (f *Field) Inject(s *Field) (err error)

func (*Field) IsAnonymous

func (f *Field) IsAnonymous() bool

func (*Field) IsLeaf

func (f *Field) IsLeaf() bool

func (*Field) Tag

func (f *Field) Tag(name string) (string, bool)

type Hook

type Hook func(field *Field) error

A Hook can be used to act upon every field visited by the repository when configuring a service.

type Initializable

type Initializable interface {
	Init() error
}

type Parser

type Parser interface {
	Parse(reflect.Type, string) (reflect.Value, error)
	CanParse(reflect.Type) bool
}

Parser is the interface implemented by a struct that can convert a raw string representation to a given type.

type ParserFunc

type ParserFunc struct {
	Type reflect.Type
	Func func(reflect.Type, string) (reflect.Value, error)
}

func (ParserFunc) CanParse

func (p ParserFunc) CanParse(typ reflect.Type) bool

func (ParserFunc) Parse

func (p ParserFunc) Parse(typ reflect.Type, raw string) (val reflect.Value, err error)

type Processor

type Processor struct {
	// contains filtered or unexported fields
}

A Processor handle the service processing and execute hooks on the resulting fields.

func NewProcessor

func NewProcessor(hooks ...Hook) *Processor

func (*Processor) AddHooks

func (p *Processor) AddHooks(hooks ...Hook)

func (*Processor) Process

func (p *Processor) Process(s interface{}) error

type Provider

type Provider interface {
	Retrieve(key string) (value string, found bool, err error)
	Name() string
	Priority() int
}

Provider is the interface implemented by all entity a configuration key can be retrieved from.

type Repository

type Repository struct {
	// contains filtered or unexported fields
}

A Repository is list of configuration providers and hooks.

func (*Repository) AddParsers

func (r *Repository) AddParsers(parsers ...Parser)

Register allow anyone to add a custom parser to the list.

func (*Repository) AddProviders

func (r *Repository) AddProviders(providers ...Provider)

Register a new Provider in this repository.

func (*Repository) Hook

func (r *Repository) Hook(f *Field) (err error)

func (*Repository) Parse

func (r *Repository) Parse(typ reflect.Type, parameter string) (val reflect.Value, err error)

Parse the parameter depending on the kind of the field, returning an appropriately typed reflect.Value.

func (*Repository) Retrieve

func (r *Repository) Retrieve(key string) (value, provider string, found bool, err error)

Retrieve a key from the provider, by priority order.

Jump to

Keyboard shortcuts

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