Skip to content

Commit

Permalink
Split out strict parsing into own option, add pulling config from url…
Browse files Browse the repository at this point in the history
… for fun
  • Loading branch information
NHAS committed Nov 10, 2024
1 parent 76c1f46 commit 34d4cce
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 20 deletions.
23 changes: 21 additions & 2 deletions config_file_parsing.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package confy

import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
Expand Down Expand Up @@ -163,7 +164,15 @@ func LoadConfigBytes[T any](data []byte, strict bool, configType ConfigType) (re
panic("LoadConfigBytes(...) only supports configs of Struct type")
}

result, _, err = Config[T](FromConfigBytes(data, strict, configType))
opts := []OptionFunc{
FromConfigBytes(data, configType),
}

if strict {
opts = append(opts, WithStrictParsing())
}

result, _, err = Config[T](opts...)

return
}
Expand All @@ -180,7 +189,15 @@ func LoadConfigFile[T any](path string, strict bool, configType ConfigType) (res
panic("LoadConfigFile(...) only supports configs of Struct type")
}

result, _, err = Config[T](FromConfigFile(path, strict, configType))
opts := []OptionFunc{
FromConfigFile(path, configType),
}

if strict {
opts = append(opts, WithStrictParsing())
}

result, _, err = Config[T](opts...)

return
}
Expand Down Expand Up @@ -235,6 +252,8 @@ func (cp *configParser[T]) apply(result *T) (err error) {
tmlDec = tmlDec.DisallowUnknownFields()
}
decoder = tmlDec
default:
return errors.New("config type could not be determined")
}

err = decoder.Decode(clone)
Expand Down
125 changes: 108 additions & 17 deletions entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import (
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"strings"
"time"
)

type optionFunc func(*options) error
type OptionFunc func(*options) error

type configDataOptions struct {
strictParsing bool
Expand Down Expand Up @@ -109,7 +112,7 @@ func init() {
// would look for environment variables:
// Thing
// Nested_NestedField
func Config[T any](suppliedOptions ...optionFunc) (result T, warnings []error, err error) {
func Config[T any](suppliedOptions ...OptionFunc) (result T, warnings []error, err error) {
if reflect.TypeOf(result).Kind() != reflect.Struct {
panic("Config(...) only supports configs of Struct type")
}
Expand All @@ -126,7 +129,7 @@ func Config[T any](suppliedOptions ...optionFunc) (result T, warnings []error, e
}

if len(o.order) == 0 {
if err := Defaults("config.json", false)(&o); err != nil {
if err := Defaults("config.json")(&o); err != nil {
return result, nil, err
}
}
Expand Down Expand Up @@ -165,7 +168,7 @@ func Config[T any](suppliedOptions ...optionFunc) (result T, warnings []error, e
// WithLogLevel sets the current slog output level
// Defaulty logging is disabled
// Very useful for debugging
func WithLogLevel(logLevel slog.Level) optionFunc {
func WithLogLevel(logLevel slog.Level) OptionFunc {
return func(c *options) error {
level.Set(logLevel)
return nil
Expand All @@ -176,11 +179,11 @@ func WithLogLevel(logLevel slog.Level) optionFunc {
// The configs struct will be configured config file -> envs -> cli, so that cli takes precedence over more static options, for ease of user configuration.
// The config file will be parsed in a non-strict way (unknown fields will just be ignored) and the config file type is automatically determined from extension (supports yaml, toml and json), if you want to change this, add the FromConfigFile(...) option after Defaults(...)
// path string : config file path
func Defaults(path string, strictConfigFileParsing bool) optionFunc {
func Defaults(path string) OptionFunc {
return func(c *options) error {

// Process in config file -> env -> cli order
err := FromConfigFile(path, false, Auto)(c)
err := FromConfigFile(path, Auto)(c)
if err != nil {
return err
}
Expand All @@ -200,9 +203,8 @@ func Defaults(path string, strictConfigFileParsing bool) optionFunc {

// FromConfigFile tells confy to load a config file from path
// path: string config file path
// strictParsing: bool allow unknown fields to exist in config file
// configType: ConfigType, what type the config file is expected to be, use `Auto` if you dont care and just want it to choose for you. Supports yaml, toml and json
func FromConfigFile(path string, strictParsing bool, configType ConfigType) optionFunc {
func FromConfigFile(path string, configType ConfigType) OptionFunc {
return func(c *options) error {
if c.currentlySet[configFile] {
return errors.New("duplicate configuration information source, " + string(configFile) + " FromConfig* option set twice, mutually exclusive")
Expand Down Expand Up @@ -239,8 +241,6 @@ func FromConfigFile(path string, strictParsing bool, configType ConfigType) opti
return configData, fileType, err
}

c.config.strictParsing = strictParsing

c.order = append(c.order, configFile)

return nil
Expand All @@ -249,9 +249,8 @@ func FromConfigFile(path string, strictParsing bool, configType ConfigType) opti

// FromConfigBytes tells confy to load a config file from raw bytes
// data: []byte config file raw bytes
// strictParsing: bool allow unknown fields to exist in config file
// configType: ConfigType, what type the config bytes are supports yaml, toml and json, the Auto configuration will return an error
func FromConfigBytes(data []byte, strictParsing bool, configType ConfigType) optionFunc {
func FromConfigBytes(data []byte, configType ConfigType) OptionFunc {
return func(c *options) error {
if c.currentlySet[configFile] {
return errors.New("duplicate configuration information source, " + string(configFile) + " FromConfig* option set twice, mutually exclusive")
Expand All @@ -274,14 +273,106 @@ func FromConfigBytes(data []byte, strictParsing bool, configType ConfigType) opt
return bytes.NewBuffer(data), configType, nil
}

c.config.strictParsing = strictParsing
c.order = append(c.order, configFile)

return nil
}
}

// FromConfigURL tells confy to load file from a url (http/https)
// url: string url of configuration file
// configType: ConfigType, what type the config file is expected to be, use `Auto` if you dont care and just want it to choose for you. Supports yaml, toml and json
func FromConfigURL(urlOpt string, configType ConfigType) OptionFunc {
return func(c *options) error {
if c.currentlySet[configFile] {
return errors.New("duplicate configuration information source, " + string(configFile) + " FromConfig* option set twice, mutually exclusive")
}
c.currentlySet[configFile] = true

c.config.dataMethod = func() (io.Reader, ConfigType, error) {

u, err := url.Parse(urlOpt)
if err != nil {
return nil, configType, err
}

client := http.Client{
Timeout: 20 * time.Second,
}

resp, err := client.Get(urlOpt)
if err != nil {
return nil, configType, fmt.Errorf("failed to get config from url: %s, err: %s", urlOpt, err)
}

if resp.StatusCode < 200 || resp.StatusCode > 299 {
resp.Body.Close()
return nil, configType, fmt.Errorf("status code was not okay: %s", resp.Status)
}

var fileType ConfigType
if configType == Auto {
ext := strings.ToLower(filepath.Ext(u.Path))
switch ext {
case ".yml", ".yaml":
logger.Info("yaml chosen as config type from extension", "url_path", u.Path)

fileType = Yaml
case ".json", ".js":
logger.Info("json chosen as config type from extension", "url_path", u.Path)

fileType = Json
case ".toml", ".tml":
logger.Info("toml chosen as config type from extension", "url_path", u.Path)

fileType = Toml
default:
logger.Info("no extension in url, using content type instead")
contentType := resp.Header.Get("content-type")
switch contentType {
case "application/yaml", "application/x-yaml", "text/yaml":
fileType = Yaml
case "application/json":
fileType = Json
case "text/x-toml", "application/toml", "text/toml":
fileType = Toml
default:
resp.Body.Close()
return nil, configType, fmt.Errorf("could not automatically determine config format from extension %q or content-type %q", ext, contentType)

}

}
}

return resp.Body, fileType, err
}

c.order = append(c.order, configFile)
return nil
}
}

// FromConfigFileFlagPath tells confy to load file from path as specified by cli flag
// cliFlagName: string cli option that defines config filepath
// configType: ConfigType, what type the config file is expected to be, use `Auto` if you dont care and just want it to choose for you. Supports yaml, toml and json
func FromConfigFileFlagPath(cliFlagName string, configType ConfigType) OptionFunc {
return func(c *options) error {

FromConfigFile("", configType)

return nil
}
}

// WithStrictParsing parse config files in a strict way, do not allow unknown fields
func WithStrictParsing() OptionFunc {
return func(c *options) error {
c.config.strictParsing = true
return nil
}
}

// FromEnvs sets confy to automatically populate the configuration structure from environment variables
// delimiter: string when looking for environment variables this string should be used for denoting nested structures
// e.g
Expand All @@ -300,7 +391,7 @@ func FromConfigBytes(data []byte, strictParsing bool, configType ConfigType) opt
//
// Configuring from Envs cannot be as comprehensive for complex types (like structures) as using the configuration file.
// To unmarshal very complex structs, the struct must implement encoding.TextUnmarshaler
func FromEnvs(delimiter string) optionFunc {
func FromEnvs(delimiter string) OptionFunc {
return func(c *options) error {
c.env.delimiter = delimiter
c.order = append(c.order, env)
Expand All @@ -319,7 +410,7 @@ func FromEnvs(delimiter string) optionFunc {
//
// WithCliTransform(transformFunc)
// results searching for env variables that are all upper case
func WithEnvTransform(t Transform) optionFunc {
func WithEnvTransform(t Transform) OptionFunc {
return func(c *options) error {
if t == nil {
logger.Warn("WithEnvTransform was used, but transform was nil")
Expand Down Expand Up @@ -348,7 +439,7 @@ func WithEnvTransform(t Transform) optionFunc {
//
// Configuring from Cli cannot be as comprehensive for complex types (like structures) as using the configuration file.
// To unmarshal very complex structs, the struct must implement encoding.TextUnmarshaler and encoding.TextMarshaler
func FromCli(delimiter string) optionFunc {
func FromCli(delimiter string) OptionFunc {
return func(c *options) error {
c.cli.delimiter = delimiter
c.order = append(c.order, cli)
Expand All @@ -367,7 +458,7 @@ func FromCli(delimiter string) optionFunc {
//
// WithCliTransform(transformFunc)
// results in cli flag names that are all upper case
func WithCliTransform(t Transform) optionFunc {
func WithCliTransform(t Transform) OptionFunc {
return func(c *options) error {
if t == nil {
logger.Warn("WithCliTranform was used, but transform was nil")
Expand Down
2 changes: 1 addition & 1 deletion entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestConfigBasic(t *testing.T) {
"-complex_array", "text1,text2,text3", // Example for ComplexArray (implementsTextUnmarshaler)
}

_, _, err := Config[testStruct](Defaults("testdata/test.json", false))
_, _, err := Config[testStruct](Defaults("testdata/test.json"))
if err != nil {
t.Fatal(err)
}
Expand Down

0 comments on commit 34d4cce

Please sign in to comment.