diff --git a/CHANGELOG.md b/CHANGELOG.md index c782529..fd9e2e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/example/custom_tags/config/config.go b/example/custom_tags/config/config.go index c63a964..4544266 100644 --- a/example/custom_tags/config/config.go +++ b/example/custom_tags/config/config.go @@ -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"` @@ -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")) diff --git a/example/custom_tags/config/fake_secrets_keystore.go b/example/custom_tags/config/fake_secrets_keystore.go deleted file mode 100644 index ea873ee..0000000 --- a/example/custom_tags/config/fake_secrets_keystore.go +++ /dev/null @@ -1,15 +0,0 @@ -package config - -import ( - "context" - "os" -) - -// fakeSecretsKeyStore is a fake keystore that reads secrets from environment variables. -// The environment variables must be prefixed with SECRET_. -// A production keystore could read secrets from a secure store, though I've just had Kubernetes inject them into -// the environment and used the normal keystore. -func fakeSecretsKeyStore(ctx context.Context, key string) (string, bool, error) { - value, ok := os.LookupEnv("SECRET_" + key) - return value, ok, nil -} diff --git a/example/custom_types/config/config.go b/example/custom_types/config/config.go index 2b2ec9b..691f502 100644 --- a/example/custom_types/config/config.go +++ b/example/custom_types/config/config.go @@ -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"` @@ -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 } diff --git a/example/custom_types/config/fake_secrets_keystore.go b/example/custom_types/config/fake_secrets_keystore.go deleted file mode 100644 index ea873ee..0000000 --- a/example/custom_types/config/fake_secrets_keystore.go +++ /dev/null @@ -1,15 +0,0 @@ -package config - -import ( - "context" - "os" -) - -// fakeSecretsKeyStore is a fake keystore that reads secrets from environment variables. -// The environment variables must be prefixed with SECRET_. -// A production keystore could read secrets from a secure store, though I've just had Kubernetes inject them into -// the environment and used the normal keystore. -func fakeSecretsKeyStore(ctx context.Context, key string) (string, bool, error) { - value, ok := os.LookupEnv("SECRET_" + key) - return value, ok, nil -} diff --git a/example/structured_logging/README.md b/example/structured_logging/README.md new file mode 100644 index 0000000..cceedf5 --- /dev/null +++ b/example/structured_logging/README.md @@ -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. \ No newline at end of file diff --git a/example/structured_logging/config/config.go b/example/structured_logging/config/config.go new file mode 100644 index 0000000..122e92c --- /dev/null +++ b/example/structured_logging/config/config.go @@ -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 +} diff --git a/example/structured_logging/main.go b/example/structured_logging/main.go new file mode 100644 index 0000000..78071f8 --- /dev/null +++ b/example/structured_logging/main.go @@ -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, + ) +}