Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for slog.Level fields #2

Merged
merged 2 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 31 additions & 23 deletions ezconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ezconf
import (
"flag"
"fmt"
"log/slog"
"os"
"sort"
"strconv"
Expand All @@ -14,19 +15,21 @@ import (
)

// CamelToSnake converts a CamelCase strings to a snake_case using the following algorithm:
// 1) for every transition from upper->lowercase insert an underscore before the uppercase character
// 2) for every transition fro lowercase->uppercase insert an underscore before the uppercase
// 3) lowercase resulting string
//
// Examples:
// CamelCase -> camel_case
// AWSConfig -> aws_config
// IPAddress -> ip_address
// S3MediaPrefix -> s3_media_prefix
// Route53Region -> route53_region
// CamelCaseA -> camel_case_a
// CamelABCCaseDEF -> camel_abc_case_def
// 1. for every transition from upper->lowercase insert an underscore before the uppercase character
//
// 2. for every transition fro lowercase->uppercase insert an underscore before the uppercase
//
// 3. lowercase resulting string
//
// Examples:
// CamelCase -> camel_case
// AWSConfig -> aws_config
// IPAddress -> ip_address
// S3MediaPrefix -> s3_media_prefix
// Route53Region -> route53_region
// CamelCaseA -> camel_case_a
// CamelABCCaseDEF -> camel_abc_case_def
func CamelToSnake(camel string) string {
snakes := make([]string, 0, 4)
snake := strings.Builder{}
Expand Down Expand Up @@ -76,7 +79,6 @@ func CamelToSnake(camel string) string {
// 2. TOML files you specify (optional)
// 3. Set environment variables
// 4. Command line parameters
//
type EZLoader struct {
name string
description string
Expand All @@ -94,7 +96,6 @@ type EZLoader struct {
// `name` and `description` are used to build environment variables and help parameters. The list of files
// can be nil, or can contain optional files to read TOML configuration from in priority order. The first file
// found and parsed will end parsing of others, but there is no requirement that any file is found.
//
func NewLoader(config interface{}, name string, description string, files []string) *EZLoader {
return &EZLoader{
name: name,
Expand All @@ -106,12 +107,11 @@ func NewLoader(config interface{}, name string, description string, files []stri
}

// MustLoad loads our configuration from our sources in the order of:
// 1. TOML files
// 2. Environment variables
// 3. Command line parameters
// 1. TOML files
// 2. Environment variables
// 3. Command line parameters
//
// If any error is encountered, the program will exit reporting the error and showing usage.
//
func (ez *EZLoader) MustLoad() {
err := ez.Load()
if err != nil {
Expand All @@ -122,12 +122,11 @@ func (ez *EZLoader) MustLoad() {
}

// Load loads our configuration from our sources in the order of:
// 1. TOML files
// 2. Environment variables
// 3. Command line parameters
// 1. TOML files
// 2. Environment variables
// 3. Command line parameters
//
// If any error is encountered it is returned for the caller to process.
//
func (ez *EZLoader) Load() error {
// first build our mapping of name snake_case -> structs.Field
fields, err := buildFields(ez.config)
Expand Down Expand Up @@ -340,12 +339,20 @@ func setValues(fields *ezFields, values map[string]ezValue) error {
}

f.Set(t)

case slog.Level:
var level slog.Level
err := level.UnmarshalText([]byte(value))
if err != nil {
return err
}
f.Set(level)
}
}
return nil
}

func buildFields(config interface{}) (*ezFields, error) {
func buildFields(config any) (*ezFields, error) {
fields := make(map[string]*structs.Field)
s := structs.New(config)
for _, f := range s.Fields() {
Expand All @@ -356,7 +363,8 @@ func buildFields(config interface{}) (*ezFields, error) {
float32, float64,
bool,
string,
time.Time:
time.Time,
slog.Level:
name := CamelToSnake(f.Name())
dupe, found := fields[name]
if found {
Expand Down
10 changes: 9 additions & 1 deletion ezconf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ezconf

import (
"fmt"
"log/slog"
"os"
"testing"
"time"
Expand Down Expand Up @@ -34,6 +35,7 @@ type allTypes struct {
MyBool bool
MyString string
MyDatetime time.Time
MyLogLevel slog.Level
}

func toFields(t *testing.T, s interface{}) *ezFields {
Expand Down Expand Up @@ -116,6 +118,10 @@ func TestSetValue(t *testing.T) {
{"my_datetime", "2018-04-03T05:30:00.123+07:00", false, "2018-04-03 05:30:00.123 +0700 +0700"},
{"my_datetime", "notdate", true, ""},

{"my_log_level", "info", false, "INFO"},
{"my_log_level", "ERROR", false, "ERROR"},
{"my_log_level", "crazy", true, ""},

{"unknown", "", true, ""},
}

Expand All @@ -139,9 +145,11 @@ func TestSetValue(t *testing.T) {
func TestEndToEnd(t *testing.T) {
at := &allTypes{}
conf := NewLoader(at, "foo", "description", []string{"testdata/missing.toml", "testdata/fields.toml", "testdata/simple.toml"})
conf.args = []string{"-my-int=48", "-debug-conf"}
conf.args = []string{"-my-int=48", "-my-log-level=error", "-debug-conf"}
err := conf.Load()
assert.NoError(t, err)
assert.Equal(t, 48, at.MyInt)
assert.Equal(t, slog.LevelError, at.MyLogLevel)
}

func TestPriority(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ezconf
import (
"flag"
"fmt"
"log/slog"
"strings"
"time"
)
Expand Down Expand Up @@ -91,6 +92,9 @@ func buildFlags(name string, description string, fields *ezFields, errorHandling

case time.Time:
flags.String(flagName, formatDatetime(f.Value().(time.Time)), help)

case slog.Level:
flags.String(flagName, v.String(), help)
}
}

Expand Down
1 change: 1 addition & 0 deletions testdata/simple.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
my_int = 32
my_bool = true
my_datetime = 2018-04-03T05:30:00Z
my_log_level = "info"

# arrays cannot
my_ints = [10, 20, 30]
Expand Down
2 changes: 1 addition & 1 deletion toml.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
// Iterates the list of files, parsing the first that is found and loading the
// result into the passed in struct pointer. If no files are passed in or
// no files are found, this is a noop.
func parseTOMLFiles(config interface{}, files []string, debug bool) error {
func parseTOMLFiles(config any, files []string, debug bool) error {
// search through our list of files, stopping when we find one
for i, file := range files {
toml, err := os.ReadFile(file)
Expand Down
2 changes: 2 additions & 0 deletions toml_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ezconf

import (
"log/slog"
"testing"
"time"

Expand All @@ -11,6 +12,7 @@ type simpleStruct struct {
MyInt int
MyBool bool
MyDatetime time.Time
MyLogLevel slog.Level

MyInts []int

Expand Down