From 1c57bc080a79bc0947dcf4c6cb98c998cd62be97 Mon Sep 17 00:00:00 2001 From: AKP Date: Mon, 21 Aug 2023 16:47:32 +0100 Subject: [PATCH] Initial commit --- LICENSE | 14 +++++++ README.md | 61 +++++++++++++++++++++++++++++ cfger.go | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 5 +++ go.sum | 4 ++ types.go | 27 +++++++++++++ 6 files changed, 224 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cfger.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 types.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..382e17f --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +BSD Zero Clause License + +Copyright (c) 2023 codemicro + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e5d64e --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# cfger + +*A basic configuration loading system* +--- + +[![Go Reference](https://pkg.go.dev/badge/git.tdpain.net/pkg/cfger.svg)](https://pkg.go.dev/git.tdpain.net/pkg/cfger) + +## Install + +``` +go get git.tdpain.net/pkg/cfger +``` + +## Example usage + +```go +package config + +import ( + "git.tdpain.net/pkg/cfger" +) + +type HTTP struct { + Host string + Port int +} + +type Database struct { + DSN string +} + +type Config struct { + Debug bool + HTTP *HTTP + Database *Database +} + +func Load() (*Config, error) { + cl := cfger.New() + if err := cl.Load("config.yml"); err != nil { + return nil, err + } + + conf := &Config{ + Debug: cl.Required("debug").AsBool(), + HTTP: &HTTP{ + Host: cl.WithDefault("http.host", "127.0.0.1").AsString(), + Port: cl.WithDefault("http.port", 8080).AsInt(), + }, + Database: &Database{ + DSN: cl.WithDefault("database.dsn", "website.db").AsString(), + }, + } + + return conf, nil +} +``` + +## License + +This project is licensed under the BSD Zero Clause License. See `./LICENSE` for more information. \ No newline at end of file diff --git a/cfger.go b/cfger.go new file mode 100644 index 0000000..2f0393f --- /dev/null +++ b/cfger.go @@ -0,0 +1,113 @@ +package cfger + +import ( + "errors" + "fmt" + "os" + "regexp" + "strconv" + "strings" + + "gopkg.in/yaml.v3" +) + +// FatalErrorHandler is called with a fatal error before cfger calls os.Exit. +// This is to allow fatal errors to be processed correctly depending on the +// application itself. +var FatalErrorHandler = func(err error) { + _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) +} + +type ConfigLoader struct { + rawConfigFileContents map[string]any + lastKey string +} + +func New() *ConfigLoader { + return &ConfigLoader{} +} + +// Load reads a YAML config file from fname and holds it in memory. +// +// If fname cannot be found, a blank config file will be loaded +// instead. +func (cl *ConfigLoader) Load(fname string) error { + cl.rawConfigFileContents = make(map[string]any) + fcont, err := os.ReadFile(fname) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } else { + return err + } + } + + if err := yaml.Unmarshal(fcont, &cl.rawConfigFileContents); err != nil { + return fmt.Errorf("cfger: unmarshal %s: %w", fname, err) + } + return nil +} + +var indexedPartRegexp = regexp.MustCompile(`(?m)([a-zA-Z]+)(?:\[(\d+)\])?`) + +// Get gets a given key from the currently loaded configuration. +// +// Within a key representation, a dot represents a recursion into a named +// object and square brackets represent an index in a named array. You could access the value "hello" using the key `alpha[0].beta` in the below example. +// +// alpha: +// - beta: "hello" +func (cl *ConfigLoader) Get(key string) OptionalItem { + // httpcore[2].bananas + cl.lastKey = key + + parts := strings.Split(key, ".") + var cursor any = cl.rawConfigFileContents + for _, part := range parts { + components := indexedPartRegexp.FindStringSubmatch(part) + key := components[1] + index, _ := strconv.ParseInt(components[2], 10, 32) + isIndexed := components[2] != "" + + item, found := cursor.(map[string]any)[key] + if !found { + return OptionalItem{nil, false} + } + + if isIndexed { + arr, conversionOk := item.([]any) + if !conversionOk { + panic(fmt.Errorf("cfger: attempted to index non-indexable config item %s", key)) + } + cursor = arr[index] + } else { + cursor = item + } + } + return OptionalItem{cursor, true} +} + +// Required gets a given key from the currently loaded configuration and raises +// a fatal error if it cannot be found. +// +// See documentation for Get for key format. +func (cl *ConfigLoader) Required(key string) OptionalItem { + opt := cl.Get(key) + if !opt.found { + FatalErrorHandler(fmt.Errorf("Required key %s not found in config file", key)) + os.Exit(1) + } + return opt +} + +// Required gets a given key from the currently loaded configuration and +// returns a default value if it cannot be found. +// +// See documentation for Get for key format. +func (cl *ConfigLoader) WithDefault(key string, defaultValue any) OptionalItem { + opt := cl.Get(key) + if !opt.found { + return OptionalItem{item: defaultValue, found: true} + } + return opt +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a0a61b1 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.tdpain.net/pkg/cfger + +go 1.18 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/types.go b/types.go new file mode 100644 index 0000000..85c1001 --- /dev/null +++ b/types.go @@ -0,0 +1,27 @@ +package cfger + +type OptionalItem struct { + item any + found bool +} + +func (x OptionalItem) AsInt() int { + if !x.found { + return 0 + } + return x.item.(int) +} + +func (x OptionalItem) AsString() string { + if !x.found { + return "" + } + return x.item.(string) +} + +func (x OptionalItem) AsBool() bool { + if !x.found { + return false + } + return x.item.(bool) +}