Initial commit
This commit is contained in:
commit
1c57bc080a
6 changed files with 224 additions and 0 deletions
14
LICENSE
Normal file
14
LICENSE
Normal file
|
@ -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.
|
61
README.md
Normal file
61
README.md
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# cfger
|
||||||
|
|
||||||
|
*A basic configuration loading system*
|
||||||
|
---
|
||||||
|
|
||||||
|
[](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.
|
113
cfger.go
Normal file
113
cfger.go
Normal file
|
@ -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
|
||||||
|
}
|
5
go.mod
Normal file
5
go.mod
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module git.tdpain.net/pkg/cfger
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1
|
4
go.sum
Normal file
4
go.sum
Normal file
|
@ -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=
|
27
types.go
Normal file
27
types.go
Normal file
|
@ -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)
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue