configer

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Apr 18, 2024 License: Apache-2.0, MIT Imports: 5 Imported by: 0

README

configer

Lightweight generic configuration package for normal human beings that is ready to go.

Go Reference Go Report Badge

Getting Started

Install the package to your go module:

go get github.com/cdfmlr/configer

Define a struct to hold the configuration and load it from a file:

package main

import (
	"fmt"
	"github.com/cdfmlr/configer"
	"time"
)

type appConfig struct {
	Version         string
	BackendServices []struct {
		Addr    string
		Timeout time.Duration
		Labels  map[string]string
	}
	DB struct {
		URL  string
		Auth struct{ User, Password string }
	}
}

// AppConfig singleton
var AppConfig appConfig

func main() {
	err := configer.New(&AppConfig, configer.TOML).ReadFromFile("./config.toml")

	if err != nil {
		panic("Failed to read config file: " + err.Error())
	}

	fmt.Printf("AppConfig: Version=%s, len(BackendServices)=%d, DB.Auth.User=%s\n", AppConfig.Version, len(AppConfig.BackendServices), AppConfig.DB.Auth.User)
}

Create a config file, e.g. config.toml:

version = "1.0.0"

[[BackendServices]]
Addr = "https://backend1"
Timeout = "5s"
Labels = { env = "dev", region = "us" }

[[BackendServices]]
Addr = "https://backend3"
[BackendServices.Labels]
env = "prod"
region = "eu"
disabled = "true"

[DB]
URL = "mysql://localhost:3306"

[DB.Auth]
User = "root"
Password = "pswd123"

Run the program to read the configuration:

go run main.go

Output:

AppConfig: Version=1.0.0, len(BackendServices)=2, DB.Auth.User=root

Click here to see the full documentation.

Motivation & Philosophy

prologue

I have relied on viper for years, and I love it.

For complex use cases, viper is the best in the wild. Especially for docker-like or kubernetes-like projects I have worked on, I found Viper’s fangs charming to handle the multiple sources of configurations with priority rules. We have few choices to make it a wrap for those Man vs. Wild, though viper is also ferocious and hard to tame.

However, for personal toy projects, for demo versions, for baby microservices, I think viper is too much. I don't need the fangs, I don't need the venom, I just need to read a config file.

Then I wrote a piece of config.go for one of my projects, which is essentially the same as this package but supports only JSON. I copy-paste it to every small project I start, it works well, and I am happy with it. A YAML version is added later, and a TOML version born, too. I think it’s time to turn it into a standalone package and put an end to the era of copy-pasting.

So here it is.

struct is a first-class citizen
struct is a first-class citizen

Instead of offering the popular Key-Value configuration, this package provides a way to load configuration data into a struct, which is more type-safe and easier to use.

I prefer this:

mqgoUrl := Config.Mqgo.URL

to this:

mqgoUrl := Config.Get("Mygo.URL")  // encourage making typos.

I prefer this:

type Config struct {
    Mqgo struct {
        URL string
    }
}

mqgoUrl := Config.Mqgo.URL

to this:

type configKey int

const (
    configKeyMqgoURL configKey = iota
)

mqgoUrl := Config.GetString(configKeyMqgoURL)
single source of truth
single source of truth

Instead of allowing configuration from multiple sources (multiple files, environment variables, command line flags, etc.), that costs you a whole weekend to learn the priority rules, that takes you the following weekdays to debug the unexpected behaviors, this package only supports loading configuration from a single source (a file or a io.Reader), which is simple and clear.

I prefer this:

$ cat > config.toml <<EOF
listen = ":8080"
EOF

$ ./myservice -c config.toml

$ curl http://localhost:8080

to this:

$ cat > /etc/myservice/config.toml <<EOF
listen = ":8080"
EOF

$ cat > ~/.myservice/config.yaml <<EOF
listen: ":8081"
EOF

$ cat > ./config.json <<EOF
{"listen": ":8082"}
EOF

$ export MYSERVICE_LISTEN=":8083"

$ ./myservice --config etcd=etcd://localhost:2379 --listen ":8084"

$ curl http://what.the.hell.is.the.port:8086?
keep it simple, stupid
keep it simple, stupid

Instead of providing a lot of features that you may never use and a lot of dependencies that scare you every time you open the go.mod file, which is definitely an overkill for your simple 10,000-line mirco-service project, this package only depends on the standard library + gopkg.in/yaml.v3 + github.com/BurntSushi/toml.

I prefer this (for small projects):

module github.com/cdfmlr/config

go 1.21.5

require (
	github.com/BurntSushi/toml v1.3.2
	gopkg.in/yaml.v3 v3.0.1
)

to this:

module github.com/spf13/viper

go 1.20

require (
	github.com/fsnotify/fsnotify v1.7.0
	github.com/hashicorp/hcl v1.0.0
	github.com/magiconair/properties v1.8.7
	github.com/mitchellh/mapstructure v1.5.0
	github.com/pelletier/go-toml/v2 v2.2.0
	github.com/sagikazarmark/crypt v0.19.0
	github.com/sagikazarmark/locafero v0.4.0
	github.com/sagikazarmark/slog-shim v0.1.0
	github.com/spf13/afero v1.11.0
	github.com/spf13/cast v1.6.0
	github.com/spf13/pflag v1.0.5
	github.com/stretchr/testify v1.9.0
	github.com/subosito/gotenv v1.6.0
	gopkg.in/ini.v1 v1.67.0
	gopkg.in/yaml.v3 v3.0.1
)

require (
	cloud.google.com/go v0.112.1 // indirect
	cloud.google.com/go/compute v1.24.0 // indirect
	cloud.google.com/go/compute/metadata v0.2.3 // indirect
	cloud.google.com/go/firestore v1.15.0 // indirect
	cloud.google.com/go/longrunning v0.5.5 // indirect
	github.com/armon/go-metrics v0.4.1 // indirect
	github.com/coreos/go-semver v0.3.0 // indirect
	github.com/coreos/go-systemd/v22 v22.3.2 // indirect
	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
	github.com/fatih/color v1.14.1 // indirect
	github.com/felixge/httpsnoop v1.0.4 // indirect
	github.com/go-logr/logr v1.4.1 // indirect
	github.com/go-logr/stdr v1.2.2 // indirect
	github.com/gogo/protobuf v1.3.2 // indirect
	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
	github.com/golang/protobuf v1.5.3 // indirect
	github.com/google/s2a-go v0.1.7 // indirect
	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
	github.com/googleapis/gax-go/v2 v2.12.3 // indirect
	github.com/hashicorp/consul/api v1.28.2 // indirect
	github.com/hashicorp/errwrap v1.1.0 // indirect
	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
	github.com/hashicorp/go-hclog v1.5.0 // indirect
	github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
	github.com/hashicorp/go-multierror v1.1.1 // indirect
	github.com/hashicorp/go-rootcerts v1.0.2 // indirect
	github.com/hashicorp/golang-lru v0.5.4 // indirect
	github.com/hashicorp/serf v0.10.1 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/klauspost/compress v1.17.2 // indirect
	github.com/mattn/go-colorable v0.1.13 // indirect
	github.com/mattn/go-isatty v0.0.17 // indirect
	github.com/mitchellh/go-homedir v1.1.0 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	github.com/nats-io/nats.go v1.34.0 // indirect
	github.com/nats-io/nkeys v0.4.7 // indirect
	github.com/nats-io/nuid v1.0.1 // indirect
	github.com/pkg/errors v0.9.1 // indirect
	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
	github.com/sourcegraph/conc v0.3.0 // indirect
	go.etcd.io/etcd/api/v3 v3.5.12 // indirect
	go.etcd.io/etcd/client/pkg/v3 v3.5.12 // indirect
	go.etcd.io/etcd/client/v2 v2.305.12 // indirect
	go.etcd.io/etcd/client/v3 v3.5.12 // indirect
	go.opencensus.io v0.24.0 // indirect
	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
	go.opentelemetry.io/otel v1.24.0 // indirect
	go.opentelemetry.io/otel/metric v1.24.0 // indirect
	go.opentelemetry.io/otel/trace v1.24.0 // indirect
	go.uber.org/atomic v1.9.0 // indirect
	go.uber.org/multierr v1.9.0 // indirect
	go.uber.org/zap v1.21.0 // indirect
	golang.org/x/crypto v0.21.0 // indirect
	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
	golang.org/x/net v0.22.0 // indirect
	golang.org/x/oauth2 v0.18.0 // indirect
	golang.org/x/sync v0.6.0 // indirect
	golang.org/x/sys v0.18.0 // indirect
	golang.org/x/text v0.14.0 // indirect
	golang.org/x/time v0.5.0 // indirect
	google.golang.org/api v0.171.0 // indirect
	google.golang.org/appengine v1.6.8 // indirect
	google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
	google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 // indirect
	google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
	google.golang.org/grpc v1.62.1 // indirect
	google.golang.org/protobuf v1.33.0 // indirect
)

Features

  • Read configuration from file or io.Reader;
  • Marshaling and unmarshalling of configuration data;
  • Support for JSON, YAML and TOML formats;
  • Binding configuration to a struct;
  • Write configuration back to file or io.Writer;

TODO

  • Export encoding, decoder and encoder interfaces for custom formats;
  • Watch config file changes (I am not sure if it is necessary, due to a config changing, in many cases, indicates a restart of everything related to. This is far beyond the responsibility of a config loader package. Consider use fsnotify at a higher level to handle this properly if necessary.)

License

This project is dual-licensed under Apache 2.0 and MIT terms. Copyright (c) 2023-present CDFMLR

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var JSON = &jsonEncoding{}

JSON encoding for Configer. Supported by encoding/json.

View Source
var TOML = &tomlEncoding{}

TOML encoding for Configer. Supported by github.com/BurntSushi/toml.

View Source
var YAML = &yamlEncoding{}

YAML encoding for Configer. Supported by gopkg.in/yaml.v3.

Functions

This section is empty.

Types

type Configer

type Configer[T Configuration] struct {
	// Config is a pointer to the configuration object.
	Config *T
	// contains filtered or unexported fields
}

Configer is a generic configuration reader and writer.

It reads configuration from an io.Reader or file, decodes it with the provided encoding, and stores the values into the Config field.

It also comes with Write/WriteToFile methods to reverse the process: it encodes the Config object and writes it to an io.Writer or file.

The type parameter T is the configuration type. Actually, It is not required introducing generics here, but if I am not misunderstanding, a type parameter is helpful to keep the Config field type without abstracting it to an interface. (It's an impulsive decision made at the time of 1.18 release. Keeping it for now, as it is at least harmless.)

func New

func New[T Configuration](config *T, encoding encoding) *Configer[T]

New creates a new Configer.

It binds the configuration it reads to the provided config object. The encoding parameter specifies the encoding to use for marshaling/unmarshalling.

func (*Configer[T]) Read

func (c *Configer[T]) Read(src io.Reader) error

Read and decode the configuration from the provided io.Reader. The result is bound to the c.Config.

Example

A simple example to of reading a TOML configuration.

var Config struct {
	Version     string
	HTTPService struct {
		ListenAddr string
		Timeout    time.Duration
	}
	Log struct {
		Level int
		File  string
	}
	BackendServices []struct {
		Addr   string
		Labels map[string]string
	}
	DB struct {
		URL  string
		Auth struct{ User, Password string }
	}
}

tomlData := `
version = "1.0.0"
HTTPService = { ListenAddr = ":8080", Timeout = "5s" }

[Log]
Level = 2
File = "app.log"

[[BackendServices]]
Addr = "https://backend1"
Labels = { env = "dev" }

[[BackendServices]]
Addr = "https://backend2"
Labels = { env = "prod", region = "us" }

[[BackendServices]]
Addr = "https://backend3"
[BackendServices.Labels]
env = "prod"
region = "eu"
disabled = "true"

[DB]
URL = "mysql://localhost:3306"

[DB.Auth]
User = "root"
Password = "pswd123"
`

configer := New(&Config, TOML)

if err := configer.Read(strings.NewReader(tomlData)); err != nil {
	panic(err)
}

fmt.Println(
	Config.Version,
	Config.HTTPService.Timeout,
	len(Config.BackendServices),
	Config.BackendServices[len(Config.BackendServices)-1].Labels["region"],
	Config.DB.Auth.User)
Output:

1.0.0 5s 3 eu root

func (*Configer[T]) ReadFromFile

func (c *Configer[T]) ReadFromFile(file string) error

ReadFromFile reads the configuration from the provided file. Decodes it with c.encoding and binds the result to c.Config.

func (*Configer[T]) Write

func (c *Configer[T]) Write(dst io.Writer) error

Write encodes c.Config with c.encoding and writes it to the provided dst.

Example

A simple example to of writing a JSON configuration.

var Config = struct {
	Version string
	Labels  map[string]string `json:"labels"`
	Comment string            `json:",omitempty"`
}{
	Version: "1.0.0",
	Labels: map[string]string{
		"env":      "dev",
		"disabled": "true",
	},
}

configer := New(&Config, JSON)

var buf bytes.Buffer

if err := configer.Write(&buf); err != nil {
	panic(err)
}

fmt.Println(buf.String())
Output:

{"Version":"1.0.0","labels":{"disabled":"true","env":"dev"}}

func (*Configer[T]) WriteToFile

func (c *Configer[T]) WriteToFile(file string) error

WriteToFile encodes the c.Config with c.encoding and writes it to a file.

type Configuration added in v0.2.0

type Configuration = any

Configuration is a configuration definition.

Configer binds the configuration it reads to a Configuration object. Or it serializes the Configuration object to write it to a file.

Any type can be used as a Configuration as long as it is serializable. It is recommended to use a struct type to represent the configuration.

Jump to

Keyboard shortcuts

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