Skip to content
Merged
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## [v0.4.0] - 2025-12-24

This release feeds back some things I've found using the package in a larger project.
This is an interim release. I'll add some issues to GitHub for things I'd like to add in future releases.

### Added

Improvements to goconfig:
* Support for `*url.URL` type
- Support for pointer types in the read pipeline
- Separate the read pipeline code and the built-in types code for easier navigation

Improvements to samples:
* Example of using custom type handlers
* Example of using custom validation tags
- Improvements to the helper methods in goconfig's `custom_types.go` to make this easier.
* Example of combining keystores using CompositeStore.
- A simple store that reads values from a Properties file.

## [v0.3.0] - 2025-12-23

This is a return to 0.x versions due to the significance of the breaking changes. It's a real about turn in the
Expand Down
6 changes: 5 additions & 1 deletion example/custom_tags/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ type Config struct {
// WhatsAppPhoneId is the phone number ID for my phone number
WhatsAppPhoneId string `key:"WHATSAPP_PHONE_ID" required:"true" pattern:"^[0-9]+$"`
// WhatsAppServerUrl is the URL of the WhatsApp Business API server. This uses the custom "secure" tag defined by this example
// Note that the main goconfig package now provides its own URL support and uses a `scheme` tag to restrict accepted URL schemes.
// This example is for demonstration purposes only, to show how to use custom type validators.
WhatsAppServerUrl *url.URL `key:"WHATSAPP_SERVER_URL" required:"true" secure:"true" default:"https://api.whatsapp.com"`
// WhatsAppAuthToken is the authentication token for the WhatsApp Business API.
WhatsAppAuthToken string `key:"WHATSAPP_AUTH_TOKEN" required:"true"`
Expand All @@ -28,8 +30,10 @@ type Config struct {

func LoadConfig() (*Config, error) {
var config Config

// Load configuration from environment variables and a local file.
// Environment variables take precedence over values in the local file.
keystore := goconfig.CompositeStore(
fakeSecretsKeyStore,
goconfig.EnvironmentKeyStore,
goconfig.NewEnvFileKeyStore("env.example"))

Expand Down
15 changes: 0 additions & 15 deletions example/custom_tags/config/fake_secrets_keystore.go

This file was deleted.

7 changes: 6 additions & 1 deletion example/custom_types/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ type Config struct {
// WhatsAppPhoneId is the phone number ID for my phone number
WhatsAppPhoneId string `key:"WHATSAPP_PHONE_ID" required:"true" pattern:"^[0-9]+$"`
// WhatsAppServerUrl is the URL of the WhatsApp Business API server.
// Note that the main goconfig package now provides its own URL support and uses a `scheme` tag to restrict accepted URL schemes.
// This example is for demonstration purposes only, to show how to use custom type validators.
WhatsAppServerUrl *SecureURL `key:"WHATSAPP_SERVER_URL" required:"true" default:"https://api.whatsapp.com"`
// WhatsAppAuthToken is the authentication token for the WhatsApp Business API.
WhatsAppAuthToken string `key:"WHATSAPP_AUTH_TOKEN" required:"true"`
Expand All @@ -28,10 +30,13 @@ type Config struct {

func LoadConfig() (*Config, error) {
var config Config

// Load configuration from environment variables and a local file.
// Environment variables take precedence over values in the local file.
keystore := goconfig.CompositeStore(
fakeSecretsKeyStore,
goconfig.EnvironmentKeyStore,
goconfig.NewEnvFileKeyStore("env.example"))

if err := goconfig.Load(context.Background(), &config, goconfig.WithKeyStore(keystore)); err != nil {
return nil, err
}
Expand Down
15 changes: 0 additions & 15 deletions example/custom_types/config/fake_secrets_keystore.go

This file was deleted.

8 changes: 8 additions & 0 deletions example/structured_logging/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Structured Logging

This demonstration shows how to use structured logging.
It shows the provided `LogError` function as well as how to iterate over the error list
and log manually.

This code is expected to error, so for that reason will exit 0. This will allow the
GitHub pull request action to pass.
22 changes: 22 additions & 0 deletions example/structured_logging/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package config

import (
"context"

"github.com/m0rjc/goconfig"
)

type DatabaseConfig struct {
Host string `key:"DB_HOST" required:"true"`
Port int `key:"DB_PORT" required:"true" min:"1024" max:"65535"`
Username string `key:"DB_USER" required:"true"`
Password string `key:"DB_PASS" required:"true" pattern:"^.{8,}$"` // min 8 chars
Database string `key:"DB_NAME" required:"true"`
Timeout int `key:"DB_TIMEOUT" default:"30" min:"1" max:"300"`
}

func Load(ctx context.Context) (*DatabaseConfig, error) {
cfg := &DatabaseConfig{}
err := goconfig.Load(ctx, cfg)
return cfg, err
}
58 changes: 58 additions & 0 deletions example/structured_logging/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package main

import (
"context"
"errors"
"log/slog"
"os"

"github.com/m0rjc/goconfig"
"github.com/m0rjc/goconfig/example/structured_logging/config"
)

func main() {
// 1. Set up a structured logger
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)

// 2. Set up environment that will fail constraints
// DB_HOST is missing (mandatory)
os.Setenv("DB_PORT", "80") // fails min:1024
os.Setenv("DB_USER", "admin") // valid
os.Setenv("DB_PASS", "short") // fails pattern: ^.{8,}$
os.Setenv("DB_NAME", "production") // valid
os.Setenv("DB_TIMEOUT", "500") // fails max:300

ctx := context.Background()
cfg, err := config.Load(ctx)

if err != nil {
logger.Info("The following log is generated by the provided gocondfig.LogError fucntion")
goconfig.LogError(logger, err, goconfig.WithLogMessage("config_error"))

var configErrors *goconfig.ConfigErrors
if errors.As(err, &configErrors) {
logger.Info("The following log is generated by iterating over the errors manually")
// 3. Log errors to structured log
for _, e := range configErrors.Errors {
slog.Error("Configuration error",
"key", e.Key,
"error", e.Err.Error(),
)
}
// The purpose of this demo is to show logging. Exit(0) for an expected error.
logger.Info("This error was expected. The program will exit successfully to allow build pipelines to pass.")
os.Exit(0)
} else {
logger.Error("This error was unexpected. The program will exit with an error code to fail build pipelines.")
os.Exit(1)
}
}

// 4. Show success (won't reach here in this example)
slog.Info("Configuration loaded successfully",
"host", cfg.Host,
"port", cfg.Port,
"database", cfg.Database,
)
}
Loading