Add config package with environment validation and logging utilities

This commit is contained in:
Luis Eduardo Jeréz Girón
2024-07-19 23:26:44 -06:00
parent 4c10c7499b
commit c7ba844dbc
6 changed files with 404 additions and 0 deletions

7
.env.example Normal file
View File

@@ -0,0 +1,7 @@
# Encryption key is used to encrypt and decrypt the sensitive data stored
# in the database such as database credentials, secret keys, etc.
PBW_ENCRYPTION_KEY=""
# Database connection string for a PostgreSQL database where the pgbackweb
# will store its data.
PBW_POSTGRES_CONN_STRING=""

34
internal/config/env.go Normal file
View File

@@ -0,0 +1,34 @@
package config
import (
"github.com/joho/godotenv"
)
type Env struct {
PBW_ENCRYPTION_KEY *string
PBW_POSTGRES_CONN_STRING *string
}
// GetEnv returns the environment variables.
//
// If there is an error, it will log it and exit the program.
func GetEnv() *Env {
err := godotenv.Load()
if err == nil {
logInfo("using .env file")
}
env := &Env{
PBW_ENCRYPTION_KEY: getEnvAsString(getEnvAsStringParams{
name: "PBW_ENCRYPTION_KEY",
isRequired: true,
}),
PBW_POSTGRES_CONN_STRING: getEnvAsString(getEnvAsStringParams{
name: "PBW_POSTGRES_CONN_STRING",
isRequired: true,
}),
}
validateEnv(env)
return env
}

View File

@@ -0,0 +1,4 @@
package config
// validateEnv runs additional validations on the environment variables.
func validateEnv(env *Env) {}

154
internal/config/helpers.go Normal file
View File

@@ -0,0 +1,154 @@
package config
import (
"errors"
"os"
"strconv"
)
type getEnvAsStringParams struct {
name string
defaultValue *string
isRequired bool
}
// defaultValue returns a pointer to the given value.
func newDefaultValue[T any](value T) *T {
return &value
}
// getEnvAsString returns the value of the environment variable with the given name.
func getEnvAsString(params getEnvAsStringParams) *string { //nolint:all
value, err := getEnvAsStringFunc(params)
if err != nil {
logFatalError(
"error getting env variable",
"name", params.name,
"error", err,
)
}
return value
}
// getEnvAsStringFunc is the outlying function for getEnvAsString.
func getEnvAsStringFunc(params getEnvAsStringParams) (*string, error) {
if params.defaultValue != nil && params.isRequired {
return nil, errors.New("cannot have both a default value and be required")
}
value, exists := os.LookupEnv(params.name)
if !exists && params.isRequired {
return nil, errors.New("required env variable does not exist")
}
if !exists {
if params.defaultValue != nil {
return params.defaultValue, nil
}
return nil, nil
}
return &value, nil
}
type getEnvAsIntParams struct {
name string
defaultValue *int
isRequired bool
}
// getEnvAsInt returns the value of the environment variable with the given name.
func getEnvAsInt(params getEnvAsIntParams) *int { //nolint:all
value, err := getEnvAsIntFunc(params)
if err != nil {
logFatalError(
"error getting env variable",
"name", params.name,
"error", err,
)
}
return value
}
// getEnvAsIntFunc is the outlying function for getEnvAsInt.
func getEnvAsIntFunc(params getEnvAsIntParams) (*int, error) {
if params.defaultValue != nil && params.isRequired {
return nil, errors.New("cannot have both a default value and be required")
}
valueStr, exists := os.LookupEnv(params.name)
if !exists && params.isRequired {
return nil, errors.New("required env variable does not exist")
}
if !exists {
if params.defaultValue != nil {
return params.defaultValue, nil
}
return nil, nil
}
value, err := strconv.Atoi(valueStr)
if err != nil {
return nil, errors.New("env variable is not an integer")
}
return &value, nil
}
type getEnvAsBoolParams struct {
name string
defaultValue *bool
isRequired bool
}
// getEnvAsBool returns the value of the environment variable with the given name.
func getEnvAsBool(params getEnvAsBoolParams) *bool { //nolint:all
value, err := getEnvAsBoolFunc(params)
if err != nil {
logFatalError(
"error getting env variable",
"name", params.name,
"error", err,
)
}
return value
}
// getEnvAsBoolFunc is the outlying function for getEnvAsBool.
func getEnvAsBoolFunc(params getEnvAsBoolParams) (*bool, error) {
if params.defaultValue != nil && params.isRequired {
return nil, errors.New("cannot have both a default value and be required")
}
valueStr, exists := os.LookupEnv(params.name)
if !exists && params.isRequired {
return nil, errors.New("required env variable does not exist")
}
if !exists {
if params.defaultValue != nil {
return params.defaultValue, nil
}
f := false
return &f, nil
}
value, err := strconv.ParseBool(valueStr)
if err != nil {
return nil, errors.New("env variable is not a boolean, must be true or false")
}
return &value, nil
}

View File

@@ -0,0 +1,190 @@
package config
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetEnvAsStringFunc(t *testing.T) {
// Test when environment variable exists
os.Setenv("TEST_ENV", "test_value")
value, err := getEnvAsStringFunc(getEnvAsStringParams{
name: "TEST_ENV",
isRequired: true,
})
assert.NoError(t, err)
assert.Equal(t, "test_value", *value)
os.Unsetenv("TEST_ENV")
// Test when environment variable does not exist, default value is provided, and is not required
value, err = getEnvAsStringFunc(getEnvAsStringParams{
name: "NON_EXISTENT_ENV",
defaultValue: newDefaultValue("default_value"),
isRequired: false,
})
assert.NoError(t, err)
assert.Equal(t, "default_value", *value)
// Test when environment variable does not exist, no default value is provided, and is required
// This should return an error
value, err = getEnvAsStringFunc(getEnvAsStringParams{
name: "NON_EXISTENT_ENV",
isRequired: true,
})
assert.Error(t, err)
assert.Nil(t, value)
// Test when environment variable exists, default value is provided, and is required
os.Setenv("TEST_ENV", "test_value")
value, err = getEnvAsStringFunc(getEnvAsStringParams{
name: "TEST_ENV",
defaultValue: newDefaultValue("default_value"),
})
assert.NoError(t, err)
assert.Equal(t, "test_value", *value)
os.Unsetenv("TEST_ENV")
// Test when environment variable exists, is not required, and no default value is provided
os.Setenv("TEST_ENV", "test_value")
value, err = getEnvAsStringFunc(getEnvAsStringParams{
name: "TEST_ENV",
isRequired: false,
})
assert.NoError(t, err)
assert.Equal(t, "test_value", *value)
os.Unsetenv("TEST_ENV")
// Test when environment variable does not exist, is not required, and no default value is provided
value, err = getEnvAsStringFunc(getEnvAsStringParams{
name: "NON_EXISTENT_ENV",
isRequired: false,
})
assert.NoError(t, err)
assert.Nil(t, value)
// Test when default value and required are both present
// This should return an error
_, err = getEnvAsStringFunc(getEnvAsStringParams{
name: "NON_EXISTENT_ENV",
defaultValue: newDefaultValue("default_value"),
isRequired: true,
})
assert.Error(t, err)
}
func TestGetEnvAsIntFunc(t *testing.T) {
// Test when environment variable exists and is an integer
os.Setenv("TEST_ENV", "123")
value, err := getEnvAsIntFunc(getEnvAsIntParams{
name: "TEST_ENV",
isRequired: true,
})
assert.NoError(t, err)
assert.Equal(t, 123, *value)
os.Unsetenv("TEST_ENV")
// Test when environment variable does not exist, default value is provided, and is not required
value, err = getEnvAsIntFunc(getEnvAsIntParams{
name: "NON_EXISTENT_ENV",
defaultValue: newDefaultValue(456),
})
assert.NoError(t, err)
assert.Equal(t, 456, *value)
// Test when environment variable does not exist, no default value is provided, and is required
// This should return an error
value, err = getEnvAsIntFunc(getEnvAsIntParams{
name: "NON_EXISTENT_ENV",
isRequired: true,
})
assert.Error(t, err)
assert.Nil(t, value)
// Test when environment variable exists, is not an integer, no default value is provided, and is required
// This should return an error
os.Setenv("TEST_ENV", "not_an_integer")
value, err = getEnvAsIntFunc(getEnvAsIntParams{
name: "TEST_ENV",
isRequired: true,
})
assert.Error(t, err)
assert.Nil(t, value)
os.Unsetenv("TEST_ENV")
// Test when environment variable exists, is not required, and no default value is provided
os.Setenv("TEST_ENV", "123")
value, err = getEnvAsIntFunc(getEnvAsIntParams{
name: "TEST_ENV",
isRequired: false,
})
assert.NoError(t, err)
assert.Equal(t, 123, *value)
os.Unsetenv("TEST_ENV")
// Test when environment variable does not exist, is not required, and no default value is provided
value, err = getEnvAsIntFunc(getEnvAsIntParams{
name: "NON_EXISTENT_ENV",
isRequired: false,
})
assert.NoError(t, err)
assert.Nil(t, value)
// Test when default value and required are both present
// This should return an error
_, err = getEnvAsIntFunc(getEnvAsIntParams{
name: "NON_EXISTENT_ENV",
defaultValue: newDefaultValue(1),
isRequired: true,
})
assert.Error(t, err)
}
func TestGetEnvAsBoolFunc(t *testing.T) {
// Test when environment variable exists and is a boolean
os.Setenv("TEST_ENV", "true")
value, err := getEnvAsBoolFunc(getEnvAsBoolParams{
name: "TEST_ENV",
isRequired: true,
})
assert.NoError(t, err)
assert.Equal(t, true, *value)
os.Unsetenv("TEST_ENV")
// Test when environment variable exists, is not a boolean, and is required
os.Setenv("TEST_ENV", "not_a_boolean")
_, err = getEnvAsBoolFunc(getEnvAsBoolParams{
name: "TEST_ENV",
isRequired: true,
})
assert.Error(t, err)
os.Unsetenv("TEST_ENV")
// Test when environment variable exists, is not required, and no default value is provided
os.Setenv("TEST_ENV", "true")
value, err = getEnvAsBoolFunc(getEnvAsBoolParams{
name: "TEST_ENV",
isRequired: false,
})
assert.NoError(t, err)
assert.Equal(t, true, *value)
os.Unsetenv("TEST_ENV")
// Test when environment variable does not exist, is not required, and no default value is provided
value, err = getEnvAsBoolFunc(getEnvAsBoolParams{
name: "NON_EXISTENT_ENV",
isRequired: false,
})
assert.NoError(t, err)
assert.Equal(t, false, *value)
// Test when default value and required are both present
// This should return an error
_, err = getEnvAsBoolFunc(getEnvAsBoolParams{
name: "NON_EXISTENT_ENV",
defaultValue: newDefaultValue(true),
isRequired: true,
})
assert.Error(t, err)
}

15
internal/config/logger.go Normal file
View File

@@ -0,0 +1,15 @@
package config
import (
"log/slog"
"os"
)
func logFatalError(msg string, args ...any) {
slog.Error(msg, args...)
os.Exit(1)
}
func logInfo(msg string, args ...any) {
slog.Info(msg, args...)
}