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