Initial commit

This commit is contained in:
akp 2023-08-21 16:47:32 +01:00
commit 1c57bc080a
No known key found for this signature in database
GPG key ID: CF8D58F3DEB20755
6 changed files with 224 additions and 0 deletions

14
LICENSE Normal file
View 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
View file

@ -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.

113
cfger.go Normal file
View 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
View 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
View 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
View 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)
}