Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions config/loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package config

import (
"context"
"fmt"
"os"
"strconv"
"time"
)

// Loader reads configuration values from environment variables,
// optionally resolving GCP secrets. Errors are accumulated and
// surfaced via Err().
type Loader struct {
ctx context.Context
err error
}

// NewLoader returns a new Loader bound to the given context.
func NewLoader(ctx context.Context) *Loader {
return &Loader{ctx: ctx}
}

// Err returns the first error encountered during loading, if any.
func (l *Loader) Err() error {
return l.err
}

// MustErr panics if any error was encountered during loading.
// Useful for fail-fast initialisation in main().
func (l *Loader) MustErr() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pas fan du naming

Peut-être just Must ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pas fan du naming

Pour me justifier : must est normalement suivi d'un verbe

Du coup ma suggestion n'est pas terrible non plus, mais je ne trouve pas le bon verbe

Copy link
Copy Markdown
Contributor

@julakmane julakmane Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

j'aime bien juste Must, l'habitude avec les uuid google sûrement
https://github.com/google/uuid/blob/master/uuid.go#L210

// Must returns uuid if err is nil and panics otherwise.
func Must(uuid UUID, err error) UUID {
	if err != nil {
		panic(err)
	}
	return uuid
}
// New creates a new random UUID or panics.  New is equivalent to
// the expression
//
//    uuid.Must(uuid.NewRandom())
func New() UUID {
	return Must(NewRandom())
}

if l.err != nil {
panic(l.err)
}
}

// Str returns the string value of key, or fallback if unset.
func (l *Loader) Str(key, fallback string) string {
return load(l, key, fallback, castString)
}

// Int returns the int value of key, or fallback if unset.
func (l *Loader) Int(key string, fallback int) int {
return load(l, key, fallback, castInt)
}

// Int64 returns the int64 value of key, or fallback if unset.
func (l *Loader) Int64(key string, fallback int64) int64 {
return load(l, key, fallback, castInt64)
}

// Float64 returns the float64 value of key, or fallback if unset.
func (l *Loader) Float64(key string, fallback float64) float64 {
return load(l, key, fallback, castFloat64)
}

// Bool returns the boolean value of key, or fallback if unset.
// Accepted truthy values: "1", "t", "true" (case-insensitive).
func (l *Loader) Bool(key string, fallback bool) bool {
return load(l, key, fallback, castBool)
}

// Duration returns the time.Duration value of key, or fallback if unset.
// Values must be valid Go duration strings, e.g. "5s", "1m30s".
func (l *Loader) Duration(key string, fallback time.Duration) time.Duration {
return load(l, key, fallback, castDuration)
}

// Required returns the string value of key. If the key is unset, it
// records an error and returns an empty string. Use when there is no
// sensible fallback.
func (l *Loader) Required(key string) string {
Comment on lines +69 to +72
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pas de "Required" pour autre chose que les string ?

Ah, l'idée est peut-être de faire

_ = loader.Required("toto")
value = loader.Int64("toto", 42)

Je me demande si il n'y a pas une meilleure interface possible 🤔

if l.err != nil {
return ""
}
raw := os.Getenv(key)
if raw == "" {
l.err = fmt.Errorf("config: required key %q is not set", key)
return ""
}
resolved, err := ResolveSecretFromGCP(l.ctx, raw)
if err != nil {
l.err = fmt.Errorf("config: resolve %q: %w", key, err)
return ""
}
return resolved
}

// load is the generic backbone used by all typed accessors.
// It resolves the env var through GCP if needed, then casts it.
func load[T any](l *Loader, key string, fallback T, cast func(string) (T, error)) T {
if l.err != nil {
return fallback
}
Comment on lines +92 to +94
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on s'arrête à la première erreur ?

On pourrait stocker une slice puis les errors.Join pour voir tous les soucis d'un coup plutôt que 1 par 1

raw := os.Getenv(key)
if raw == "" {
return fallback
}
resolved, err := ResolveSecretFromGCP(l.ctx, raw)
if err != nil {
l.err = fmt.Errorf("config: resolve %q: %w", key, err)
return fallback
}
val, err := cast(resolved)
if err != nil {
l.err = fmt.Errorf("config: cast %q=%q: %w", key, resolved, err)
return fallback
}
return val
}

func castString(s string) (string, error) {
return s, nil
}

func castInt(s string) (int, error) {
v, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("expected int, got %q", s)
}
return v, nil
}

func castInt64(s string) (int64, error) {
v, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, fmt.Errorf("expected int64, got %q", s)
}
return v, nil
}

func castFloat64(s string) (float64, error) {
v, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, fmt.Errorf("expected float64, got %q", s)
}
return v, nil
}

func castBool(s string) (bool, error) {
v, err := strconv.ParseBool(s)
if err != nil {
return false, fmt.Errorf("expected bool, got %q", s)
}
return v, nil
}

func castDuration(s string) (time.Duration, error) {
v, err := time.ParseDuration(s)
if err != nil {
return 0, fmt.Errorf("expected duration (e.g. \"5s\"), got %q", s)
}
return v, nil
}
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ module github.com/thetreep/toolbox

go 1.23

toolchain go1.23.12

require (
braces.dev/errtrace v0.3.0
cloud.google.com/go/secretmanager v1.13.3
Expand All @@ -10,8 +12,10 @@ require (
github.com/cockroachdb/errors v1.11.1
github.com/jussi-kalliokoski/slogdriver v1.0.1
github.com/kr/pretty v0.3.1
github.com/lmittmann/tint v1.0.7
github.com/nicksnyder/go-i18n/v2 v2.4.1
github.com/nyaruka/phonenumbers v1.3.6
github.com/sqlc-dev/pqtype v0.3.0
github.com/stretchr/testify v1.9.0
github.com/urfave/cli v1.22.14
go.opencensus.io v0.24.0
Expand Down Expand Up @@ -44,12 +48,10 @@ require (
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
github.com/jussi-kalliokoski/goldjson v1.0.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lmittmann/tint v1.0.7 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sqlc-dev/pqtype v0.3.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
Expand Down
Loading