Skip to content

DeanPDX/dotconfig

Repository files navigation

Dotconfig Go Reference Go Report Card Codecov

This package aims to simplify configuration from environment variables. In local development, we can supply a .env file with key/value pairs. When deployed, values come from a secret manager. This is similar to joho/godotenv but the aim here is to not only read the .env file but use reflection to produce a config struct. We also support optional/required fields in the struct and default values.

Usage

Create a .env file in your current working directory with the following contents:

MAX_BYTES_PER_REQUEST='1024'
# Double quotes are fine
API_VERSION="1.19"
# All of these are valie for booleans:
# 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False
IS_DEV='1'
# Raw values with no quotes are also fine
STRIPE_SECRET=sk_test_insertkeyhere
# Right now supporting newlines via "\n" in strings:
WELCOME_MESSAGE='Hello,\nWelcome to the app!\n-The App Dev Team'

You can read from this file and initialize your config with values with the following code:

package main

import (
	"fmt"

	"github.com/DeanPDX/dotconfig"
)

// Our AppConfig with env struct tags:
type AppConfig struct {
	MaxBytesPerRequest int     `env:"MAX_BYTES_PER_REQUEST" default:"2048"` // Defaults to 2048
	APIVersion         float64 `env:"API_VERSION,required"` // Required to be present and not empty string
	IsDev              bool    `env:"IS_DEV,optional"` // Optional: defaults to zero value
	StripeSecret       string  `env:"STRIPE_SECRET"`
	WelcomeMessage     string  `env:"WELCOME_MESSAGE"`
}

func Main() {
	config, err := dotconfig.FromFileName[AppConfig](".env")
	if err != nil {
		fmt.Printf("Error: %v.", err)
	}
	// Config is ready to use. Don't print to console in a real 
	// app. But for the purposes of testing:
	fmt.Println(config)
}

So for local dev we can use this .env file. But when you deploy your app, you set these values from environment variables / secret managers. Your app that consumes this config struct doesn't have to concern itself with where the values came from.

If your key value pairs are coming from a source other than a file, or you want to control file IO yourself, you can call FromReader instead and pass in a io.Reader. There is a runnable example of that in the godoc.

Error Handling and Options

By default, file IO errors in dotconfig.FromFileName won't produce an error. This is because when you are running in the cloud with a secret manager, not finding a .env file is the happy path. If you want to return errors from os.Open you can do so with an option:

config, err := dotconfig.FromFileName[AppConfig](".env", dotconfig.ReturnFileErrors)

By default, if your struct contains fields that don't have an env:"MY_ENV" tag, we assume you want us to ignore those fields. If you want missing env tags to produce errors, use the dotconfig.EnforceStructTags option:

config, err := dotconfig.FromFileName[AppConfig](".env", dotconfig.EnforceStructTags)

dotconfig.FromFileName and dotconfig.FromReader both return multiple wrapped errors. If you want to print all errors to the console you can do that:

type AppConfig struct {
	ForgotToAddStructTag string
	UnsupportedType   	 complex64 `env:"UNSUPPORTED_TYPE"`
}
config, err := dotconfig.FromFileName[AppConfig](".env", dotconfig.EnforceStructTags)
if err != nil {
	fmt.Printf("Error %v.", err)
}
// Output:
// Error: multiple errors:
//  - missing struct tag on field: ForgotToAddStructTag
//  - unsupported field type: complex64

Sometimes you want more fine-grained control of error handling (because certain states you can recover from). If you want to handle each error type, you can use dotconfig.Errors in conjunction with errors.Unwrap and errors.Is. Here's an example where each error type is being handled:

type MyConfig struct {}
_, err := dotconfig.FromFileName[MyConfig](".env", dotconfig.EnforceStructTags)
if err != nil {
	// Get error slice from err
	errs := dotconfig.Errors(err)
	for _, err := range errs {
		// Handle various error types however you want
		switch {
		case errors.Is(errors.Unwrap(err), dotconfig.ErrMissingEnvVar):
			// Handle missing environment variable
		case errors.Is(errors.Unwrap(err), dotconfig.ErrMissingStructTag):
			// Handle missing struct tag
		case errors.Is(errors.Unwrap(err), dotconfig.ErrUnsupportedFieldType):
			// Handle unsupported field type
		case errors.Is(errors.Unwrap(err), dotconfig.ErrMissingRequiredField):
			// Handle missing required field
		}
	}
}

Contributing

Contributions are always welcome. Have a new idea or find a bug? Submit a pull request or create an issue!

IMPORTANT: This package is being used in production and any future updates should maintain backwards compatibility.

About

Simplify configuration of microservices.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages