Skip to content

Instantly share code, notes, and snippets.

@ulexxander
Created January 16, 2023 13:50
Show Gist options
  • Select an option

  • Save ulexxander/45760fbe9c4f5e0c1bfad27f5b7ad6a3 to your computer and use it in GitHub Desktop.

Select an option

Save ulexxander/45760fbe9c4f5e0c1bfad27f5b7ad6a3 to your computer and use it in GitHub Desktop.
Comparing few Go libraries for scanning environment variables into structs
package envcompare_test
import (
"net"
"testing"
"github.com/caarlos0/env/v6"
envsimpler "github.com/go-simpler/env"
"github.com/kelseyhightower/envconfig"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
// github.com/kelseyhightower/envconfig
// envconfig.Process("", &parsed)
// github.com/caarlos0/env/v6
// env.Parse(&parsed)
// github.com/go-simpler/env
// envsimpler.Load(&parsed)
// Needed capabilities:
// 1. Scan into struct, map fields to env variables via tags
// 2. Support standard scalar types, encoding.TextUnmarshaler, maybe slices, arrays and maps
// 3. Should report error if fails to parse variable
// 4. Required / optional variables
func TestKelseyhightowerEnvconfig(t *testing.T) {
t.Log("https://github.com/kelseyhightower/envconfig - 4.3k stars")
t.Run("supports different types", func(t *testing.T) {
t.Setenv("LOG_LEVEL", "trace")
t.Setenv("POSTGRES_HOST", "postgres")
t.Setenv("POSTGRES_PORT", "5432")
t.Setenv("BTC_ENABLED", "true")
t.Setenv("AUTH_CREDENTIALS", "user1:abc123,user2:def456")
t.Setenv("IP_WHITELIST", "192.168.86.102,127.0.0.1")
type config struct {
LogLevel logrus.Level `envconfig:"LOG_LEVEL"`
PostgresHost string `envconfig:"POSTGRES_HOST"`
PostgresPort int `envconfig:"POSTGRES_PORT"`
BTCEnabled bool `envconfig:"BTC_ENABLED"`
AuthCredentials map[string]string `envconfig:"AUTH_CREDENTIALS"`
IPWhitelist []net.IP `envconfig:"IP_WHITELIST"`
}
var parsed config
err := envconfig.Process("", &parsed)
require.NoError(t, err)
require.Equal(t, config{
LogLevel: logrus.TraceLevel,
PostgresHost: "postgres",
PostgresPort: 5432,
BTCEnabled: true,
AuthCredentials: map[string]string{
"user1": "abc123",
"user2": "def456",
},
IPWhitelist: []net.IP{
net.ParseIP("192.168.86.102"),
net.ParseIP("127.0.0.1"),
},
}, parsed)
})
t.Run("reports parsing errors", func(t *testing.T) {
t.Setenv("LOG_LEVEL", "123")
t.Setenv("POSTGRES_PORT", "abc")
t.Setenv("BTC_ENABLED", "???")
type config struct {
LogLevel logrus.Level `envconfig:"LOG_LEVEL"`
PostgresPort int `envconfig:"POSTGRES_PORT"`
BTCEnabled bool `envconfig:"BTC_ENABLED"`
}
var parsed config
err := envconfig.Process("", &parsed)
require.EqualError(t, err, "envconfig.Process: assigning LOG_LEVEL to LogLevel: "+
"converting '123' to type logrus.Level. details: not a valid logrus Level: \"123\"")
t.Fatal("Only first error is reported")
})
t.Run("reports missing variables", func(t *testing.T) {
type config struct {
PostgresHost string `envconfig:"POSTGRES_HOST" required:"true"`
PostgresPort int `envconfig:"POSTGRES_PORT" required:"true"`
}
var parsed config
err := envconfig.Process("", &parsed)
require.EqualError(t, err, "required key POSTGRES_HOST missing value")
t.Fatal("Only first missing field is reported")
})
t.Run("supports default values via tags", func(t *testing.T) {
type config struct {
PostgresHost string `envconfig:"POSTGRES_HOST" required:"true" default:"postgres"`
PostgresPort int `envconfig:"POSTGRES_PORT" required:"true" default:"5432"`
}
var parsed config
err := envconfig.Process("", &parsed)
require.NoError(t, err)
require.Equal(t, config{
PostgresHost: "postgres",
PostgresPort: 5432,
}, parsed)
})
}
func TestCaarlos0Env(t *testing.T) {
t.Log("https://github.com/caarlos0/env - 2.9k stars")
// You can make all fields that don't have a default value be required by setting the RequiredIfNoDef: true in the Options.
t.Run("supports different types", func(t *testing.T) {
t.Setenv("LOG_LEVEL", "trace")
t.Setenv("POSTGRES_HOST", "postgres")
t.Setenv("POSTGRES_PORT", "5432")
t.Setenv("BTC_ENABLED", "true")
// t.Setenv("AUTH_CREDENTIALS", "user1:abc123,user2:def456")
t.Setenv("IP_WHITELIST", "192.168.86.102,127.0.0.1")
type config struct {
LogLevel logrus.Level `env:"LOG_LEVEL"`
PostgresHost string `env:"POSTGRES_HOST"`
PostgresPort int `env:"POSTGRES_PORT"`
BTCEnabled bool `env:"BTC_ENABLED"`
// AuthCredentials map[string]string `env:"AUTH_CREDENTIALS"`
IPWhitelist []net.IP `env:"IP_WHITELIST"`
}
var parsed config
err := env.Parse(&parsed)
require.NoError(t, err)
require.Equal(t, config{
LogLevel: logrus.TraceLevel,
PostgresHost: "postgres",
PostgresPort: 5432,
BTCEnabled: true,
// AuthCredentials: map[string]string{
// "user1": "abc123",
// "user2": "def456",
// },
IPWhitelist: []net.IP{
net.ParseIP("192.168.86.102"),
net.ParseIP("127.0.0.1"),
},
}, parsed)
t.Log("Maps are not supported out the box")
})
t.Run("reports parsing errors", func(t *testing.T) {
t.Setenv("LOG_LEVEL", "123")
t.Setenv("POSTGRES_PORT", "abc")
t.Setenv("BTC_ENABLED", "???")
type config struct {
LogLevel logrus.Level `env:"LOG_LEVEL"`
PostgresPort int `env:"POSTGRES_PORT"`
BTCEnabled bool `env:"BTC_ENABLED"`
}
var parsed config
err := env.Parse(&parsed)
require.EqualError(t, err,
`env: parse error on field "LogLevel" of type "logrus.Level": not a valid logrus Level: "123"; `+
`parse error on field "PostgresPort" of type "int": strconv.ParseInt: parsing "abc": invalid syntax; `+
`parse error on field "BTCEnabled" of type "bool": strconv.ParseBool: parsing "???": invalid syntax`)
})
t.Run("reports missing variables", func(t *testing.T) {
type config struct {
PostgresHost string `env:"POSTGRES_HOST,required"`
PostgresPort int `env:"POSTGRES_PORT,required"`
}
var parsed config
err := env.Parse(&parsed)
require.EqualError(t, err,
`env: required environment variable "POSTGRES_HOST" is not set; `+
`required environment variable "POSTGRES_PORT" is not set`)
})
t.Run("reports empty variables", func(t *testing.T) {
t.Setenv("POSTGRES_HOST", "")
t.Setenv("POSTGRES_PORT", "")
type config struct {
PostgresHost string `env:"POSTGRES_HOST,notEmpty"`
PostgresPort int `env:"POSTGRES_PORT,notEmpty"`
}
var parsed config
err := env.Parse(&parsed)
require.EqualError(t, err,
`env: environment variable "POSTGRES_HOST" should not be empty; `+
`environment variable "POSTGRES_PORT" should not be empty`)
})
t.Run("supports default values via tags", func(t *testing.T) {
type config struct {
PostgresHost string `env:"POSTGRES_HOST,required" envDefault:"postgres"`
PostgresPort int `env:"POSTGRES_PORT,required" envDefault:"5432"`
}
var parsed config
err := env.Parse(&parsed)
require.NoError(t, err)
require.Equal(t, config{
PostgresHost: "postgres",
PostgresPort: 5432,
}, parsed)
})
t.Run("required if no def", func(t *testing.T) {
t.Setenv("POSTGRES_HOST", "")
type config struct {
PostgresHost string `env:"POSTGRES_HOST"`
PostgresPort int `env:"POSTGRES_PORT" envDefault:"5432"`
}
var parsed config
err := env.Parse(&parsed, env.Options{
RequiredIfNoDef: true,
})
require.EqualError(t, err, `env: required environment variable "POSTGRES_HOST" is not set`)
})
}
func TestGoSimplerEnv(t *testing.T) {
t.Log("https://github.com/go-simpler/env - 36 stars")
// - No "not empty" option.
// - Use the expand option to automatically expand the value of the environment variable using os.Expand.
// - For cases where most environment variables are required, strict mode is available,
// in which all variables without the default tag are treated as required.
// To enable this mode, use the WithStrictMode option:
t.Run("supports different types", func(t *testing.T) {
t.Setenv("LOG_LEVEL", "trace")
t.Setenv("POSTGRES_HOST", "postgres")
t.Setenv("POSTGRES_PORT", "5432")
t.Setenv("BTC_ENABLED", "true")
// t.Setenv("AUTH_CREDENTIALS", "user1:abc123,user2:def456")
t.Setenv("IP_WHITELIST", "192.168.86.102 127.0.0.1")
type config struct {
LogLevel logrus.Level `env:"LOG_LEVEL"`
PostgresHost string `env:"POSTGRES_HOST"`
PostgresPort int `env:"POSTGRES_PORT"`
BTCEnabled bool `env:"BTC_ENABLED"`
// AuthCredentials map[string]string `env:"AUTH_CREDENTIALS"`
IPWhitelist []net.IP `env:"IP_WHITELIST"`
}
var parsed config
err := envsimpler.Load(&parsed)
require.NoError(t, err)
require.Equal(t, config{
LogLevel: logrus.TraceLevel,
PostgresHost: "postgres",
PostgresPort: 5432,
BTCEnabled: true,
// AuthCredentials: map[string]string{
// "user1": "abc123",
// "user2": "def456",
// },
IPWhitelist: []net.IP{
net.ParseIP("192.168.86.102"),
net.ParseIP("127.0.0.1"),
},
}, parsed)
t.Log("Default slice separator is space, not comma")
t.Log("Maps are not supported out the box")
})
t.Run("reports parsing errors", func(t *testing.T) {
t.Setenv("LOG_LEVEL", "123")
t.Setenv("POSTGRES_PORT", "abc")
t.Setenv("BTC_ENABLED", "???")
type config struct {
LogLevel logrus.Level `env:"LOG_LEVEL"`
PostgresPort int `env:"POSTGRES_PORT"`
BTCEnabled bool `env:"BTC_ENABLED"`
}
var parsed config
err := envsimpler.Load(&parsed)
require.EqualError(t, err, `unmarshaling text: not a valid logrus Level: "123"`)
t.Fatal("Only first error is reported")
})
t.Run("reports missing variables", func(t *testing.T) {
type config struct {
PostgresHost string `env:"POSTGRES_HOST,required"`
PostgresPort int `env:"POSTGRES_PORT,required"`
}
var parsed config
err := envsimpler.Load(&parsed)
require.EqualError(t, err, `env: [POSTGRES_HOST POSTGRES_PORT] are required but not set`)
})
t.Run("reports empty variables", func(t *testing.T) {
t.Setenv("POSTGRES_HOST", "")
t.Setenv("POSTGRES_PORT", "")
type config struct {
PostgresHost string `env:"POSTGRES_HOST,required"`
PostgresPort int `env:"POSTGRES_PORT,required"`
}
var parsed config
err := envsimpler.Load(&parsed)
require.EqualError(t, err, `parsing int: strconv.ParseInt: parsing "": invalid syntax`)
t.Fatal("Not possible to ensure variable is not set to empty value")
})
t.Run("supports default values via tags", func(t *testing.T) {
type config struct {
PostgresHost string `env:"POSTGRES_HOST" default:"postgres"`
PostgresPort int `env:"POSTGRES_PORT" default:"5432"`
}
var parsed config
err := envsimpler.Load(&parsed)
require.NoError(t, err)
require.Equal(t, config{
PostgresHost: "postgres",
PostgresPort: 5432,
}, parsed)
t.Fatal("Not possible to ensure variable is not set to empty value")
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment