Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/brumhard/alligotor

The zero configuration configuration package
https://github.com/brumhard/alligotor

configuration go golang

Last synced: 2 months ago
JSON representation

The zero configuration configuration package

Awesome Lists containing this project

README

        

# Alligotor




![golangci-lint](https://github.com/brumhard/alligotor/workflows/golangci-lint/badge.svg)
[![Go Report Card](https://goreportcard.com/badge/github.com/brumhard/alligotor)](https://goreportcard.com/report/github.com/brumhard/alligotor)
[![Go Reference](https://pkg.go.dev/badge/github.com/brumhard/alligotor.svg)](https://pkg.go.dev/github.com/brumhard/alligotor)

The zero configuration configuration package.

## Install

```shell script
go get github.com/brumhard/alligotor
```

## What is Alligotor?

Alligotor is designed to be used as the configuration source for executables (not commands in a command line
application)
for example for api servers or any other long-running applications that need a startup config.

It takes only a few lines of code to get going, and it supports:

- setting defaults just like you're used to from for example json unmarshalling (see this [example](example_test.go))
- reading from YAML and JSON files from io.Reader, local file system or fs.FS
- reading from environment variables
- reading from command line flags
- defining custom source to load config from your preferred source (e.g. etcd)
- extremely simple API
- support for every type (by implementing TextUnmarshaler) and out of the box support for many common ones
- autogenerated property names for each child property in the config, but still configurable via struct tags
- set overwrite order by defining the sources in the preferred order in `alligotor.New()`

---

## Why Alligotor?

There are a lot of configuration packages for Go that give you the ability to load you configuration from several
sources like env vars, command line flags or config files.

Alligotor was designed to have the least configuration effort possible
(autogenerating the property names for the source trough reflection)
while still keeping it customizable. So for example if a config struct looks like the following:

```Go
type cfg struct {
API struct {
Port int
}
}
```

The port value will be loaded by default from the env variable `_API_PORT` and the flag `--api-port` without the
need to set that explicitly.

That's why if you keep the package defaults you only need one function call, and your config struct definition to fill
this struct with values from environment variables, several config files and command line flags or your defined custom
source.

---

## Known unsupported usecases

### Read directly into properties of embedded structs

Generally embedded structs are supported but certain use cases don't work. So for example in the following struct:

```Go
type DB struct {
Host string
}

type Config {
DB
}
```

You can set the value for the DB.Host with the env variable `_DB_HOST` but not with `_HOST` directly.

### Arrays

Since there is no nice way of representing arrays in all config sources (for example environment variables) it's
currently not supported in these sources.

The `ReadersSource` on the other hand can easily read arrays.

---

## Minimal example

```Go
package main

import (
"github.com/brumhard/alligotor"
"go.uber.org/zap/zapcore"
"time"
)

func main() {
// define the config struct
cfg := struct {
SomeList []string
SomeMap map[string]string
API struct {
Enabled bool
LogLevel zapcore.Level
}
DB struct {
HostName string
Timeout time.Duration
}
}{
// could define defaults here
}

// get the values
_ = alligotor.Get(&cfg)
}
```

> Just like with the json package alligotor only supports setting public properties since it relies on reflection.

---

## Custom setup

As alligotor aims for good customizability, the Collector's constructor supports as many sources as you like. Included
in the package are one source for env vars, one for config files (supporting readers, local file system or fs.FS)
and one for cli flags (see [sources](#sources)).

It is shown in the following example.

```Go
// all predefined sources
_ = alligotor.New(
alligotor.NewFilesSource("./test_config.*"),
alligotor.NewEnvSource("TEST"),
alligotor.NewFlagsSource(),
)

// only from env vars with prefix "TEST" and custom separator
_ = alligotor.New(
alligotor.NewEnvSource("TEST", alligotor.WithEnvSeparator("::")),
)
```

As shown in the latter example, the sources support an option to set a custom separator. In case it is not set
explicitly, it will be set to the defaults:

- env vars: `_` (underscore)
- cli flags: `.` (dash)

---

## Sources

For each of the following sources the following example config struct is used.

```Go
// example struct
type Config struct {
Enabled bool
Sub struct {
Port int
}
}
```

### Environment variables

The source for environment variables can be used as follows:

```Go
_ = alligotor.New(
alligotor.NewEnvSource("TEST", alligotor.WithEnvSeparator("::")),
)
```

It supports setting a custom prefix as well as a custom separator. The separator is needed for nested config structs.

So for example for the example struct from above and the defined source configuration the value for the `Port` field
will be read from `TEST::SUB::PORT`.

Like with the other sources the properties name to look up can be changed by adding a struct tag. In that case
add `config:"env=something"` as a struct tag for the `Port` field and it will be read from `TEST::SUB::SOMETHING`.

### Commandline flags

The source for command line flags can be used as follows:

```Go
_ = alligotor.New(
alligotor.NewFlagsSource(alligotor.WithFlagSeparator(".")),
)
```

It supports setting a custom separator, that is needed for nested structs.

So for example for the example struct from above and the defined source configuration the value for the `Port` field
will be read from `--sub.port`.

Like with the other sources the properties name to look up can be changed by adding a struct tag. In that case
add `config:"flag=something"` as a struct tag for the `Port` field and it will be read from `--sub.something`.

In addition the struct tag can be defined as `config:"flag=p"` to set the short name for the flag (`-p`) or any of
`config:"flag=p some"` or `config:"flag=some p"` to overwrite the name and the short name.

To set a flags usage string in addition to the `config` struct tag also the `description` struct tag is read and set as
the flags usage that is returned when the user requests help with `--help` or `-h`.

### Files

The source for files can be used in one of the following ways:

```Go
_ = alligotor.New(
// any io.Reader is supported
alligotor.NewReadersSource(strings.NewReader(`{"key":"value"}`))
)

_ = alligotor.New(
// reads from local fs in this case
alligotor.NewFilesSource("dir/example_config.*", "test2/config.yml"),
)

_ = alligotor.New(
// fsys has a type implementing fs.FS in this case
alligotor.NewFSFilesSource(fsys, "dir/example_config.*", "test2/config.yml")
)
```

`NewReadersSource` reads the config file from any io.Reader so for example a file or an http endpoint.
`NewFilesSource` and `NewFSFilesSource` are simple wrappers around the `ReadersSource` to find the files using glob
patterns on any filesystem (either local FS or fs.FS). This differentiation is used since os.DirFS does not support
propper relative and absolute paths for the local filesystem.

Reading from files works as expected (just like json or yaml unmarshaling). The only difference is that it looks for
fields in a case-insensitive manner.

Of course also here the name can be defined by setting the struct tag to for example `config="file=something"` which
works just like the json or yaml struct tag.

> Currently, only yaml and json files are supported but others will be added if needed.

### Struct tags

Struct tags are used to overwrite the name for the env source that is generated by default. They are defined in the
following format:

```Go
type Config struct {
Enabled bool `config:"key=value,key2=value2"`
}
```

where key could for example be `file` or `env`. The struct tag can also be consumed from custom sources from the `Field`
property `Field.Configs()`, which contains a map from struct tag key to value.

### Custom

Custom sources can be added by implementing the following interfaces. For an example on how to implement a config source
take a look at the [env source](env.go), which implements reading from environment variables in less than 100 lines of
code.

#### ConfigSource

Each config source needs to implement at least the following interface.

```Go
type ConfigSource interface {
Read(field *Field) (interface{}, error)
}
```

As shown it contains only one method that receives a Field instance and returns the value that was found for the field.
For sources that only support setting values as strings (like for example environment variables) just return a byte
slice containing the string and it will automatically be converted to the target type if possible. Any other type is
used directly leading to an error on type mismatch.

> You should not return structs directly since this could lead to errors if some struct properties are set and others
> are not. This would then overwrite the target with the zero value, which is not intended.

The received fields match directly to the fields in the config. So for example for a config struct like the following:

```Go
type Config struct {
Sub struct {
Field string
}
}
```

two fields will be send to the `Read function`, one containing the whole sub struct and one referencing only the Field
property. The structs are included to enable structs that implement the `TextUnmarshaler` interface. If no value is
found for a specific field nil should be returned in order to not override any existing value for that field with an
empty one.

#### ConfigSourceInitializer

If the custom config source depends on some initialization before reading the fields the `ConfigSourceInitializer`
interface can be implemented as well. The method is invoked right before calling the Read function. In the existing
sources this is used for example to read the the files to not do it for every field or read in the environment
variables.

```Go
type ConfigSourceInitializer interface {
Init(fields []Field) error
}
```