Compare commits
8 Commits
hwsauth/v0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| bb6820f269 | |||
| 380e366891 | |||
| e8ffec6b7e | |||
| 1745458a95 | |||
| f3d6a01105 | |||
| 9179736c90 | |||
| 05be28d7f3 | |||
| 8f7c87cef2 |
@@ -3,7 +3,7 @@
|
|||||||
//
|
//
|
||||||
// ezconf allows you to:
|
// ezconf allows you to:
|
||||||
// - Load configurations from multiple packages using their ConfigFromEnv functions
|
// - Load configurations from multiple packages using their ConfigFromEnv functions
|
||||||
// - Parse package source code to extract environment variable documentation
|
// - Parse config struct tags to extract environment variable documentation
|
||||||
// - Generate and update .env files with all required environment variables
|
// - Generate and update .env files with all required environment variables
|
||||||
// - Print environment variable lists with descriptions and current values
|
// - Print environment variable lists with descriptions and current values
|
||||||
// - Track additional custom environment variables
|
// - Track additional custom environment variables
|
||||||
@@ -40,16 +40,16 @@
|
|||||||
// // Use configuration...
|
// // Use configuration...
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// Alternatively, you can manually register packages:
|
// Alternatively, you can manually register config structs:
|
||||||
//
|
//
|
||||||
// loader := ezconf.New()
|
// loader := ezconf.New()
|
||||||
//
|
//
|
||||||
// // Add package paths to parse for ENV comments
|
// // Add config struct for tag parsing
|
||||||
// loader.AddPackagePath("/path/to/golib/hlog")
|
// loader.AddConfigStruct(&mypackage.Config{}, "MyPackage")
|
||||||
//
|
//
|
||||||
// // Add configuration loaders
|
// // Add configuration loaders
|
||||||
// loader.AddConfigFunc("hlog", func() (interface{}, error) {
|
// loader.AddConfigFunc("mypackage", func() (interface{}, error) {
|
||||||
// return hlog.ConfigFromEnv()
|
// return mypackage.ConfigFromEnv()
|
||||||
// })
|
// })
|
||||||
//
|
//
|
||||||
// loader.Load()
|
// loader.Load()
|
||||||
@@ -94,27 +94,34 @@
|
|||||||
// Default: "postgres://localhost/mydb",
|
// Default: "postgres://localhost/mydb",
|
||||||
// })
|
// })
|
||||||
//
|
//
|
||||||
// # ENV Comment Format
|
// # Struct Tag Format
|
||||||
//
|
//
|
||||||
// ezconf parses struct field comments in the following format:
|
// ezconf uses struct tags to define environment variable metadata:
|
||||||
//
|
//
|
||||||
// type Config struct {
|
// type Config struct {
|
||||||
// // ENV LOG_LEVEL: Log level for the application (default: info)
|
// LogLevel string `ezconf:"LOG_LEVEL,description:Log level for the application,default:info"`
|
||||||
// LogLevel string
|
// DatabaseURL string `ezconf:"DATABASE_URL,description:Database connection string,required"`
|
||||||
//
|
// LogDir string `ezconf:"LOG_DIR,description:Directory for log files,required:when LOG_OUTPUT is file"`
|
||||||
// // ENV DATABASE_URL: Database connection string (required)
|
|
||||||
// DatabaseURL string
|
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// The format is:
|
// Tag components (comma-separated):
|
||||||
// - ENV ENV_VAR_NAME: Description (optional modifiers)
|
// - First value: environment variable name (required)
|
||||||
// - (required) or (required if condition) - marks variable as required
|
// - description:...: Description of the variable
|
||||||
// - (default: value) - specifies default value
|
// - default:...: Default value
|
||||||
|
// - required: Marks the variable as required
|
||||||
|
// - required:condition: Marks as required with a condition description
|
||||||
//
|
//
|
||||||
// # Integration
|
// # Integration
|
||||||
//
|
//
|
||||||
|
// Packages can implement the Integration interface to provide automatic
|
||||||
|
// registration with ezconf. The interface requires:
|
||||||
|
// - Name() string: Registration key for the config
|
||||||
|
// - ConfigPointer() any: Pointer to config struct for tag parsing
|
||||||
|
// - ConfigFunc() func() (any, error): Function to load config from env
|
||||||
|
// - GroupName() string: Display name for grouping env vars
|
||||||
|
//
|
||||||
// ezconf integrates with:
|
// ezconf integrates with:
|
||||||
// - All golib packages that follow the ConfigFromEnv pattern
|
// - All golib packages that follow the ConfigFromEnv pattern
|
||||||
// - Any custom configuration structs with ENV comments
|
// - Any custom configuration structs with ezconf struct tags
|
||||||
// - Standard .env file format
|
// - Standard .env file format
|
||||||
package ezconf
|
package ezconf
|
||||||
|
|||||||
@@ -16,11 +16,16 @@ type EnvVar struct {
|
|||||||
Group string // Group name for organizing variables (e.g., "Database", "Logging")
|
Group string // Group name for organizing variables (e.g., "Database", "Logging")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// configStruct holds a config struct pointer and its group name for parsing
|
||||||
|
type configStruct struct {
|
||||||
|
configPtr any
|
||||||
|
groupName string
|
||||||
|
}
|
||||||
|
|
||||||
// ConfigLoader manages configuration loading from multiple sources
|
// ConfigLoader manages configuration loading from multiple sources
|
||||||
type ConfigLoader struct {
|
type ConfigLoader struct {
|
||||||
configFuncs map[string]ConfigFunc // Map of config names to ConfigFromEnv functions
|
configFuncs map[string]ConfigFunc // Map of config names to ConfigFromEnv functions
|
||||||
packagePaths []string // Paths to packages to parse for ENV comments
|
configStructs []configStruct // Config struct pointers for tag parsing
|
||||||
groupNames map[string]string // Map of package paths to group names
|
|
||||||
extraEnvVars []EnvVar // Additional environment variables to track
|
extraEnvVars []EnvVar // Additional environment variables to track
|
||||||
envVars []EnvVar // All extracted environment variables
|
envVars []EnvVar // All extracted environment variables
|
||||||
configs map[string]any // Loaded configurations
|
configs map[string]any // Loaded configurations
|
||||||
@@ -33,8 +38,7 @@ type ConfigFunc func() (any, error)
|
|||||||
func New() *ConfigLoader {
|
func New() *ConfigLoader {
|
||||||
return &ConfigLoader{
|
return &ConfigLoader{
|
||||||
configFuncs: make(map[string]ConfigFunc),
|
configFuncs: make(map[string]ConfigFunc),
|
||||||
packagePaths: make([]string, 0),
|
configStructs: make([]configStruct, 0),
|
||||||
groupNames: make(map[string]string),
|
|
||||||
extraEnvVars: make([]EnvVar, 0),
|
extraEnvVars: make([]EnvVar, 0),
|
||||||
envVars: make([]EnvVar, 0),
|
envVars: make([]EnvVar, 0),
|
||||||
configs: make(map[string]any),
|
configs: make(map[string]any),
|
||||||
@@ -54,16 +58,20 @@ func (cl *ConfigLoader) AddConfigFunc(name string, fn ConfigFunc) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddPackagePath adds a package directory path to parse for ENV comments
|
// AddConfigStruct adds a config struct pointer for parsing ezconf tags.
|
||||||
func (cl *ConfigLoader) AddPackagePath(path string) error {
|
// The configPtr must be a pointer to a struct with ezconf struct tags.
|
||||||
if path == "" {
|
// The groupName is used for organizing environment variables in output.
|
||||||
return errors.New("package path cannot be empty")
|
func (cl *ConfigLoader) AddConfigStruct(configPtr any, groupName string) error {
|
||||||
|
if configPtr == nil {
|
||||||
|
return errors.New("config pointer cannot be nil")
|
||||||
}
|
}
|
||||||
// Check if path exists
|
if groupName == "" {
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
groupName = "Other"
|
||||||
return errors.Errorf("package path does not exist: %s", path)
|
|
||||||
}
|
}
|
||||||
cl.packagePaths = append(cl.packagePaths, path)
|
cl.configStructs = append(cl.configStructs, configStruct{
|
||||||
|
configPtr: configPtr,
|
||||||
|
groupName: groupName,
|
||||||
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,27 +80,22 @@ func (cl *ConfigLoader) AddEnvVar(envVar EnvVar) {
|
|||||||
cl.extraEnvVars = append(cl.extraEnvVars, envVar)
|
cl.extraEnvVars = append(cl.extraEnvVars, envVar)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseEnvVars extracts environment variables from packages and extra vars
|
// ParseEnvVars extracts environment variables from config struct tags and extra vars.
|
||||||
// This can be called without having actual environment variables set
|
// This can be called without having actual environment variables set.
|
||||||
func (cl *ConfigLoader) ParseEnvVars() error {
|
func (cl *ConfigLoader) ParseEnvVars() error {
|
||||||
// Clear existing env vars to prevent duplicates
|
// Clear existing env vars to prevent duplicates
|
||||||
cl.envVars = make([]EnvVar, 0)
|
cl.envVars = make([]EnvVar, 0)
|
||||||
|
|
||||||
// Parse packages for ENV comments
|
// Parse config structs for ezconf tags
|
||||||
for _, pkgPath := range cl.packagePaths {
|
for _, cs := range cl.configStructs {
|
||||||
envVars, err := ParseConfigPackage(pkgPath)
|
envVars, err := ParseConfigStruct(cs.configPtr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "failed to parse package: %s", pkgPath)
|
return errors.Wrap(err, "failed to parse config struct")
|
||||||
}
|
|
||||||
|
|
||||||
// Set group name for these variables from stored mapping
|
|
||||||
groupName := cl.groupNames[pkgPath]
|
|
||||||
if groupName == "" {
|
|
||||||
groupName = "Other"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set group name for these variables
|
||||||
for i := range envVars {
|
for i := range envVars {
|
||||||
envVars[i].Group = groupName
|
envVars[i].Group = cs.groupName
|
||||||
}
|
}
|
||||||
|
|
||||||
cl.envVars = append(cl.envVars, envVars...)
|
cl.envVars = append(cl.envVars, envVars...)
|
||||||
@@ -109,8 +112,8 @@ func (cl *ConfigLoader) ParseEnvVars() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfigs executes the config functions to load actual configurations
|
// LoadConfigs executes the config functions to load actual configurations.
|
||||||
// This should be called after environment variables are properly set
|
// This should be called after environment variables are properly set.
|
||||||
func (cl *ConfigLoader) LoadConfigs() error {
|
func (cl *ConfigLoader) LoadConfigs() error {
|
||||||
// Load configurations
|
// Load configurations
|
||||||
for name, fn := range cl.configFuncs {
|
for name, fn := range cl.configFuncs {
|
||||||
|
|||||||
@@ -2,11 +2,17 @@ package ezconf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// testConfig is a Config struct used by multiple tests
|
||||||
|
type testConfig struct {
|
||||||
|
LogLevel string `ezconf:"LOG_LEVEL,description:Log level for the application,default:info"`
|
||||||
|
LogOutput string `ezconf:"LOG_OUTPUT,description:Output destination,default:console"`
|
||||||
|
DatabaseURL string `ezconf:"DATABASE_URL,description:Database connection string,required"`
|
||||||
|
}
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
loader := New()
|
loader := New()
|
||||||
if loader == nil {
|
if loader == nil {
|
||||||
@@ -16,8 +22,8 @@ func TestNew(t *testing.T) {
|
|||||||
if loader.configFuncs == nil {
|
if loader.configFuncs == nil {
|
||||||
t.Error("configFuncs map is nil")
|
t.Error("configFuncs map is nil")
|
||||||
}
|
}
|
||||||
if loader.packagePaths == nil {
|
if loader.configStructs == nil {
|
||||||
t.Error("packagePaths slice is nil")
|
t.Error("configStructs slice is nil")
|
||||||
}
|
}
|
||||||
if loader.extraEnvVars == nil {
|
if loader.extraEnvVars == nil {
|
||||||
t.Error("extraEnvVars slice is nil")
|
t.Error("extraEnvVars slice is nil")
|
||||||
@@ -66,35 +72,39 @@ func TestAddConfigFunc_EmptyName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddPackagePath(t *testing.T) {
|
func TestAddConfigStruct(t *testing.T) {
|
||||||
loader := New()
|
loader := New()
|
||||||
|
|
||||||
// Use current directory as test path
|
err := loader.AddConfigStruct(&testConfig{}, "Test")
|
||||||
err := loader.AddPackagePath(".")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("AddPackagePath failed: %v", err)
|
t.Errorf("AddConfigStruct failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(loader.packagePaths) != 1 {
|
if len(loader.configStructs) != 1 {
|
||||||
t.Errorf("expected 1 package path, got %d", len(loader.packagePaths))
|
t.Errorf("expected 1 config struct, got %d", len(loader.configStructs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddPackagePath_InvalidPath(t *testing.T) {
|
func TestAddConfigStruct_NilPointer(t *testing.T) {
|
||||||
loader := New()
|
loader := New()
|
||||||
|
|
||||||
err := loader.AddPackagePath("/nonexistent/path")
|
err := loader.AddConfigStruct(nil, "Test")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("expected error for nonexistent path")
|
t.Error("expected error for nil pointer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddPackagePath_EmptyPath(t *testing.T) {
|
func TestAddConfigStruct_EmptyGroupName(t *testing.T) {
|
||||||
loader := New()
|
loader := New()
|
||||||
|
|
||||||
err := loader.AddPackagePath("")
|
err := loader.AddConfigStruct(&testConfig{}, "")
|
||||||
if err == nil {
|
if err != nil {
|
||||||
t.Error("expected error for empty path")
|
t.Errorf("AddConfigStruct failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should default to "Other"
|
||||||
|
if loader.configStructs[0].groupName != "Other" {
|
||||||
|
t.Errorf("expected group name 'Other', got %s", loader.configStructs[0].groupName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,8 +141,8 @@ func TestLoad(t *testing.T) {
|
|||||||
return testCfg, nil
|
return testCfg, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add current package path
|
// Add config struct for tag parsing
|
||||||
loader.AddPackagePath(".")
|
loader.AddConfigStruct(&testConfig{}, "Test")
|
||||||
|
|
||||||
// Add an extra env var
|
// Add an extra env var
|
||||||
loader.AddEnvVar(EnvVar{
|
loader.AddEnvVar(EnvVar{
|
||||||
@@ -249,8 +259,8 @@ func TestParseEnvVars(t *testing.T) {
|
|||||||
return "test config", nil
|
return "test config", nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add current package path
|
// Add config struct for tag parsing
|
||||||
loader.AddPackagePath(".")
|
loader.AddConfigStruct(&testConfig{}, "Test")
|
||||||
|
|
||||||
// Add an extra env var
|
// Add an extra env var
|
||||||
loader.AddEnvVar(EnvVar{
|
loader.AddEnvVar(EnvVar{
|
||||||
@@ -353,8 +363,8 @@ func TestParseEnvVars_Then_LoadConfigs(t *testing.T) {
|
|||||||
return testCfg, nil
|
return testCfg, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add current package path
|
// Add config struct for tag parsing
|
||||||
loader.AddPackagePath(".")
|
loader.AddConfigStruct(&testConfig{}, "Test")
|
||||||
|
|
||||||
// Add an extra env var
|
// Add an extra env var
|
||||||
loader.AddEnvVar(EnvVar{
|
loader.AddEnvVar(EnvVar{
|
||||||
@@ -398,63 +408,68 @@ func TestParseEnvVars_Then_LoadConfigs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoad_Integration(t *testing.T) {
|
func TestParseEnvVars_GroupName(t *testing.T) {
|
||||||
// Integration test with real hlog package
|
|
||||||
hlogPath := filepath.Join("..", "hlog")
|
|
||||||
if _, err := os.Stat(hlogPath); os.IsNotExist(err) {
|
|
||||||
t.Skip("hlog package not found, skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
loader := New()
|
loader := New()
|
||||||
|
|
||||||
// Add hlog package
|
loader.AddConfigStruct(&testConfig{}, "MyGroup")
|
||||||
if err := loader.AddPackagePath(hlogPath); err != nil {
|
|
||||||
t.Fatalf("failed to add hlog package: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load without config function (just parse)
|
err := loader.ParseEnvVars()
|
||||||
if err := loader.Load(); err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Load failed: %v", err)
|
t.Fatalf("ParseEnvVars failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
envVars := loader.GetEnvVars()
|
envVars := loader.GetEnvVars()
|
||||||
if len(envVars) == 0 {
|
|
||||||
t.Error("expected env vars from hlog package")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Found %d environment variables from hlog", len(envVars))
|
|
||||||
for _, ev := range envVars {
|
for _, ev := range envVars {
|
||||||
t.Logf(" %s: %s (default: %s, required: %t)", ev.Name, ev.Description, ev.Default, ev.Required)
|
if ev.Group != "MyGroup" {
|
||||||
|
t.Errorf("expected group 'MyGroup', got '%s' for var %s", ev.Group, ev.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseEnvVars_GenerateEnvFile_Integration(t *testing.T) {
|
func TestParseEnvVars_CurrentValues(t *testing.T) {
|
||||||
// Test the new separated ParseEnvVars functionality
|
|
||||||
hlogPath := filepath.Join("..", "hlog")
|
|
||||||
if _, err := os.Stat(hlogPath); os.IsNotExist(err) {
|
|
||||||
t.Skip("hlog package not found, skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
loader := New()
|
loader := New()
|
||||||
|
|
||||||
// Add hlog package
|
loader.AddConfigStruct(&testConfig{}, "Test")
|
||||||
if err := loader.AddPackagePath(hlogPath); err != nil {
|
|
||||||
t.Fatalf("failed to add hlog package: %v", err)
|
// Set an env var
|
||||||
|
t.Setenv("LOG_LEVEL", "debug")
|
||||||
|
|
||||||
|
err := loader.ParseEnvVars()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseEnvVars failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse env vars without loading configs (this should work even if required env vars are missing)
|
envVars := loader.GetEnvVars()
|
||||||
|
for _, ev := range envVars {
|
||||||
|
if ev.Name == "LOG_LEVEL" {
|
||||||
|
if ev.CurrentValue != "debug" {
|
||||||
|
t.Errorf("expected CurrentValue 'debug', got '%s'", ev.CurrentValue)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Error("LOG_LEVEL not found in env vars")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseEnvVars_GenerateEnvFile_Integration(t *testing.T) {
|
||||||
|
loader := New()
|
||||||
|
|
||||||
|
// Add config struct for tag parsing
|
||||||
|
loader.AddConfigStruct(&testConfig{}, "Test")
|
||||||
|
|
||||||
|
// Parse env vars
|
||||||
if err := loader.ParseEnvVars(); err != nil {
|
if err := loader.ParseEnvVars(); err != nil {
|
||||||
t.Fatalf("ParseEnvVars failed: %v", err)
|
t.Fatalf("ParseEnvVars failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
envVars := loader.GetEnvVars()
|
envVars := loader.GetEnvVars()
|
||||||
if len(envVars) == 0 {
|
if len(envVars) == 0 {
|
||||||
t.Error("expected env vars from hlog package")
|
t.Error("expected env vars from config struct")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now test that we can generate an env file without calling Load()
|
// Now test that we can generate an env file without calling Load()
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
envFile := filepath.Join(tempDir, "test-generated.env")
|
envFile := tempDir + "/test-generated.env"
|
||||||
|
|
||||||
err := loader.GenerateEnvFile(envFile, false)
|
err := loader.GenerateEnvFile(envFile, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -472,16 +487,16 @@ func TestParseEnvVars_GenerateEnvFile_Integration(t *testing.T) {
|
|||||||
t.Error("expected header in generated file")
|
t.Error("expected header in generated file")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should contain environment variables from hlog
|
// Should contain environment variables from config struct
|
||||||
foundHlogVar := false
|
foundVar := false
|
||||||
for _, ev := range envVars {
|
for _, ev := range envVars {
|
||||||
if strings.Contains(output, ev.Name) {
|
if strings.Contains(output, ev.Name) {
|
||||||
foundHlogVar = true
|
foundVar = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !foundHlogVar {
|
if !foundVar {
|
||||||
t.Error("expected to find at least one hlog environment variable in generated file")
|
t.Error("expected to find at least one environment variable in generated file")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("Successfully generated env file with %d variables", len(envVars))
|
t.Logf("Successfully generated env file with %d variables", len(envVars))
|
||||||
|
|||||||
@@ -1,31 +1,70 @@
|
|||||||
package ezconf
|
package ezconf
|
||||||
|
|
||||||
// Integration is an interface that packages can implement to provide
|
type Integration struct {
|
||||||
|
Name string
|
||||||
|
ConfigPointer any
|
||||||
|
ConfigFunc func() (any, error)
|
||||||
|
GroupName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIntegration(name, groupname string, cfgptr any, cfgfunc func() (any, error)) *Integration {
|
||||||
|
return &Integration{
|
||||||
|
name,
|
||||||
|
cfgptr,
|
||||||
|
cfgfunc,
|
||||||
|
groupname,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntegrationDepr is an interface that packages can implement to provide
|
||||||
// easy integration with ezconf
|
// easy integration with ezconf
|
||||||
type Integration interface {
|
type IntegrationDepr interface {
|
||||||
// Name returns the name to use when registering the config
|
// Name returns the name to use when registering the config
|
||||||
Name() string
|
Name() string
|
||||||
|
|
||||||
// PackagePath returns the path to the package for source parsing
|
// ConfigPointer returns a pointer to the config struct for tag parsing
|
||||||
PackagePath() string
|
ConfigPointer() any
|
||||||
|
|
||||||
// ConfigFunc returns the ConfigFromEnv function
|
// ConfigFunc returns the ConfigFromEnv function
|
||||||
ConfigFunc() func() (interface{}, error)
|
ConfigFunc() func() (any, error)
|
||||||
|
|
||||||
// GroupName returns the display name for grouping environment variables
|
// GroupName returns the display name for grouping environment variables
|
||||||
GroupName() string
|
GroupName() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterIntegration registers a package that implements the Integration interface
|
// AddIntegration registers a package using an Integration object returned by another package
|
||||||
func (cl *ConfigLoader) RegisterIntegration(integration Integration) error {
|
func (cl *ConfigLoader) AddIntegration(integration *Integration) error {
|
||||||
// Add package path
|
// Add config struct for tag parsing
|
||||||
pkgPath := integration.PackagePath()
|
configPtr := integration.ConfigPointer
|
||||||
if err := cl.AddPackagePath(pkgPath); err != nil {
|
if err := cl.AddConfigStruct(configPtr, integration.GroupName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store group name for this package
|
// Add config function
|
||||||
cl.groupNames[pkgPath] = integration.GroupName()
|
if err := cl.AddConfigFunc(integration.Name, integration.ConfigFunc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddIntegrations registers multiple integrations at once
|
||||||
|
func (cl *ConfigLoader) AddIntegrations(integrations ...*Integration) error {
|
||||||
|
for _, integration := range integrations {
|
||||||
|
if err := cl.AddIntegration(integration); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterIntegration registers a package that implements the Integration interface
|
||||||
|
func (cl *ConfigLoader) RegisterIntegration(integration IntegrationDepr) error {
|
||||||
|
// Add config struct for tag parsing
|
||||||
|
configPtr := integration.ConfigPointer()
|
||||||
|
if err := cl.AddConfigStruct(configPtr, integration.GroupName()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Add config function
|
// Add config function
|
||||||
if err := cl.AddConfigFunc(integration.Name(), integration.ConfigFunc()); err != nil {
|
if err := cl.AddConfigFunc(integration.Name(), integration.ConfigFunc()); err != nil {
|
||||||
@@ -36,7 +75,7 @@ func (cl *ConfigLoader) RegisterIntegration(integration Integration) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RegisterIntegrations registers multiple integrations at once
|
// RegisterIntegrations registers multiple integrations at once
|
||||||
func (cl *ConfigLoader) RegisterIntegrations(integrations ...Integration) error {
|
func (cl *ConfigLoader) RegisterIntegrations(integrations ...IntegrationDepr) error {
|
||||||
for _, integration := range integrations {
|
for _, integration := range integrations {
|
||||||
if err := cl.RegisterIntegration(integration); err != nil {
|
if err := cl.RegisterIntegration(integration); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,24 +1,34 @@
|
|||||||
package ezconf
|
package ezconf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Mock integration for testing
|
// mockConfig is a test config struct with ezconf tags
|
||||||
|
type mockConfig struct {
|
||||||
|
Host string `ezconf:"MOCK_HOST,description:Host to connect to,default:localhost"`
|
||||||
|
Port int `ezconf:"MOCK_PORT,description:Port to connect to,default:8080"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockConfig2 is a second test config struct
|
||||||
|
type mockConfig2 struct {
|
||||||
|
Token string `ezconf:"MOCK_TOKEN,description:API token,required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockIntegration implements the Integration interface for testing
|
||||||
type mockIntegration struct {
|
type mockIntegration struct {
|
||||||
name string
|
name string
|
||||||
packagePath string
|
configPtr any
|
||||||
configFunc func() (interface{}, error)
|
configFunc func() (interface{}, error)
|
||||||
|
groupName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m mockIntegration) Name() string {
|
func (m mockIntegration) Name() string {
|
||||||
return m.name
|
return m.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m mockIntegration) PackagePath() string {
|
func (m mockIntegration) ConfigPointer() any {
|
||||||
return m.packagePath
|
return m.configPtr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m mockIntegration) ConfigFunc() func() (interface{}, error) {
|
func (m mockIntegration) ConfigFunc() func() (interface{}, error) {
|
||||||
@@ -26,7 +36,10 @@ func (m mockIntegration) ConfigFunc() func() (interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m mockIntegration) GroupName() string {
|
func (m mockIntegration) GroupName() string {
|
||||||
|
if m.groupName == "" {
|
||||||
return "Test Group"
|
return "Test Group"
|
||||||
|
}
|
||||||
|
return m.groupName
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisterIntegration(t *testing.T) {
|
func TestRegisterIntegration(t *testing.T) {
|
||||||
@@ -34,7 +47,7 @@ func TestRegisterIntegration(t *testing.T) {
|
|||||||
|
|
||||||
integration := mockIntegration{
|
integration := mockIntegration{
|
||||||
name: "test",
|
name: "test",
|
||||||
packagePath: ".",
|
configPtr: &mockConfig{},
|
||||||
configFunc: func() (interface{}, error) {
|
configFunc: func() (interface{}, error) {
|
||||||
return "test config", nil
|
return "test config", nil
|
||||||
},
|
},
|
||||||
@@ -45,9 +58,9 @@ func TestRegisterIntegration(t *testing.T) {
|
|||||||
t.Fatalf("RegisterIntegration failed: %v", err)
|
t.Fatalf("RegisterIntegration failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify package path was added
|
// Verify config struct was added
|
||||||
if len(loader.packagePaths) != 1 {
|
if len(loader.configStructs) != 1 {
|
||||||
t.Errorf("expected 1 package path, got %d", len(loader.packagePaths))
|
t.Errorf("expected 1 config struct, got %d", len(loader.configStructs))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify config func was added
|
// Verify config func was added
|
||||||
@@ -68,14 +81,46 @@ func TestRegisterIntegration(t *testing.T) {
|
|||||||
if cfg != "test config" {
|
if cfg != "test config" {
|
||||||
t.Errorf("expected 'test config', got %v", cfg)
|
t.Errorf("expected 'test config', got %v", cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify env vars were parsed from struct tags
|
||||||
|
envVars := loader.GetEnvVars()
|
||||||
|
if len(envVars) != 2 {
|
||||||
|
t.Errorf("expected 2 env vars, got %d", len(envVars))
|
||||||
|
}
|
||||||
|
|
||||||
|
foundHost := false
|
||||||
|
foundPort := false
|
||||||
|
for _, ev := range envVars {
|
||||||
|
if ev.Name == "MOCK_HOST" {
|
||||||
|
foundHost = true
|
||||||
|
if ev.Default != "localhost" {
|
||||||
|
t.Errorf("expected default 'localhost', got '%s'", ev.Default)
|
||||||
|
}
|
||||||
|
if ev.Group != "Test Group" {
|
||||||
|
t.Errorf("expected group 'Test Group', got '%s'", ev.Group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ev.Name == "MOCK_PORT" {
|
||||||
|
foundPort = true
|
||||||
|
if ev.Default != "8080" {
|
||||||
|
t.Errorf("expected default '8080', got '%s'", ev.Default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundHost {
|
||||||
|
t.Error("MOCK_HOST not found in env vars")
|
||||||
|
}
|
||||||
|
if !foundPort {
|
||||||
|
t.Error("MOCK_PORT not found in env vars")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisterIntegration_InvalidPath(t *testing.T) {
|
func TestRegisterIntegration_NilConfigPointer(t *testing.T) {
|
||||||
loader := New()
|
loader := New()
|
||||||
|
|
||||||
integration := mockIntegration{
|
integration := mockIntegration{
|
||||||
name: "test",
|
name: "test",
|
||||||
packagePath: "/nonexistent/path",
|
configPtr: nil,
|
||||||
configFunc: func() (interface{}, error) {
|
configFunc: func() (interface{}, error) {
|
||||||
return "test config", nil
|
return "test config", nil
|
||||||
},
|
},
|
||||||
@@ -83,7 +128,7 @@ func TestRegisterIntegration_InvalidPath(t *testing.T) {
|
|||||||
|
|
||||||
err := loader.RegisterIntegration(integration)
|
err := loader.RegisterIntegration(integration)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("expected error for invalid package path")
|
t.Error("expected error for nil config pointer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +137,7 @@ func TestRegisterIntegrations(t *testing.T) {
|
|||||||
|
|
||||||
integration1 := mockIntegration{
|
integration1 := mockIntegration{
|
||||||
name: "test1",
|
name: "test1",
|
||||||
packagePath: ".",
|
configPtr: &mockConfig{},
|
||||||
configFunc: func() (interface{}, error) {
|
configFunc: func() (interface{}, error) {
|
||||||
return "config1", nil
|
return "config1", nil
|
||||||
},
|
},
|
||||||
@@ -100,7 +145,7 @@ func TestRegisterIntegrations(t *testing.T) {
|
|||||||
|
|
||||||
integration2 := mockIntegration{
|
integration2 := mockIntegration{
|
||||||
name: "test2",
|
name: "test2",
|
||||||
packagePath: ".",
|
configPtr: &mockConfig2{},
|
||||||
configFunc: func() (interface{}, error) {
|
configFunc: func() (interface{}, error) {
|
||||||
return "config2", nil
|
return "config2", nil
|
||||||
},
|
},
|
||||||
@@ -130,6 +175,12 @@ func TestRegisterIntegrations(t *testing.T) {
|
|||||||
if cfg1 != "config1" || cfg2 != "config2" {
|
if cfg1 != "config1" || cfg2 != "config2" {
|
||||||
t.Error("config values mismatch")
|
t.Error("config values mismatch")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should have env vars from both structs
|
||||||
|
envVars := loader.GetEnvVars()
|
||||||
|
if len(envVars) != 3 {
|
||||||
|
t.Errorf("expected 3 env vars (2 from mockConfig + 1 from mockConfig2), got %d", len(envVars))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisterIntegrations_PartialFailure(t *testing.T) {
|
func TestRegisterIntegrations_PartialFailure(t *testing.T) {
|
||||||
@@ -137,7 +188,7 @@ func TestRegisterIntegrations_PartialFailure(t *testing.T) {
|
|||||||
|
|
||||||
integration1 := mockIntegration{
|
integration1 := mockIntegration{
|
||||||
name: "test1",
|
name: "test1",
|
||||||
packagePath: ".",
|
configPtr: &mockConfig{},
|
||||||
configFunc: func() (interface{}, error) {
|
configFunc: func() (interface{}, error) {
|
||||||
return "config1", nil
|
return "config1", nil
|
||||||
},
|
},
|
||||||
@@ -145,7 +196,7 @@ func TestRegisterIntegrations_PartialFailure(t *testing.T) {
|
|||||||
|
|
||||||
integration2 := mockIntegration{
|
integration2 := mockIntegration{
|
||||||
name: "test2",
|
name: "test2",
|
||||||
packagePath: "/nonexistent",
|
configPtr: nil, // This should cause failure
|
||||||
configFunc: func() (interface{}, error) {
|
configFunc: func() (interface{}, error) {
|
||||||
return "config2", nil
|
return "config2", nil
|
||||||
},
|
},
|
||||||
@@ -159,54 +210,5 @@ func TestRegisterIntegrations_PartialFailure(t *testing.T) {
|
|||||||
|
|
||||||
func TestIntegration_Interface(t *testing.T) {
|
func TestIntegration_Interface(t *testing.T) {
|
||||||
// Verify that mockIntegration implements Integration interface
|
// Verify that mockIntegration implements Integration interface
|
||||||
var _ Integration = (*mockIntegration)(nil)
|
var _ IntegrationDepr = (*mockIntegration)(nil)
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegisterIntegration_RealPackage(t *testing.T) {
|
|
||||||
// Integration test with real hlog package if available
|
|
||||||
hlogPath := filepath.Join("..", "hlog")
|
|
||||||
if _, err := os.Stat(hlogPath); os.IsNotExist(err) {
|
|
||||||
t.Skip("hlog package not found, skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
loader := New()
|
|
||||||
|
|
||||||
// Create a simple integration for testing
|
|
||||||
integration := mockIntegration{
|
|
||||||
name: "hlog",
|
|
||||||
packagePath: hlogPath,
|
|
||||||
configFunc: func() (interface{}, error) {
|
|
||||||
// Return a mock config instead of calling real ConfigFromEnv
|
|
||||||
return struct{ LogLevel string }{LogLevel: "info"}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := loader.RegisterIntegration(integration)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("RegisterIntegration with real package failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := loader.Load(); err != nil {
|
|
||||||
t.Fatalf("Load failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should have parsed env vars from hlog
|
|
||||||
envVars := loader.GetEnvVars()
|
|
||||||
if len(envVars) == 0 {
|
|
||||||
t.Error("expected env vars from hlog package")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for known hlog variables
|
|
||||||
foundLogLevel := false
|
|
||||||
for _, ev := range envVars {
|
|
||||||
if ev.Name == "LOG_LEVEL" {
|
|
||||||
foundLogLevel = true
|
|
||||||
t.Logf("Found LOG_LEVEL: %s", ev.Description)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !foundLogLevel {
|
|
||||||
t.Error("expected to find LOG_LEVEL from hlog")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
172
ezconf/parser.go
172
ezconf/parser.go
@@ -1,146 +1,102 @@
|
|||||||
package ezconf
|
package ezconf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"go/ast"
|
"reflect"
|
||||||
"go/parser"
|
|
||||||
"go/token"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseConfigFile parses a Go source file and extracts ENV comments from struct fields
|
// ParseConfigStruct extracts environment variable metadata from a config
|
||||||
func ParseConfigFile(filename string) ([]EnvVar, error) {
|
// struct's ezconf struct tags using reflection.
|
||||||
content, err := os.ReadFile(filename)
|
//
|
||||||
if err != nil {
|
// The configPtr parameter must be a pointer to a struct. Each field with an
|
||||||
return nil, errors.Wrap(err, "failed to read file")
|
// ezconf tag will be parsed to extract environment variable information.
|
||||||
|
//
|
||||||
|
// Tag format: `ezconf:"VAR_NAME,description:Description text,default:value,required"`
|
||||||
|
//
|
||||||
|
// Components:
|
||||||
|
// - First value: environment variable name (required)
|
||||||
|
// - description:...: Description of the variable
|
||||||
|
// - default:...: Default value
|
||||||
|
// - required: Marks the variable as required (optionally required:condition)
|
||||||
|
func ParseConfigStruct(configPtr any) ([]EnvVar, error) {
|
||||||
|
if configPtr == nil {
|
||||||
|
return nil, errors.New("config pointer cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
fset := token.NewFileSet()
|
v := reflect.ValueOf(configPtr)
|
||||||
file, err := parser.ParseFile(fset, filename, content, parser.ParseComments)
|
if v.Kind() != reflect.Ptr {
|
||||||
if err != nil {
|
return nil, errors.New("config must be a pointer to a struct")
|
||||||
return nil, errors.Wrap(err, "failed to parse file")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v = v.Elem()
|
||||||
|
if v.Kind() != reflect.Struct {
|
||||||
|
return nil, errors.New("config must be a pointer to a struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
t := v.Type()
|
||||||
envVars := make([]EnvVar, 0)
|
envVars := make([]EnvVar, 0)
|
||||||
|
|
||||||
// Walk through the AST
|
for i := 0; i < t.NumField(); i++ {
|
||||||
ast.Inspect(file, func(n ast.Node) bool {
|
field := t.Field(i)
|
||||||
// Look for struct type declarations
|
tag := field.Tag.Get("ezconf")
|
||||||
typeSpec, ok := n.(*ast.TypeSpec)
|
if tag == "" {
|
||||||
if !ok {
|
continue
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
structType, ok := typeSpec.Type.(*ast.StructType)
|
envVar, err := parseEzconfTag(tag)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return true
|
return nil, errors.Wrapf(err, "failed to parse ezconf tag on field %s", field.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate through struct fields
|
|
||||||
for _, field := range structType.Fields.List {
|
|
||||||
var comment string
|
|
||||||
|
|
||||||
// Try to get from doc comment (comment before field)
|
|
||||||
if field.Doc != nil && len(field.Doc.List) > 0 {
|
|
||||||
comment = field.Doc.List[0].Text
|
|
||||||
comment = strings.TrimPrefix(comment, "//")
|
|
||||||
comment = strings.TrimSpace(comment)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to get from inline comment (comment after field)
|
|
||||||
if comment == "" && field.Comment != nil && len(field.Comment.List) > 0 {
|
|
||||||
comment = field.Comment.List[0].Text
|
|
||||||
comment = strings.TrimPrefix(comment, "//")
|
|
||||||
comment = strings.TrimSpace(comment)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse ENV comment
|
|
||||||
if strings.HasPrefix(comment, "ENV ") {
|
|
||||||
envVar, err := parseEnvComment(comment)
|
|
||||||
if err == nil {
|
|
||||||
envVars = append(envVars, *envVar)
|
envVars = append(envVars, *envVar)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
return envVars, nil
|
return envVars, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseConfigPackage parses all Go files in a package directory and extracts ENV comments
|
// parseEzconfTag parses an ezconf struct tag value to extract environment
|
||||||
func ParseConfigPackage(packagePath string) ([]EnvVar, error) {
|
// variable information.
|
||||||
// Find all .go files in the package
|
//
|
||||||
files, err := filepath.Glob(filepath.Join(packagePath, "*.go"))
|
// Expected format: "VAR_NAME,description:Description text,default:value,required"
|
||||||
if err != nil {
|
func parseEzconfTag(tag string) (*EnvVar, error) {
|
||||||
return nil, errors.Wrap(err, "failed to glob package files")
|
if tag == "" {
|
||||||
|
return nil, errors.New("tag cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
allEnvVars := make([]EnvVar, 0)
|
parts := strings.Split(tag, ",")
|
||||||
|
if len(parts) == 0 {
|
||||||
for _, file := range files {
|
return nil, errors.New("tag cannot be empty")
|
||||||
// Skip test files
|
|
||||||
if strings.HasSuffix(file, "_test.go") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
envVars, err := ParseConfigFile(file)
|
|
||||||
if err != nil {
|
|
||||||
// Log error but continue with other files
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
allEnvVars = append(allEnvVars, envVars...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return allEnvVars, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseEnvComment parses a field comment to extract environment variable information.
|
|
||||||
// Expected format: ENV ENV_NAME: Description (required <condition>) (default: <value>)
|
|
||||||
func parseEnvComment(comment string) (*EnvVar, error) {
|
|
||||||
// Check if comment starts with ENV
|
|
||||||
if !strings.HasPrefix(comment, "ENV ") {
|
|
||||||
return nil, errors.New("comment does not start with 'ENV '")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove "ENV " prefix
|
|
||||||
comment = strings.TrimPrefix(comment, "ENV ")
|
|
||||||
|
|
||||||
// Extract env var name (everything before the first colon)
|
|
||||||
colonIdx := strings.Index(comment, ":")
|
|
||||||
if colonIdx == -1 {
|
|
||||||
return nil, errors.New("missing colon separator")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
envVar := &EnvVar{
|
envVar := &EnvVar{
|
||||||
Name: strings.TrimSpace(comment[:colonIdx]),
|
Name: strings.TrimSpace(parts[0]),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract description and optional parts
|
if envVar.Name == "" {
|
||||||
remainder := strings.TrimSpace(comment[colonIdx+1:])
|
return nil, errors.New("environment variable name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
// Check for (required ...) pattern
|
for _, part := range parts[1:] {
|
||||||
requiredPattern := regexp.MustCompile(`\(required[^)]*\)`)
|
part = strings.TrimSpace(part)
|
||||||
if requiredPattern.MatchString(remainder) {
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(part, "description:"):
|
||||||
|
envVar.Description = strings.TrimSpace(strings.TrimPrefix(part, "description:"))
|
||||||
|
case strings.HasPrefix(part, "default:"):
|
||||||
|
envVar.Default = strings.TrimSpace(strings.TrimPrefix(part, "default:"))
|
||||||
|
case part == "required":
|
||||||
envVar.Required = true
|
envVar.Required = true
|
||||||
remainder = requiredPattern.ReplaceAllString(remainder, "")
|
case strings.HasPrefix(part, "required:"):
|
||||||
|
envVar.Required = true
|
||||||
|
// Store the condition in the description if it adds context
|
||||||
|
condition := strings.TrimSpace(strings.TrimPrefix(part, "required:"))
|
||||||
|
if condition != "" && envVar.Description != "" {
|
||||||
|
envVar.Description = envVar.Description + " (required " + condition + ")"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for (default: ...) pattern
|
|
||||||
defaultPattern := regexp.MustCompile(`\(default:\s*([^)]*)\)`)
|
|
||||||
if matches := defaultPattern.FindStringSubmatch(remainder); len(matches) > 1 {
|
|
||||||
envVar.Default = strings.TrimSpace(matches[1])
|
|
||||||
remainder = defaultPattern.ReplaceAllString(remainder, "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// What remains is the description
|
|
||||||
envVar.Description = strings.TrimSpace(remainder)
|
|
||||||
|
|
||||||
return envVar, nil
|
return envVar, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
package ezconf
|
package ezconf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseEnvComment(t *testing.T) {
|
func TestParseEzconfTag(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
comment string
|
tag string
|
||||||
wantEnvVar *EnvVar
|
wantEnvVar *EnvVar
|
||||||
expectError bool
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "simple env variable",
|
name: "simple env variable",
|
||||||
comment: "ENV LOG_LEVEL: Log level for the application",
|
tag: "LOG_LEVEL,description:Log level for the application",
|
||||||
wantEnvVar: &EnvVar{
|
wantEnvVar: &EnvVar{
|
||||||
Name: "LOG_LEVEL",
|
Name: "LOG_LEVEL",
|
||||||
Description: "Log level for the application",
|
Description: "Log level for the application",
|
||||||
@@ -26,7 +24,7 @@ func TestParseEnvComment(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "env variable with default",
|
name: "env variable with default",
|
||||||
comment: "ENV LOG_LEVEL: Log level for the application (default: info)",
|
tag: "LOG_LEVEL,description:Log level for the application,default:info",
|
||||||
wantEnvVar: &EnvVar{
|
wantEnvVar: &EnvVar{
|
||||||
Name: "LOG_LEVEL",
|
Name: "LOG_LEVEL",
|
||||||
Description: "Log level for the application",
|
Description: "Log level for the application",
|
||||||
@@ -37,7 +35,7 @@ func TestParseEnvComment(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "required env variable",
|
name: "required env variable",
|
||||||
comment: "ENV DATABASE_URL: Database connection string (required)",
|
tag: "DATABASE_URL,description:Database connection string,required",
|
||||||
wantEnvVar: &EnvVar{
|
wantEnvVar: &EnvVar{
|
||||||
Name: "DATABASE_URL",
|
Name: "DATABASE_URL",
|
||||||
Description: "Database connection string",
|
Description: "Database connection string",
|
||||||
@@ -48,24 +46,35 @@ func TestParseEnvComment(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "required with condition and default",
|
name: "required with condition and default",
|
||||||
comment: "ENV LOG_DIR: Directory for log files (required when LOG_OUTPUT is file) (default: /var/log)",
|
tag: "LOG_DIR,description:Directory for log files,required:when LOG_OUTPUT is file,default:/var/log",
|
||||||
wantEnvVar: &EnvVar{
|
wantEnvVar: &EnvVar{
|
||||||
Name: "LOG_DIR",
|
Name: "LOG_DIR",
|
||||||
Description: "Directory for log files",
|
Description: "Directory for log files (required when LOG_OUTPUT is file)",
|
||||||
Required: true,
|
Required: true,
|
||||||
Default: "/var/log",
|
Default: "/var/log",
|
||||||
},
|
},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing colon",
|
name: "name only",
|
||||||
comment: "ENV LOG_LEVEL Log level",
|
tag: "SIMPLE_VAR",
|
||||||
|
wantEnvVar: &EnvVar{
|
||||||
|
Name: "SIMPLE_VAR",
|
||||||
|
Description: "",
|
||||||
|
Required: false,
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty tag",
|
||||||
|
tag: "",
|
||||||
wantEnvVar: nil,
|
wantEnvVar: nil,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "not an ENV comment",
|
name: "empty name",
|
||||||
comment: "This is a regular comment",
|
tag: ",description:some desc",
|
||||||
wantEnvVar: nil,
|
wantEnvVar: nil,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
@@ -73,7 +82,7 @@ func TestParseEnvComment(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
envVar, err := parseEnvComment(tt.comment)
|
envVar, err := parseEzconfTag(tt.tag)
|
||||||
|
|
||||||
if tt.expectError {
|
if tt.expectError {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -103,32 +112,17 @@ func TestParseEnvComment(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseConfigFile(t *testing.T) {
|
func TestParseConfigStruct(t *testing.T) {
|
||||||
// Create a temporary test file
|
type TestConfig struct {
|
||||||
tempDir := t.TempDir()
|
LogLevel string `ezconf:"LOG_LEVEL,description:Log level for the application,default:info"`
|
||||||
testFile := filepath.Join(tempDir, "config.go")
|
LogOutput string `ezconf:"LOG_OUTPUT,description:Output destination,default:console"`
|
||||||
|
DatabaseURL string `ezconf:"DATABASE_URL,description:Database connection string,required"`
|
||||||
content := `package testpkg
|
NoTag string
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
// ENV LOG_LEVEL: Log level for the application (default: info)
|
|
||||||
LogLevel string
|
|
||||||
|
|
||||||
// ENV LOG_OUTPUT: Output destination (default: console)
|
|
||||||
LogOutput string
|
|
||||||
|
|
||||||
// ENV DATABASE_URL: Database connection string (required)
|
|
||||||
DatabaseURL string
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
if err := os.WriteFile(testFile, []byte(content), 0644); err != nil {
|
|
||||||
t.Fatalf("failed to create test file: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
envVars, err := ParseConfigFile(testFile)
|
envVars, err := ParseConfigStruct(&TestConfig{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseConfigFile failed: %v", err)
|
t.Fatalf("ParseConfigStruct failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(envVars) != 3 {
|
if len(envVars) != 3 {
|
||||||
@@ -152,51 +146,70 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseConfigPackage(t *testing.T) {
|
func TestParseConfigStruct_NilPointer(t *testing.T) {
|
||||||
// Test with actual hlog package
|
_, err := ParseConfigStruct(nil)
|
||||||
hlogPath := filepath.Join("..", "hlog")
|
|
||||||
if _, err := os.Stat(hlogPath); os.IsNotExist(err) {
|
|
||||||
t.Skip("hlog package not found, skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
envVars, err := ParseConfigPackage(hlogPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ParseConfigPackage failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(envVars) == 0 {
|
|
||||||
t.Error("expected at least one env var from hlog package")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for known hlog variables
|
|
||||||
foundLogLevel := false
|
|
||||||
for _, envVar := range envVars {
|
|
||||||
if envVar.Name == "LOG_LEVEL" {
|
|
||||||
foundLogLevel = true
|
|
||||||
t.Logf("Found LOG_LEVEL: %s", envVar.Description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !foundLogLevel {
|
|
||||||
t.Error("expected to find LOG_LEVEL in hlog package")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseConfigFile_InvalidFile(t *testing.T) {
|
|
||||||
_, err := ParseConfigFile("/nonexistent/file.go")
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("expected error for nonexistent file")
|
t.Error("expected error for nil pointer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseConfigPackage_InvalidPath(t *testing.T) {
|
func TestParseConfigStruct_NotPointer(t *testing.T) {
|
||||||
envVars, err := ParseConfigPackage("/nonexistent/package")
|
type TestConfig struct {
|
||||||
|
Foo string `ezconf:"FOO,description:test"`
|
||||||
|
}
|
||||||
|
_, err := ParseConfigStruct(TestConfig{})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for non-pointer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseConfigStruct_NotStruct(t *testing.T) {
|
||||||
|
str := "not a struct"
|
||||||
|
_, err := ParseConfigStruct(&str)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for non-struct pointer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseConfigStruct_NoTags(t *testing.T) {
|
||||||
|
type EmptyConfig struct {
|
||||||
|
Foo string
|
||||||
|
Bar int
|
||||||
|
}
|
||||||
|
|
||||||
|
envVars, err := ParseConfigStruct(&EmptyConfig{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseConfigPackage should not error on invalid path: %v", err)
|
t.Fatalf("ParseConfigStruct failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should return empty slice for invalid path
|
|
||||||
if len(envVars) != 0 {
|
if len(envVars) != 0 {
|
||||||
t.Errorf("expected 0 env vars for invalid path, got %d", len(envVars))
|
t.Errorf("expected 0 env vars for struct with no tags, got %d", len(envVars))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseConfigStruct_UnexportedFields(t *testing.T) {
|
||||||
|
type TestConfig struct {
|
||||||
|
exported string `ezconf:"EXPORTED,description:An exported field"`
|
||||||
|
unexported string `ezconf:"UNEXPORTED,description:An unexported field"`
|
||||||
|
}
|
||||||
|
|
||||||
|
envVars, err := ParseConfigStruct(&TestConfig{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseConfigStruct failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(envVars) != 2 {
|
||||||
|
t.Errorf("expected 2 env vars (both exported and unexported), got %d", len(envVars))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseConfigStruct_InvalidTag(t *testing.T) {
|
||||||
|
type TestConfig struct {
|
||||||
|
Bad string `ezconf:",description:missing name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ParseConfigStruct(&TestConfig{})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for invalid tag")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import (
|
|||||||
// It can be populated from environment variables using ConfigFromEnv
|
// It can be populated from environment variables using ConfigFromEnv
|
||||||
// or created programmatically.
|
// or created programmatically.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
LogLevel Level // ENV LOG_LEVEL: Log level for the logger - trace, debug, info, warn, error, fatal, panic (default: info)
|
LogLevel Level `ezconf:"LOG_LEVEL,description:Log level for the logger - trace debug info warn error fatal panic,default:info"`
|
||||||
LogOutput string // ENV LOG_OUTPUT: Output destination for logs - console, file, or both (default: console)
|
LogOutput string `ezconf:"LOG_OUTPUT,description:Output destination for logs - console file or both,default:console"`
|
||||||
LogDir string // ENV LOG_DIR: Directory path for log files (required when LOG_OUTPUT is "file" or "both")
|
LogDir string `ezconf:"LOG_DIR,description:Directory path for log files,required:when LOG_OUTPUT is file or both"`
|
||||||
LogFileName string // ENV LOG_FILE_NAME: Name of the log file (required when LOG_OUTPUT is "file" or "both")
|
LogFileName string `ezconf:"LOG_FILE_NAME,description:Name of the log file,required:when LOG_OUTPUT is file or both"`
|
||||||
LogAppend bool // ENV LOG_APPEND: Append to existing log file or overwrite (default: true)
|
LogAppend bool `ezconf:"LOG_APPEND,description:Append to existing log file or overwrite,default:true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigFromEnv loads logger configuration from environment variables.
|
// ConfigFromEnv loads logger configuration from environment variables.
|
||||||
|
|||||||
@@ -1,35 +1,9 @@
|
|||||||
package hlog
|
package hlog
|
||||||
|
|
||||||
import "runtime"
|
import "git.haelnorr.com/h/golib/ezconf"
|
||||||
|
|
||||||
// EZConfIntegration provides integration with ezconf for automatic configuration
|
// NewEZConfIntegration creates a new EZConf integration
|
||||||
type EZConfIntegration struct{}
|
func NewEZConfIntegration() *ezconf.Integration {
|
||||||
|
return ezconf.NewIntegration("hlog", "HLog",
|
||||||
// PackagePath returns the path to the hlog package for source parsing
|
&Config{}, func() (any, error) { return ConfigFromEnv() })
|
||||||
func (e EZConfIntegration) PackagePath() string {
|
|
||||||
_, filename, _, _ := runtime.Caller(0)
|
|
||||||
// Return directory of this file
|
|
||||||
return filename[:len(filename)-len("/ezconf.go")]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigFunc returns the ConfigFromEnv function for ezconf
|
|
||||||
func (e EZConfIntegration) ConfigFunc() func() (interface{}, error) {
|
|
||||||
return func() (interface{}, error) {
|
|
||||||
return ConfigFromEnv()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the name to use when registering with ezconf
|
|
||||||
func (e EZConfIntegration) Name() string {
|
|
||||||
return "hlog"
|
|
||||||
}
|
|
||||||
|
|
||||||
// GroupName returns the display name for grouping environment variables
|
|
||||||
func (e EZConfIntegration) GroupName() string {
|
|
||||||
return "HLog"
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEZConfIntegration creates a new EZConf integration helper
|
|
||||||
func NewEZConfIntegration() EZConfIntegration {
|
|
||||||
return EZConfIntegration{}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ require (
|
|||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require git.haelnorr.com/h/golib/ezconf v0.2.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.haelnorr.com/h/golib/env v0.9.1
|
git.haelnorr.com/h/golib/env v0.9.1
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
git.haelnorr.com/h/golib/env v0.9.1 h1:2Vsj+mJKnO5f1Md1GO5v9ggLN5zWa0baCewcSHTjoNY=
|
git.haelnorr.com/h/golib/env v0.9.1 h1:2Vsj+mJKnO5f1Md1GO5v9ggLN5zWa0baCewcSHTjoNY=
|
||||||
git.haelnorr.com/h/golib/env v0.9.1/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg=
|
git.haelnorr.com/h/golib/env v0.9.1/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg=
|
||||||
|
git.haelnorr.com/h/golib/ezconf v0.2.1 h1:axMyKtgO9Zk6E8CrYrLpMzifvpjz73yxCQq0lOtuhck=
|
||||||
|
git.haelnorr.com/h/golib/ezconf v0.2.1/go.mod h1:rETDcjpcEyyeBgCiZSU617wc0XycwZSC5+IAOtXmwP8=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Host string // ENV HWS_HOST: Host to listen on (default: 127.0.0.1)
|
Host string `ezconf:"HWS_HOST,description:Host to listen on,default:127.0.0.1"`
|
||||||
Port uint64 // ENV HWS_PORT: Port to listen on (default: 3000)
|
Port uint64 `ezconf:"HWS_PORT,description:Port to listen on,default:3000"`
|
||||||
GZIP bool // ENV HWS_GZIP: Flag for GZIP compression on requests (default: false)
|
GZIP bool `ezconf:"HWS_GZIP,description:Flag for GZIP compression on requests,default:false"`
|
||||||
ReadHeaderTimeout time.Duration // ENV HWS_READ_HEADER_TIMEOUT: Timeout for reading request headers in seconds (default: 2)
|
ReadHeaderTimeout time.Duration `ezconf:"HWS_READ_HEADER_TIMEOUT,description:Timeout for reading request headers in seconds,default:2"`
|
||||||
WriteTimeout time.Duration // ENV HWS_WRITE_TIMEOUT: Timeout for writing requests in seconds (default: 10)
|
WriteTimeout time.Duration `ezconf:"HWS_WRITE_TIMEOUT,description:Timeout for writing requests in seconds,default:10"`
|
||||||
IdleTimeout time.Duration // ENV HWS_IDLE_TIMEOUT: Timeout for idle connections in seconds (default: 120)
|
IdleTimeout time.Duration `ezconf:"HWS_IDLE_TIMEOUT,description:Timeout for idle connections in seconds,default:120"`
|
||||||
ShutdownDelay time.Duration // ENV HWS_SHUTDOWN_DELAY: Delay in seconds before server shutsdown when Shutdown is called (default: 5)
|
ShutdownDelay time.Duration `ezconf:"HWS_SHUTDOWN_DELAY,description:Delay in seconds before server shuts down when Shutdown is called,default:5"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigFromEnv returns a Config struct loaded from the environment variables
|
// ConfigFromEnv returns a Config struct loaded from the environment variables
|
||||||
|
|||||||
@@ -1,35 +1,9 @@
|
|||||||
package hws
|
package hws
|
||||||
|
|
||||||
import "runtime"
|
import "git.haelnorr.com/h/golib/ezconf"
|
||||||
|
|
||||||
// EZConfIntegration provides integration with ezconf for automatic configuration
|
// NewEZConfIntegration creates a new EZConf integration
|
||||||
type EZConfIntegration struct{}
|
func NewEZConfIntegration() *ezconf.Integration {
|
||||||
|
return ezconf.NewIntegration("hws", "HWS",
|
||||||
// PackagePath returns the path to the hws package for source parsing
|
&Config{}, func() (any, error) { return ConfigFromEnv() })
|
||||||
func (e EZConfIntegration) PackagePath() string {
|
|
||||||
_, filename, _, _ := runtime.Caller(0)
|
|
||||||
// Return directory of this file
|
|
||||||
return filename[:len(filename)-len("/ezconf.go")]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigFunc returns the ConfigFromEnv function for ezconf
|
|
||||||
func (e EZConfIntegration) ConfigFunc() func() (any, error) {
|
|
||||||
return func() (any, error) {
|
|
||||||
return ConfigFromEnv()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the name to use when registering with ezconf
|
|
||||||
func (e EZConfIntegration) Name() string {
|
|
||||||
return "hws"
|
|
||||||
}
|
|
||||||
|
|
||||||
// GroupName returns the display name for grouping environment variables
|
|
||||||
func (e EZConfIntegration) GroupName() string {
|
|
||||||
return "HWS"
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEZConfIntegration creates a new EZConf integration helper
|
|
||||||
func NewEZConfIntegration() EZConfIntegration {
|
|
||||||
return EZConfIntegration{}
|
|
||||||
}
|
}
|
||||||
|
|||||||
10
hws/go.mod
10
hws/go.mod
@@ -4,22 +4,24 @@ go 1.25.5
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
git.haelnorr.com/h/golib/env v0.9.1
|
git.haelnorr.com/h/golib/env v0.9.1
|
||||||
git.haelnorr.com/h/golib/hlog v0.9.0
|
git.haelnorr.com/h/golib/hlog v0.11.0
|
||||||
git.haelnorr.com/h/golib/notify v0.1.0
|
git.haelnorr.com/h/golib/notify v0.1.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
k8s.io/apimachinery v0.35.0
|
k8s.io/apimachinery v0.35.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require git.haelnorr.com/h/golib/ezconf v0.2.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/gobwas/glob v0.2.3
|
github.com/gobwas/glob v0.2.3
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rs/zerolog v1.34.0 // indirect
|
github.com/rs/zerolog v1.34.0 // indirect
|
||||||
golang.org/x/sys v0.12.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
k8s.io/klog/v2 v2.130.1 // indirect
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||||
|
|||||||
15
hws/go.sum
15
hws/go.sum
@@ -1,7 +1,9 @@
|
|||||||
git.haelnorr.com/h/golib/env v0.9.1 h1:2Vsj+mJKnO5f1Md1GO5v9ggLN5zWa0baCewcSHTjoNY=
|
git.haelnorr.com/h/golib/env v0.9.1 h1:2Vsj+mJKnO5f1Md1GO5v9ggLN5zWa0baCewcSHTjoNY=
|
||||||
git.haelnorr.com/h/golib/env v0.9.1/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg=
|
git.haelnorr.com/h/golib/env v0.9.1/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg=
|
||||||
git.haelnorr.com/h/golib/hlog v0.9.0 h1:ib8n2MdmiRK2TF067p220kXmhDe9aAnlcsgpuv+QpvE=
|
git.haelnorr.com/h/golib/ezconf v0.2.1 h1:axMyKtgO9Zk6E8CrYrLpMzifvpjz73yxCQq0lOtuhck=
|
||||||
git.haelnorr.com/h/golib/hlog v0.9.0/go.mod h1:oOlzb8UVHUYP1k7dN5PSJXVskAB2z8EYgRN85jAi0Zk=
|
git.haelnorr.com/h/golib/ezconf v0.2.1/go.mod h1:rETDcjpcEyyeBgCiZSU617wc0XycwZSC5+IAOtXmwP8=
|
||||||
|
git.haelnorr.com/h/golib/hlog v0.11.0 h1:tCT8HWs51Nbin58sCTLcq5re6CZqo5/IHCzk3G+S3vQ=
|
||||||
|
git.haelnorr.com/h/golib/hlog v0.11.0/go.mod h1:HjhXS5G3A0BwOZq7nu2qpNBtvOFiCa1GbAuBRxAkYqs=
|
||||||
git.haelnorr.com/h/golib/notify v0.1.0 h1:xdf6zd21F6n+SuGTeJiuLNMf6zFXMvwpKD0gmNq8N10=
|
git.haelnorr.com/h/golib/notify v0.1.0 h1:xdf6zd21F6n+SuGTeJiuLNMf6zFXMvwpKD0gmNq8N10=
|
||||||
git.haelnorr.com/h/golib/notify v0.1.0/go.mod h1:ARqaRmCYb8LMURhDM75sG+qX+YpqXmUVeAtacwjHjBc=
|
git.haelnorr.com/h/golib/notify v0.1.0/go.mod h1:ARqaRmCYb8LMURhDM75sG+qX+YpqXmUVeAtacwjHjBc=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
@@ -12,11 +14,13 @@ github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
|
|||||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@@ -28,8 +32,9 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import (
|
|||||||
func Test_GZIP_Compression(t *testing.T) {
|
func Test_GZIP_Compression(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
dbg, _ := hlog.LogLevel("debug")
|
||||||
|
logcfg := &hlog.Config{
|
||||||
|
LogLevel: dbg,
|
||||||
|
}
|
||||||
t.Run("GZIP enabled compresses response", func(t *testing.T) {
|
t.Run("GZIP enabled compresses response", func(t *testing.T) {
|
||||||
server, err := hws.NewServer(&hws.Config{
|
server, err := hws.NewServer(&hws.Config{
|
||||||
Host: "127.0.0.1",
|
Host: "127.0.0.1",
|
||||||
@@ -25,7 +29,7 @@ func Test_GZIP_Compression(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
logger, err := hlog.NewLogger(hlog.LogLevel("Debug"), &buf, nil, "")
|
logger, err := hlog.NewLogger(logcfg, &buf)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = server.AddLogger(logger)
|
err = server.AddLogger(logger)
|
||||||
@@ -80,7 +84,7 @@ func Test_GZIP_Compression(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
logger, err := hlog.NewLogger(hlog.LogLevel("Debug"), &buf, nil, "")
|
logger, err := hlog.NewLogger(logcfg, &buf)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = server.AddLogger(logger)
|
err = server.AddLogger(logger)
|
||||||
@@ -131,7 +135,7 @@ func Test_GZIP_Compression(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
logger, err := hlog.NewLogger(hlog.LogLevel("Debug"), &buf, nil, "")
|
logger, err := hlog.NewLogger(logcfg, &buf)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = server.AddLogger(logger)
|
err = server.AddLogger(logger)
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ func Test_AddLogger(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_LogError_AllLevels(t *testing.T) {
|
func Test_LogError_AllLevels(t *testing.T) {
|
||||||
|
dbg, _ := hlog.LogLevel("debug")
|
||||||
|
logcfg := &hlog.Config{
|
||||||
|
LogLevel: dbg,
|
||||||
|
}
|
||||||
t.Run("DEBUG level", func(t *testing.T) {
|
t.Run("DEBUG level", func(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
// Create server with logger explicitly set to Debug level
|
// Create server with logger explicitly set to Debug level
|
||||||
@@ -34,7 +38,7 @@ func Test_LogError_AllLevels(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
logger, err := hlog.NewLogger(hlog.LogLevel("debug"), &buf, nil, "")
|
logger, err := hlog.NewLogger(logcfg, &buf)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = server.AddLogger(logger)
|
err = server.AddLogger(logger)
|
||||||
|
|||||||
@@ -31,8 +31,12 @@ func createTestServer(t *testing.T, w io.Writer) *hws.Server {
|
|||||||
ShutdownDelay: 0, // No delay for tests
|
ShutdownDelay: 0, // No delay for tests
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
dbg, _ := hlog.LogLevel("debug")
|
||||||
|
logcfg := &hlog.Config{
|
||||||
|
LogLevel: dbg,
|
||||||
|
}
|
||||||
|
|
||||||
logger, err := hlog.NewLogger(hlog.LogLevel("Debug"), w, nil, "")
|
logger, err := hlog.NewLogger(logcfg, w)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = server.AddLogger(logger)
|
err = server.AddLogger(logger)
|
||||||
@@ -228,5 +232,4 @@ func Test_NewServer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,16 @@ import (
|
|||||||
// Config holds the configuration settings for the authenticator.
|
// Config holds the configuration settings for the authenticator.
|
||||||
// All time-based settings are in minutes.
|
// All time-based settings are in minutes.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
SSL bool // ENV HWSAUTH_SSL: Enable SSL secure cookies (default: false)
|
SSL bool `ezconf:"HWSAUTH_SSL,description:Enable SSL secure cookies,default:false"`
|
||||||
TrustedHost string // ENV HWSAUTH_TRUSTED_HOST: Full server address for SSL (required if SSL is true)
|
TrustedHost string `ezconf:"HWSAUTH_TRUSTED_HOST,description:Full server address for SSL,required:if SSL is true"`
|
||||||
SecretKey string // ENV HWSAUTH_SECRET_KEY: Secret key for signing JWT tokens (required)
|
SecretKey string `ezconf:"HWSAUTH_SECRET_KEY,description:Secret key for signing JWT tokens,required"`
|
||||||
AccessTokenExpiry int64 // ENV HWSAUTH_ACCESS_TOKEN_EXPIRY: Access token expiry in minutes (default: 5)
|
AccessTokenExpiry int64 `ezconf:"HWSAUTH_ACCESS_TOKEN_EXPIRY,description:Access token expiry in minutes,default:5"`
|
||||||
RefreshTokenExpiry int64 // ENV HWSAUTH_REFRESH_TOKEN_EXPIRY: Refresh token expiry in minutes (default: 1440)
|
RefreshTokenExpiry int64 `ezconf:"HWSAUTH_REFRESH_TOKEN_EXPIRY,description:Refresh token expiry in minutes,default:1440"`
|
||||||
TokenFreshTime int64 // ENV HWSAUTH_TOKEN_FRESH_TIME: Token fresh time in minutes (default: 5)
|
TokenFreshTime int64 `ezconf:"HWSAUTH_TOKEN_FRESH_TIME,description:Token fresh time in minutes,default:5"`
|
||||||
LandingPage string // ENV HWSAUTH_LANDING_PAGE: Redirect destination for authenticated users (default: "/profile")
|
LandingPage string `ezconf:"HWSAUTH_LANDING_PAGE,description:Redirect destination for authenticated users,default:/profile"`
|
||||||
DatabaseType string // ENV HWSAUTH_DATABASE_TYPE: Database type (postgres, mysql, sqlite, mariadb) (default: "postgres")
|
DatabaseType string `ezconf:"HWSAUTH_DATABASE_TYPE,description:Database type (postgres mysql sqlite mariadb),default:postgres"`
|
||||||
DatabaseVersion string // ENV HWSAUTH_DATABASE_VERSION: Database version string (default: "15")
|
DatabaseVersion string `ezconf:"HWSAUTH_DATABASE_VERSION,description:Database version string,default:15"`
|
||||||
JWTTableName string // ENV HWSAUTH_JWT_TABLE_NAME: Custom JWT blacklist table name (default: "jwtblacklist")
|
JWTTableName string `ezconf:"HWSAUTH_JWT_TABLE_NAME,description:Custom JWT blacklist table name,default:jwtblacklist"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigFromEnv loads configuration from environment variables.
|
// ConfigFromEnv loads configuration from environment variables.
|
||||||
|
|||||||
@@ -1,35 +1,9 @@
|
|||||||
package hwsauth
|
package hwsauth
|
||||||
|
|
||||||
import "runtime"
|
import "git.haelnorr.com/h/golib/ezconf"
|
||||||
|
|
||||||
// EZConfIntegration provides integration with ezconf for automatic configuration
|
|
||||||
type EZConfIntegration struct{}
|
|
||||||
|
|
||||||
// PackagePath returns the path to the hwsauth package for source parsing
|
|
||||||
func (e EZConfIntegration) PackagePath() string {
|
|
||||||
_, filename, _, _ := runtime.Caller(0)
|
|
||||||
// Return directory of this file
|
|
||||||
return filename[:len(filename)-len("/ezconf.go")]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigFunc returns the ConfigFromEnv function for ezconf
|
|
||||||
func (e EZConfIntegration) ConfigFunc() func() (any, error) {
|
|
||||||
return func() (any, error) {
|
|
||||||
return ConfigFromEnv()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the name to use when registering with ezconf
|
|
||||||
func (e EZConfIntegration) Name() string {
|
|
||||||
return "hwsauth"
|
|
||||||
}
|
|
||||||
|
|
||||||
// GroupName returns the display name for grouping environment variables
|
|
||||||
func (e EZConfIntegration) GroupName() string {
|
|
||||||
return "HWSAuth"
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEZConfIntegration creates a new EZConf integration helper
|
// NewEZConfIntegration creates a new EZConf integration helper
|
||||||
func NewEZConfIntegration() EZConfIntegration {
|
func NewEZConfIntegration() *ezconf.Integration {
|
||||||
return EZConfIntegration{}
|
return ezconf.NewIntegration("hwsauth", "HWSAuth", &Config{},
|
||||||
|
func() (any, error) { return ConfigFromEnv() })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ go 1.25.5
|
|||||||
require (
|
require (
|
||||||
git.haelnorr.com/h/golib/cookies v0.9.0
|
git.haelnorr.com/h/golib/cookies v0.9.0
|
||||||
git.haelnorr.com/h/golib/env v0.9.1
|
git.haelnorr.com/h/golib/env v0.9.1
|
||||||
git.haelnorr.com/h/golib/hlog v0.10.4
|
git.haelnorr.com/h/golib/ezconf v0.2.1
|
||||||
|
git.haelnorr.com/h/golib/hlog v0.11.0
|
||||||
git.haelnorr.com/h/golib/hws v0.5.0
|
git.haelnorr.com/h/golib/hws v0.5.0
|
||||||
git.haelnorr.com/h/golib/jwt v0.10.1
|
git.haelnorr.com/h/golib/jwt v0.10.1
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
@@ -25,7 +26,7 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rs/zerolog v1.34.0 // indirect
|
github.com/rs/zerolog v1.34.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
k8s.io/apimachinery v0.35.0 // indirect
|
k8s.io/apimachinery v0.35.0 // indirect
|
||||||
k8s.io/klog/v2 v2.130.1 // indirect
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ git.haelnorr.com/h/golib/cookies v0.9.0 h1:Vf+eX1prHkKuGrQon1BHY87yaPc1H+HJFRXDO
|
|||||||
git.haelnorr.com/h/golib/cookies v0.9.0/go.mod h1:y1385YExI9gLwckCVDCYVcsFXr6N7T3brJjnJD2QIuo=
|
git.haelnorr.com/h/golib/cookies v0.9.0/go.mod h1:y1385YExI9gLwckCVDCYVcsFXr6N7T3brJjnJD2QIuo=
|
||||||
git.haelnorr.com/h/golib/env v0.9.1 h1:2Vsj+mJKnO5f1Md1GO5v9ggLN5zWa0baCewcSHTjoNY=
|
git.haelnorr.com/h/golib/env v0.9.1 h1:2Vsj+mJKnO5f1Md1GO5v9ggLN5zWa0baCewcSHTjoNY=
|
||||||
git.haelnorr.com/h/golib/env v0.9.1/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg=
|
git.haelnorr.com/h/golib/env v0.9.1/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg=
|
||||||
git.haelnorr.com/h/golib/hlog v0.10.4 h1:vpCsV/OddjIYx8F48U66WxojjmhEbeLGQAOBG4ViSRQ=
|
git.haelnorr.com/h/golib/ezconf v0.2.1 h1:axMyKtgO9Zk6E8CrYrLpMzifvpjz73yxCQq0lOtuhck=
|
||||||
git.haelnorr.com/h/golib/hlog v0.10.4/go.mod h1:+wJ8vecQY/JITTXKmI3JfkHiUGyMs7N6wooj2wuWZbc=
|
git.haelnorr.com/h/golib/ezconf v0.2.1/go.mod h1:rETDcjpcEyyeBgCiZSU617wc0XycwZSC5+IAOtXmwP8=
|
||||||
|
git.haelnorr.com/h/golib/hlog v0.11.0 h1:tCT8HWs51Nbin58sCTLcq5re6CZqo5/IHCzk3G+S3vQ=
|
||||||
|
git.haelnorr.com/h/golib/hlog v0.11.0/go.mod h1:HjhXS5G3A0BwOZq7nu2qpNBtvOFiCa1GbAuBRxAkYqs=
|
||||||
git.haelnorr.com/h/golib/hws v0.5.0 h1:0CSv2f+dm/KzB/o5o6uXCyvN74iBdMTImhkyAZzU52c=
|
git.haelnorr.com/h/golib/hws v0.5.0 h1:0CSv2f+dm/KzB/o5o6uXCyvN74iBdMTImhkyAZzU52c=
|
||||||
git.haelnorr.com/h/golib/hws v0.5.0/go.mod h1:dxAbbGGNzqLXhZXwgt091QsvsPBdrS+1YsNQNldNVoM=
|
git.haelnorr.com/h/golib/hws v0.5.0/go.mod h1:dxAbbGGNzqLXhZXwgt091QsvsPBdrS+1YsNQNldNVoM=
|
||||||
git.haelnorr.com/h/golib/jwt v0.10.1 h1:1Adxt9H3Y4fWFvFjWpvg/vSFhbgCMDMxgiE3m7KvDMI=
|
git.haelnorr.com/h/golib/jwt v0.10.1 h1:1Adxt9H3Y4fWFvFjWpvg/vSFhbgCMDMxgiE3m7KvDMI=
|
||||||
@@ -44,8 +46,8 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
|
|||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -33,14 +33,18 @@ func (auth *Authenticator[T, TX]) Logout(tx TX, w http.ResponseWriter, r *http.R
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "auth.getTokens")
|
return errors.Wrap(err, "auth.getTokens")
|
||||||
}
|
}
|
||||||
|
if aT != nil {
|
||||||
err = aT.Revoke(jwt.DBTransaction(tx))
|
err = aT.Revoke(jwt.DBTransaction(tx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "aT.Revoke")
|
return errors.Wrap(err, "aT.Revoke")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if rT != nil {
|
||||||
err = rT.Revoke(jwt.DBTransaction(tx))
|
err = rT.Revoke(jwt.DBTransaction(tx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "rT.Revoke")
|
return errors.Wrap(err, "rT.Revoke")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
cookies.DeleteCookie(w, "access", "/")
|
cookies.DeleteCookie(w, "access", "/")
|
||||||
cookies.DeleteCookie(w, "refresh", "/")
|
cookies.DeleteCookie(w, "refresh", "/")
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -16,12 +16,20 @@ import (
|
|||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// server.AddMiddleware(auth.Authenticate())
|
// server.AddMiddleware(auth.Authenticate(nil))
|
||||||
func (auth *Authenticator[T, TX]) Authenticate() hws.Middleware {
|
//
|
||||||
return auth.server.NewMiddleware(auth.authenticate())
|
// If extraCheck is provided, it will run just before the user is added to the context,
|
||||||
|
// and the return will determine if the user will be added, or the request passed on
|
||||||
|
// without the user.
|
||||||
|
func (auth *Authenticator[T, TX]) Authenticate(
|
||||||
|
extraCheck func(ctx context.Context, model T, tx TX, w http.ResponseWriter, r *http.Request) (bool, *hws.HWSError),
|
||||||
|
) hws.Middleware {
|
||||||
|
return auth.server.NewMiddleware(auth.authenticate(extraCheck))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Authenticator[T, TX]) authenticate() hws.MiddlewareFunc {
|
func (auth *Authenticator[T, TX]) authenticate(
|
||||||
|
extraCheck func(ctx context.Context, model T, tx TX, w http.ResponseWriter, r *http.Request) (bool, *hws.HWSError),
|
||||||
|
) hws.MiddlewareFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) (*http.Request, *hws.HWSError) {
|
return func(w http.ResponseWriter, r *http.Request) (*http.Request, *hws.HWSError) {
|
||||||
if globTest(r.URL.Path, auth.ignoredPaths) {
|
if globTest(r.URL.Path, auth.ignoredPaths) {
|
||||||
return r, nil
|
return r, nil
|
||||||
@@ -66,6 +74,14 @@ func (auth *Authenticator[T, TX]) authenticate() hws.MiddlewareFunc {
|
|||||||
Msg("Failed to authenticate user")
|
Msg("Failed to authenticate user")
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
var check bool
|
||||||
|
if extraCheck != nil {
|
||||||
|
var err *hws.HWSError
|
||||||
|
check, err = extraCheck(ctx, model.model, txTyped, w, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &hws.HWSError{
|
return nil, &hws.HWSError{
|
||||||
@@ -76,8 +92,11 @@ func (auth *Authenticator[T, TX]) authenticate() hws.MiddlewareFunc {
|
|||||||
}
|
}
|
||||||
authContext := setAuthenticatedModel(r.Context(), model)
|
authContext := setAuthenticatedModel(r.Context(), model)
|
||||||
newReq := r.WithContext(authContext)
|
newReq := r.WithContext(authContext)
|
||||||
|
if extraCheck == nil || check {
|
||||||
return newReq, nil
|
return newReq, nil
|
||||||
}
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func globTest(testPath string, globs []glob.Glob) bool {
|
func globTest(testPath string, globs []glob.Glob) bool {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (auth *Authenticator[T, TX]) RefreshAuthTokens(tx TX, w http.ResponseWriter
|
|||||||
rememberMe := map[string]bool{
|
rememberMe := map[string]bool{
|
||||||
"session": false,
|
"session": false,
|
||||||
"exp": true,
|
"exp": true,
|
||||||
}[aT.TTL]
|
}[rT.TTL]
|
||||||
// issue new tokens for the user
|
// issue new tokens for the user
|
||||||
err = jwt.SetTokenCookies(w, r, auth.tokenGenerator, rT.SUB, true, rememberMe, auth.SSL)
|
err = jwt.SetTokenCookies(w, r, auth.tokenGenerator, rT.SUB, true, rememberMe, auth.SSL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -55,14 +55,21 @@ func (auth *Authenticator[T, TX]) getTokens(
|
|||||||
) (*jwt.AccessToken, *jwt.RefreshToken, error) {
|
) (*jwt.AccessToken, *jwt.RefreshToken, error) {
|
||||||
// get the existing tokens from the cookies
|
// get the existing tokens from the cookies
|
||||||
atStr, rtStr := jwt.GetTokenCookies(r)
|
atStr, rtStr := jwt.GetTokenCookies(r)
|
||||||
aT, err := auth.tokenGenerator.ValidateAccess(jwt.DBTransaction(tx), atStr)
|
var aT *jwt.AccessToken
|
||||||
|
var rT *jwt.RefreshToken
|
||||||
|
var err error
|
||||||
|
if atStr != "" {
|
||||||
|
aT, err = auth.tokenGenerator.ValidateAccess(jwt.DBTransaction(tx), atStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrap(err, "tokenGenerator.ValidateAccess")
|
return nil, nil, errors.Wrap(err, "tokenGenerator.ValidateAccess")
|
||||||
}
|
}
|
||||||
rT, err := auth.tokenGenerator.ValidateRefresh(jwt.DBTransaction(tx), rtStr)
|
}
|
||||||
|
if rtStr != "" {
|
||||||
|
rT, err = auth.tokenGenerator.ValidateRefresh(jwt.DBTransaction(tx), rtStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrap(err, "tokenGenerator.ValidateRefresh")
|
return nil, nil, errors.Wrap(err, "tokenGenerator.ValidateRefresh")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return aT, rT, nil
|
return aT, rT, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,13 +79,17 @@ func revokeTokenPair(
|
|||||||
aT *jwt.AccessToken,
|
aT *jwt.AccessToken,
|
||||||
rT *jwt.RefreshToken,
|
rT *jwt.RefreshToken,
|
||||||
) error {
|
) error {
|
||||||
|
if aT != nil {
|
||||||
err := aT.Revoke(tx)
|
err := aT.Revoke(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "aT.Revoke")
|
return errors.Wrap(err, "aT.Revoke")
|
||||||
}
|
}
|
||||||
err = rT.Revoke(tx)
|
}
|
||||||
|
if rT != nil {
|
||||||
|
err := rT.Revoke(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "rT.Revoke")
|
return errors.Wrap(err, "rT.Revoke")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
type API struct {
|
type API struct {
|
||||||
*Config
|
*Config
|
||||||
token string // ENV TMDB_TOKEN: API token for TMDB (required)
|
token string `ezconf:"TMDB_TOKEN,description:API token for TMDB,required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPIConnection() (*API, error) {
|
func NewAPIConnection() (*API, error) {
|
||||||
|
|||||||
@@ -1,36 +1,9 @@
|
|||||||
package tmdb
|
package tmdb
|
||||||
|
|
||||||
import "runtime"
|
import "git.haelnorr.com/h/golib/ezconf"
|
||||||
|
|
||||||
// EZConfIntegration provides integration with ezconf for automatic configuration
|
// NewEZConfIntegration creates a new EZConf integration
|
||||||
type EZConfIntegration struct{}
|
func NewEZConfIntegration() *ezconf.Integration {
|
||||||
|
return ezconf.NewIntegration("tmdb", "TMDB", &Config{},
|
||||||
// PackagePath returns the path to the tmdb package for source parsing
|
func() (any, error) { return NewAPIConnection() })
|
||||||
func (e EZConfIntegration) PackagePath() string {
|
|
||||||
_, filename, _, _ := runtime.Caller(0)
|
|
||||||
// Return directory of this file
|
|
||||||
return filename[:len(filename)-len("/ezconf.go")]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigFunc returns the NewAPIConnection function for ezconf
|
|
||||||
// Note: tmdb uses NewAPIConnection instead of ConfigFromEnv
|
|
||||||
func (e EZConfIntegration) ConfigFunc() func() (interface{}, error) {
|
|
||||||
return func() (interface{}, error) {
|
|
||||||
return NewAPIConnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the name to use when registering with ezconf
|
|
||||||
func (e EZConfIntegration) Name() string {
|
|
||||||
return "tmdb"
|
|
||||||
}
|
|
||||||
|
|
||||||
// GroupName returns the display name for grouping environment variables
|
|
||||||
func (e EZConfIntegration) GroupName() string {
|
|
||||||
return "TMDB"
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEZConfIntegration creates a new EZConf integration helper
|
|
||||||
func NewEZConfIntegration() EZConfIntegration {
|
|
||||||
return EZConfIntegration{}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,5 @@ require (
|
|||||||
git.haelnorr.com/h/golib/env v0.9.1
|
git.haelnorr.com/h/golib/env v0.9.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require git.haelnorr.com/h/golib/ezconf v0.2.1
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
git.haelnorr.com/h/golib/env v0.9.1 h1:2Vsj+mJKnO5f1Md1GO5v9ggLN5zWa0baCewcSHTjoNY=
|
git.haelnorr.com/h/golib/env v0.9.1 h1:2Vsj+mJKnO5f1Md1GO5v9ggLN5zWa0baCewcSHTjoNY=
|
||||||
git.haelnorr.com/h/golib/env v0.9.1/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg=
|
git.haelnorr.com/h/golib/env v0.9.1/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg=
|
||||||
|
git.haelnorr.com/h/golib/ezconf v0.2.1 h1:axMyKtgO9Zk6E8CrYrLpMzifvpjz73yxCQq0lOtuhck=
|
||||||
|
git.haelnorr.com/h/golib/ezconf v0.2.1/go.mod h1:rETDcjpcEyyeBgCiZSU617wc0XycwZSC5+IAOtXmwP8=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
|||||||
Reference in New Issue
Block a user