configer

package module
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2023 License: MIT Imports: 9 Imported by: 0

README

configer

I wrote this package to have a quick and easy way to configure my personal projects. It uses viper as the configuration package under the hood, and the pflag library to work with flags.

Installation

To get the latest version, run

go get github.com/Ozoniuss/configer

Usage

Start by defining a structure for storing the project's overall configuration. The runtime configurations will be unmarshaled into an object of this struct when the project is started, which is passed on via dependency injection.

// Config holds the project's runtime configuration.
type Config struct {
	Server   Server
	Key      int
	Insecure bool
}

// Server defines the configuration options for the server.
type Server struct {
	Address string
	Port    int32
}

Use the configer ConfigOptions struct to specify the project's configuration options. Through the ConfigKey field, specify the field of the config struct the option is attached to:

// getProjectOpts returns all the configuration options enabled for the project.
func getProjectOpts() []cfg.ConfigOption {
	return []cfg.ConfigOption{
		{FlagName: "server-address", Shorthand: "", Value: "localhost", ConfigKey: "server.address",
			Usage: "The address on which the server is listening for connections."},
		{FlagName: "server-port", Shorthand: "", Value: 8080, ConfigKey: "server.port",
			Usage: "The server port opened for incoming connections."},
		{FlagName: "key", Shorthand: "k", Value: 123456, ConfigKey: "key",
			Usage: "The key required to access the server."},
		{FlagName: "insecure", Shorthand: "", Value: true, ConfigKey: "insecure",
			Usage: "Specify whether or not TLS is enabled."},
	}
}

Note that the ConfigKey has not been chosen randomly and has a special meaning. See this section for more details about config keys.

Create the config:

import (
	"fmt"
	cfg "github.com/Ozoniuss/configer"
)

func main() {
	var config Config

	parserOptions := []cfg.ParserOption{
		cfg.WithConfigName("config"),
		cfg.WithConfigType("yml"),
		cfg.WithConfigPath("."),
		cfg.WithConfigPath("./config"),
		cfg.WithEnvPrefix("DEMO"),
		cfg.WithEnvKeyReplacer("_"),
	}

	err := cfg.NewConfig(&config, getProjectOpts(), parserOptions...)
	if err != nil {
		fmt.Println("could not initialize config: %w", err)
		return
	}
	
	fmt.Printf("config: %+v", config)
}

Output:

config: {Server:{Address:localhost Port:8080} Key:123456 Insecure:true}

See the available config options:

$ ./main --help
Usage of main:
      --insecure                Specify whether or not TLS is enabled. (default true)
  -k, --key int                 The key required to access the server. (default 123456)
      --server-address string   The address on which the server is listening for connections. (default "localhost")
      --server-port int         The server port opened for incoming connections. (default 8080)

And you're all set. Specify config options via flags, environment variables, configuration files and default values, in this order of precedence.

$ DEMO_PORT=9090
$ ./main --key 100 --server-address 0.0.0.0
{Server:{Address:0.0.0.0 Port:9090} Key:100 Insecure:true}

Setting the config keys when defining project options

Note that ConfigKey tells the parser which struct field to assign the option to, when unmarshaling into a Config object. So for example if your config looks like this:

type Config struct {
	Val1 MyType1
}
type MyType1 struct {
	Val2 MyType2
}
type MyType2 struct {
	Val3 int
}

Then if you want to define a configuration option that unmarshals to the inner Val3 field of this config, you have to set its config key to be val1.val2.val3.

func getProjectOpts() []cfg.ConfigOption {
	return []cfg.ConfigOption{
		{FlagName: "myflag", Shorthand: "", Value: 100, ConfigKey: "val1.val2.val3",
			Usage: "Config key example."},
	}
}
// config: {Val1:{Val2:{Val3:100}}}

func getProjectOpts() []cfg.ConfigOption {
	return []cfg.ConfigOption{
		{FlagName: "myflag", Shorthand: "", Value: 100, ConfigKey: "val1.val2.val3000",
			Usage: "Config key example."},
	}
}
// config: {Val1:{Val2:{Val3:0}}}

A limitation here is that you can only unmarshal project options to exported struct fields. This has to do with settability in reflection: see the Structs section in the Laws of Reflection blog post.

Custom config keys

It is possible to use custom config keys for your struct, if desired. This can be achieved with the help of the mapstructure struct tag. Take a look at the following example:

type Config struct {
	Val1 MyType1 `mapstructure:"custom1"`
}
type MyType1 struct {
	Val2 MyType2 `mapstructure:"custom2"`
}
type MyType2 struct {
	Val3 int `mapstructure:"custom3"`
}

func getProjectOpts() []cfg.ConfigOption {
	return []cfg.ConfigOption{
		{FlagName: "myflag", Shorthand: "", Value: 100, ConfigKey: "custom1.custom2.custom3",
			Usage: "Config key example."},
	}
}
// config: {Val1:{Val2:{Val3:100}}}

This is particularly useful for multi-word struct keys, especially when associating them with environment variables:

type Config struct {
	Auth Auth
}
type Auth struct {
	JWTSecret string `mapstructure:"jwt_secret"`
}

func getProjectOpts() []cfg.ConfigOption {
	return []cfg.ConfigOption{
		{FlagName: "jwt-secret", Shorthand: "", Value: "dontlook", ConfigKey: "auth.jwt_secret",
			Usage: "Config key example."},
	}
}

// Note that the main function above was used.
// $ export DEMO_AUTH_JWT_SECRET='realsecret' && go run main.go 
// config: {Auth:{JWTSecret:realsecret}}

Personal notes

When working with configs, I often found myself repeating the same patterns for coding a basic configuration setup on my projects. For this reason, I abstracted away that pattern to a custom package that I find useful for most of my projects.

However, this works slightly different that how viper would have worked by default using a similar setup. I find that in some aspects what viper is doing is not intuitive, at least for me, and for this reason the usage of this package is slightly different than viper's usage. For example:

  • Files set via viper.SetConfigFile throw a different error if not found than if set via viper.AddConfigPath combined with viper.SetConfigName and viper.SetConfigType;
  • Writing files using viper.WriteXXX and viper.SafeWriteXXX is a challenge. Additionally, these same functions behave differently if the config file was set with one of the two methods above;
  • The behaviour of the config files is just so different between the two approaches listed above it's just unbelieveable. I often get lost working with config files with viper;
  • viper.SetConfigFile doesn't seem to actually overwrite what was set via viper.AddConfigPath as mentioned in the docs.

The comments I currently left to the viper repository can be found on my open source contribution list. I will likely make some more contributions in the future.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewConfig

func NewConfig(configStruct interface{}, appOptions []ConfigOption, parserOptions ...ParserOption) error

NewConfig generates a new configuration setting for the project, based on the provided config options. It unmarshals the options to the provided struct, which can then be used in the project to read those options.

func WithConfigFile

func WithConfigFile(configFile string) configFileOption

WithConfigFile allows providing a specific configuration file for the parser. This will overwrite all the options that have been specified through WithConfigPath, including the default ones.

This option is not set by default.

func WithConfigName

func WithConfigName(configName string) configNameOption

WithConfigName allows specifying the name of the configuration file the parser looks for. Currently, the parser supports searching for a single file name.

By default, the parser searches for a configuration file named "config".

func WithConfigPath

func WithConfigPath(path string) configPathOption

WithConfigPath allows specifying paths where the parser should search for a configuration options file. The format of this configuration file may be defined using the functions WithConfigType and WithConfigName.

By default, the parser searches for configuration files in the project directory.

func WithConfigType

func WithConfigType(configType string) configTypeOption

WithConfigType allows specifying the type of the configuration file the parser looks for. Currently, the parser supports searching for a single file type.

By default, the parser searches for yaml configuration files.

func WithEnvKeyReplacer

func WithEnvKeyReplacer(replacer string) envKeyReplacerOption

WithEnvKeyReplacer allows to specify a string replacer in order to use a different separator in environment variables than the configuration option key. The separator used for configuration keys is "."

e.g. if the replacer is set to "_", the flag bound to "env.variable" can be set via the environment variable "ENV_VARIABLE".

By default, the parser uses the "_" separator for environment variables.

func WithEnvPrefix

func WithEnvPrefix(prefix string) envPrefixOptions

WithEnvPrefix allows using a specific prefix for the environment variables associated with the project.

e.g. if the prefix is set to "DEMO", for the option parser.option the parser associates the "DEMO_PARSER_OPTION" environment variable, assuming the replacer is set to "_"

By default, the parser doesn't look for any prefix.

func WithReadFlag

func WithReadFlag() readFlagOption

WithReadFlag defines a flag that can be used to explicitly set the location from where the configuration file is read. It ignores the options that were set through WithConfigPath.

If the specified location is a directory, the parser will search for a config file with the name and type specified via WithConfigName and WithConfigType, or the default values if those were not set. Otherwise, if the location is a file, it will try to read the config from that file. If no config file is found, the parser will throw an error.

By default, this flag will not be defined.

func WithSupressLogs added in v0.3.0

func WithSupressLogs() suppressLogsOption

WithSupressLogs suppresses the logs generated by creating a new parser.

By default, logs are active.

func WithWriteFlag

func WithWriteFlag() writeFlagOption

WithWriteFlag defines a flag that can be used to write the project configuration to a file.

If the specified location is a directory, the parser will search for a config file with the name and type specified via WithConfigName and WithConfigType, or the default values if those were not set. Otherwise, if the location is a file, it will try to read the config from that file.

If there already is a configuration file at the specified location, this option will overwrite that file.

By default, this flag will not be defined.

Types

type ConfigOption

type ConfigOption struct {
	// The name of the flag associated with the configuration option. May be
	// empty if the option is associated with no flag.
	FlagName string
	// The shorthand of the flag. Must be empty if a flagname is not specified.
	Shorthand string
	// The value of the configuration option.
	Value any
	// The description of the configuration option. May be displayed in the
	// command line using the --help flag.
	Usage string
	// The key associated with the configuration option. If desired to associate
	// the key with the field of a struct, should be named the same as the
	// struct field (case insensitive). If the field is also a struct, the "."
	// separator should be used for the config key.
	//
	// E.g. if binding to the struct below,
	//
	// =========================
	// | type Config struct {  |
	// | 	Server Server      |
	// | }                     |
	// |                       |
	// | type Server struct {  |
	// | 	Address int        |
	// | }                     |
	// =========================
	//
	// the value of the config option with key "server.address" would get
	// unmarshaled to the server's address in the struct.
	ConfigKey string
}

ConfigOption defines the values for each coniguration option of a microservice.

type ParserOption

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

ParserOption is an interface which allows implementing the functional options pattern for the config parser. By default, the package comes with some default options, and offers the possibility to overwrite them in every project using this package by using the WithXXX public functions exported below.

Jump to

Keyboard shortcuts

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