Skip to content

Commit

Permalink
add support for default values with environment variables
Browse files Browse the repository at this point in the history
  • Loading branch information
aranw committed Nov 23, 2024
1 parent 75f52d0 commit 3c259d2
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 3 deletions.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# yamlcfg

yamlcfg is a wrapper around the [gopkg.in/yaml.v3](https://gopkg.in/yaml.v3) library and provides a convient way to configure Golang applications with YAML and environment variables.
yamlcfg is a wrapper around the [gopkg.in/yaml.v3](https://gopkg.in/yaml.v3) library and provides a convient way to configure Golang applications with YAML and environment variables.

The library can also automatically call `Validate` functions if present on the given config struct.

## Installation
## Installation

To install, run:

Expand Down Expand Up @@ -66,3 +66,12 @@ func main() {
}

```

## Example with default value


Example `config.yaml` with environment configuration and default values

```yaml
log_level: ${LOG_LEVEL:info}
```
35 changes: 34 additions & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"embed"
"fmt"
"os"
"regexp"

"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -49,7 +50,7 @@ func ParseWithConfig[T any](cfg *T, path string) (*T, error) {
// It will also expand any environment variables in the yaml data
func UnmarshalConfig[T any](cfg *T, data []byte) error {
// expand any $VAR values in the config from environment variables
data = []byte(os.ExpandEnv(string(data)))
data = []byte(parseEnv(string(data)))

dec := yaml.NewDecoder(bytes.NewReader(data))
dec.KnownFields(true)
Expand All @@ -70,3 +71,35 @@ func parse[T any](cfg *T, b []byte) (*T, error) {

return cfg, nil
}

// parseEnv replaces $ENV_NAME and ${ENV_NAME:default} placeholders.
// Default values are only supported with ${ENV_NAME:default}.
func parseEnv(input string) string {
// Regex to match both $ENV_NAME and ${ENV_NAME:default}
re := regexp.MustCompile(`\$(\w+)|\$\{(\w+)(?::([^}]*))?\}`)

return re.ReplaceAllStringFunc(input, func(match string) string {
parts := re.FindStringSubmatch(match)
if len(parts) == 0 {
return match // No match, return as-is
}

// Check if it's $ENV_NAME or ${ENV_NAME:default}
if parts[1] != "" {
// $ENV_NAME style
varName := parts[1]
if value, found := os.LookupEnv(varName); found {
return value
}
return "" // If not found, replace with an empty string
}

// ${ENV_NAME:default} style
varName := parts[2]
defaultValue := parts[3]
if value, found := os.LookupEnv(varName); found {
return value
}
return defaultValue // Fallback to default value if not found
})
}
9 changes: 9 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ func TestParse(t *testing.T) {
qt.Assert(t, qt.Equals(cfg.SomeValue, "this is for testing purposes"))
})

t.Run("successfully parses yaml config with default value", func(t *testing.T) {
cfg, err := Parse[TestStruct]("testdata/test_with_default.yaml")
if err != nil {
t.Fatal(err)
}

qt.Assert(t, qt.Equals(cfg.SomeValue, "this_is_the_default"))
})

t.Run("fails to read unknown file", func(t *testing.T) {
cfg, err := Parse[TestStruct]("testdata/this_file_does_not_exist.yaml")
qt.Assert(t, qt.IsNotNil(err))
Expand Down
1 change: 1 addition & 0 deletions testdata/test_with_default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
some_value: "${SOME_VALUE:this_is_the_default}"

0 comments on commit 3c259d2

Please sign in to comment.