Skip to content

Commit

Permalink
Merge pull request #1 from ndisidore/nathan/ctx-and-more-types
Browse files Browse the repository at this point in the history
Introduce more types and add context so we can slog from said ctx
  • Loading branch information
ndisidore authored Oct 2, 2024
2 parents 5d65ace + 0a01b5b commit bc5c802
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 49 deletions.
25 changes: 12 additions & 13 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go
name: Build & Test

on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 1.22

- name: Build
run: go build -v ./...
- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...
- name: Test
run: go test -v ./...
29 changes: 17 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ This aims to bridge that gap. It was created with the intention of allowing envi

### With flags.

Lets look at the default signature for a flag, here for `uint64`
Lets look at the default signature for a flag, here for `uint64`

```
func Uint64(name string, value uint64, usage string) *uint64
```

With `go-env` we can provide a value for, ahem, `value` that is a type checked env in a single function call (thanks generics!).

```go
import (
"os"
Expand All @@ -32,20 +34,22 @@ import (
)

os.Setenv("FIRST_FLAG_ENV_VAR", "42")
var ctx = context.Background()
// since `FIRST_FLAG_ENV_VAR` env is set, this will parse and use that value
var myFlag1 = flags.Uint64("first-flag-cmd-line", env.MustFromEnvOrDefault("FIRST_FLAG_ENV_VAR", 7), "an example uint64 flag") *uint64
var myFlag1 = flags.Uint64("first-flag-cmd-line", env.MustFromEnvOrDefault(ctx, "FIRST_FLAG_ENV_VAR", 7), "an example uint64 flag") *uint64
// since `SECOND_FLAG_ENV_VAR` env is not set, this will fallback to the default value
var myFlag2 = flags.Uint64("second-flag-cmd-line", env.MustFromEnvOrDefault("SECOND_FLAG_ENV_VAR", 7), "an example uint64 flag") *uint64
var myFlag2 = flags.Uint64("second-flag-cmd-line", env.MustFromEnvOrDefault(ctx, "SECOND_FLAG_ENV_VAR", 7), "an example uint64 flag") *uint64
fmt.Printf("%d; %d", *myFlag1, *myFlag2) // outputs -> 42; 7
```

Since `MustFromEnvOrDefault` (and its error returning counterpart `FromEnvOrDefault`) use generics and analyze the type dynamically, the call looks very similar for other flag data types.

```go
var myStrFlag = flags.String("my-str-flag", env.MustFromEnvOrDefault("MY_STR", "a string"), "an example string flag") *string
var myBoolFlag = flags.Bool("my-bool-flag", env.MustFromEnvOrDefault("MY_BOOL", true), "an example bool flag") *bool
var myDurFlag = flags.Duration("my-duration-flag", env.MustFromEnvOrDefault("MY_DURATION", time.Second * 5), "an example duration flag") *time.Duration
var myFloatFlag = flags.Float64("my-float64-flag", env.MustFromEnvOrDefault("MY_FLOAT64", 7.11), "an example float64 flag") *float64
var ctx = context.Background()
var myStrFlag = flags.String("my-str-flag", env.MustFromEnvOrDefault(ctx, "MY_STR", "a string"), "an example string flag") *string
var myBoolFlag = flags.Bool("my-bool-flag", env.MustFromEnvOrDefault(ctx, "MY_BOOL", true), "an example bool flag") *bool
var myDurFlag = flags.Duration("my-duration-flag", env.MustFromEnvOrDefault(ctx, "MY_DURATION", time.Second * 5), "an example duration flag") *time.Duration
var myFloatFlag = flags.Float64("my-float64-flag", env.MustFromEnvOrDefault(ctx, "MY_FLOAT64", 7.11), "an example float64 flag") *float64
```

### Without flags.
Expand All @@ -54,14 +58,15 @@ Of course, usage with [flag](https://pkg.go.dev/flag) is not strictly necessary

```go
var (
myStrVar = env.MustFromEnvOrDefault("MY_STR", "a string")
myBoolVar = env.MustFromEnvOrDefault("MY_BOOL", true)
ctx = context.Background()
myStrVar = env.MustFromEnvOrDefault(ctx, "MY_STR", "a string")
myBoolVar = env.MustFromEnvOrDefault(ctx, "MY_BOOL", true)
)

// --- or ---

myStrVar2, err := env.FromEnvOrDefault("MY_STR", "a string")
myStrVar2, err := env.FromEnvOrDefault(ctx, "MY_STR", "a string")
if err != nil { ... }
myBoolVar2, err := env.FromEnvOrDefault("MY_BOOL", true)
myBoolVar2, err := env.FromEnvOrDefault(ctx, "MY_BOOL", true)
if err != nil { ... }
```
```
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/ndisidore/go-env

go 1.18
go 1.22
24 changes: 24 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package env
import (
"errors"
"os"
"time"
)

type (
envParseOpts struct {
envLoader EnvLoader
separator string
defaultOnError bool
timeLayout string
sensitive bool
}

// EnvLoader is an alias for a function that loads values from the env. It mirrors the signature of os.Getenv.
Expand All @@ -24,6 +27,7 @@ var (
envLoader: os.Getenv,
separator: ",",
defaultOnError: false,
timeLayout: time.RFC3339,
}
)

Expand Down Expand Up @@ -60,3 +64,23 @@ func WithFallbackToDefaultOnError(fallback bool) EnvParseOption {
return nil
}
}

// WithTimeLayout allows overriding the time layout used to parse time.Time values. Default is RFC3339.
func WithTimeLayout(layout string) EnvParseOption {
return func(o *envParseOpts) error {
if layout == "" {
return errors.New("time layout cannot be empty string")
}

o.timeLayout = layout
return nil
}
}

// WithSensitive informs the parser that the value being parsed is sensitive and should not be logged.
func WithSensitive(sensitive bool) EnvParseOption {
return func(o *envParseOpts) error {
o.sensitive = sensitive
return nil
}
}
57 changes: 49 additions & 8 deletions parser.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package env

import (
"context"
"fmt"
"log"
"log/slog"
"net/url"
"os"
"strconv"
"strings"
"time"
Expand All @@ -11,17 +14,18 @@ import (
type (
// Parseable represents the types the parser is capable of handling.
Parseable interface {
string | bool | int | uint | int64 | uint64 | float64 | time.Duration | []string | []bool | []int | []uint | []int64 | []uint64 | []float64
string | bool | int | uint | int64 | uint64 | float64 | time.Duration | time.Time | url.URL | []string | []bool | []int | []uint | []int64 | []uint64 | []float64 | []time.Duration | []time.Time | []url.URL
}
)

// MustFromEnvOrDefault attempts to parse the environment variable provided. If it is empty or missing, the default value is used.
//
// If an error is encountered, depending on whether the `WithFallbackToDefaultOnError` option is provided it will either fallback or fatally log & exit.
func MustFromEnvOrDefault[T Parseable](envVar string, defaultVal T, opts ...EnvParseOption) (dest T) {
parsed, err := FromEnvOrDefault(envVar, defaultVal, opts...)
func MustFromEnvOrDefault[T Parseable](ctx context.Context, envVar string, defaultVal T, opts ...EnvParseOption) (dest T) {
parsed, err := FromEnvOrDefault(ctx, envVar, defaultVal, opts...)
if err != nil {
log.Fatal(err)
slog.Default().ErrorContext(ctx, "failed to parse env var", slog.String("env_var", envVar), slog.String("error", err.Error()))
os.Exit(1)
}

return parsed
Expand All @@ -30,7 +34,7 @@ func MustFromEnvOrDefault[T Parseable](envVar string, defaultVal T, opts ...EnvP
// FromEnvOrDefault attempts to parse the environment variable provided. If it is empty or missing, the default value is used.
//
// If an error is encountered, depending on whether the `WithFallbackToDefaultOnError` option is provided it will either fallback or return the error back to the client.
func FromEnvOrDefault[T Parseable](envVar string, defaultVal T, opts ...EnvParseOption) (dest T, err error) {
func FromEnvOrDefault[T Parseable](ctx context.Context, envVar string, defaultVal T, opts ...EnvParseOption) (dest T, err error) {
parseOpts := &defaultParseOptions
for _, opt := range opts {
if err := opt(parseOpts); err != nil {
Expand Down Expand Up @@ -65,12 +69,16 @@ func FromEnvOrDefault[T Parseable](envVar string, defaultVal T, opts ...EnvParse
v, err = strconv.ParseFloat(envStr, 64)
case time.Duration:
v, err = time.ParseDuration(envStr)
case time.Time:
v, err = time.Parse(parseOpts.timeLayout, envStr)
case url.URL:
v, err = url.Parse(envStr)
case []string:
v = strings.Split(envStr, parseOpts.separator)
case []bool:
vs := make([]bool, 0)
for i, at := range splitAndTrim(envStr, parseOpts.separator) {
parsed, innerErr := strconv.ParseBool(strings.TrimSpace(at))
parsed, innerErr := strconv.ParseBool(at)
if innerErr != nil {
err = fmt.Errorf("item %s (pos: %d) failed to parse: %w", at, i, innerErr)
break
Expand Down Expand Up @@ -133,6 +141,39 @@ func FromEnvOrDefault[T Parseable](envVar string, defaultVal T, opts ...EnvParse
vs = append(vs, parsed)
}
v = vs
case []time.Duration:
vs := make([]time.Duration, 0)
for i, at := range splitAndTrim(envStr, parseOpts.separator) {
parsed, innerErr := time.ParseDuration(at)
if innerErr != nil {
err = fmt.Errorf("item %s (pos: %d) failed to parse: %w", at, i, innerErr)
break
}
vs = append(vs, parsed)
}
v = vs
case []time.Time:
vs := make([]time.Time, 0)
for i, at := range splitAndTrim(envStr, parseOpts.separator) {
parsed, innerErr := time.Parse(parseOpts.timeLayout, at)
if innerErr != nil {
err = fmt.Errorf("item %s (pos: %d) failed to parse: %w", at, i, innerErr)
break
}
vs = append(vs, parsed)
}
v = vs
case []url.URL:
vs := make([]url.URL, 0)
for i, at := range splitAndTrim(envStr, parseOpts.separator) {
parsed, innerErr := url.Parse(at)
if innerErr != nil {
err = fmt.Errorf("item %s (pos: %d) failed to parse: %w", at, i, innerErr)
break
}
vs = append(vs, *parsed)
}
v = vs
}
if err != nil {
if parseOpts.defaultOnError {
Expand All @@ -144,7 +185,7 @@ func FromEnvOrDefault[T Parseable](envVar string, defaultVal T, opts ...EnvParse

dest, ok := v.(T)
if !ok {
return dest, fmt.Errorf("failed to cast env %s (value: %v) to %T", envVar, v, dest)
return dest, fmt.Errorf("failed to cast env %s to %T", envVar, dest)
}
return dest, nil
}
Expand Down
Loading

0 comments on commit bc5c802

Please sign in to comment.