Skip to content

Commit

Permalink
Add config package with environment validation and logging utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
eduardolat committed Jul 20, 2024
1 parent 4c10c74 commit c7ba844
Show file tree
Hide file tree
Showing 6 changed files with 404 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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 changes: 34 additions & 0 deletions internal/config/env.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 4 additions & 0 deletions internal/config/env_validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package config

// validateEnv runs additional validations on the environment variables.
func validateEnv(env *Env) {}
154 changes: 154 additions & 0 deletions internal/config/helpers.go
Original file line number Diff line number Diff line change
@@ -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
}
190 changes: 190 additions & 0 deletions internal/config/helpers_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit c7ba844

Please sign in to comment.