v1 #1
210
README.md
210
README.md
@@ -1,180 +1,56 @@
|
|||||||
# module conf
|
# package conf
|
||||||
|
|
||||||
**go get code.uint32.ru/tiny/conf** to download.
|
**go get code.uint32.ru/tiny/conf** to download.
|
||||||
|
|
||||||
**import "code.uint32.ru/tiny/conf"** to use in your code.
|
**import "code.uint32.ru/tiny/conf"** to use in your code.
|
||||||
|
conf implements a very simple config parser with permissive input syntax.
|
||||||
|
|
||||||
Module conf implements a very simple config parser with two types of
|
It parses input file or io.Reader looking for key value pairs separated by equal
|
||||||
values: key value pairs and single word options. Each of these must be
|
sign into conf.Map which is a map[string]conf.Value under the hood.
|
||||||
put on separate line in a file like this:
|
|
||||||
```
|
|
||||||
key1 = value
|
|
||||||
key2 = value1, value2, value3
|
|
||||||
option1
|
|
||||||
option2
|
|
||||||
```
|
|
||||||
Values can also be read from any io.Reader or io.ReadCloser. Note that
|
|
||||||
values in key value pairs mey either be separated with spaces or not.
|
|
||||||
|
|
||||||
Typical use case would look like this:
|
Input syntax rules are as follows:
|
||||||
|
|
||||||
|
1. Lines starting with "#" are considered to be comments and are discarded. If a line starts with any number of whitespace characters followed by "#" it is also discarded as comment.
|
||||||
|
|
||||||
|
2. If a line is not a comment it must follow the "key = value" pattern. Any of the following is valid: "key=value", "key= value", "key =value". Leading and trailing whitespace characters are trimmed before parsing line. Whitespaces surrounding
|
||||||
|
key and value strings are also trimmed.
|
||||||
|
|
||||||
|
3. Keys in the input must be unique, ohterwise Read / ReadFile methods will return an error.
|
||||||
|
|
||||||
|
Map object has Find, Get and GetDefault methods used to retrieve Value from Map by corresponding key.
|
||||||
|
|
||||||
|
Value is essentially a string which can be converted into several types using Value's methods:
|
||||||
|
|
||||||
|
**String()** returns Value as string
|
||||||
|
|
||||||
|
**StringSlice()** interprets Value as a comma-separated list (whitespace around elements is trimmed)
|
||||||
|
|
||||||
|
**Int()** converts Value to int
|
||||||
|
|
||||||
|
**IntSlice()** tries to convert Value to []int
|
||||||
|
|
||||||
|
**Float64()** converts Value to float64
|
||||||
|
|
||||||
|
**Float64Slice()** tries to convert to []float64
|
||||||
|
|
||||||
|
**Map()** tries to interpret Value as comma-separated list of key-value pairs like in "key1: value1, key2: value2, key3: value3"
|
||||||
|
|
||||||
|
**URL()** calls url.Parse to convert Value to *url.URL
|
||||||
|
|
||||||
|
See documentation for description of all available methods.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
```go
|
```go
|
||||||
config, err := conf.ParseFile("filename")
|
m, _ := conf.ReadFile("myfile")
|
||||||
|
retries, err := m.Get("retries").Int()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Means we failed to read from file
|
// handle err
|
||||||
// config variable is now nil and unusable
|
|
||||||
}
|
}
|
||||||
value, err := config.Find("mykey")
|
|
||||||
|
addr, err := m.Get("addr").URL()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Means that key has not been found
|
// handle err
|
||||||
}
|
|
||||||
// value now holds conf.Setting type which can be accessed directly.
|
|
||||||
fmt.Println(value.Value)
|
|
||||||
n, err := value.Float64() // tries to parse float from Setting.Value field.
|
|
||||||
// if err is nil then n holds float64.
|
|
||||||
```
|
|
||||||
Shorter syntax is also available with Get() method which drops errors if
|
|
||||||
key was not found and simply returns Setting with empty Value field:
|
|
||||||
```go
|
|
||||||
listnumbers, err := config.Get("numbers").IntSlice()
|
|
||||||
// listnumbers holds slice of ints. If err is nil all of found values
|
|
||||||
// have converted successfully.
|
|
||||||
```
|
|
||||||
Note that we'd still like to check for errors in above example even if
|
|
||||||
we're sure the key exists. This way we'll configrm that all values have been converted.
|
|
||||||
|
|
||||||
There is also a GetDefault() method:
|
|
||||||
```go
|
|
||||||
def := config.GetDefault("non-existant-key", "myvalue")
|
|
||||||
fmt.Println(def.Value) // "myvalue"
|
|
||||||
```
|
|
||||||
To check whether single-word options were found use:
|
|
||||||
```go
|
|
||||||
if config.HasOption("wordoption") {
|
|
||||||
// do something
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
See description of module's types and methods which are
|
|
||||||
quite self-explanatory.
|
|
||||||
|
|
||||||
See also **[https://pkg.go.dev/code.uint32.ru/tiny/conf](https://pkg.go.dev/code.uint32.ru/tiny/conf)** for a complete description of module's
|
See example folder.
|
||||||
functions.
|
|
||||||
|
|
||||||
Below is listing of a working example of a program parsing config (also found **example/main.go** in the repository).
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"code.uint32.ru/tiny/conf"
|
|
||||||
)
|
|
||||||
|
|
||||||
var testConf = []byte(`
|
|
||||||
# commented
|
|
||||||
port=10000
|
|
||||||
servers = 10.0.0.1, 10.0.0.2, 10.0.0.3
|
|
||||||
bool0=0
|
|
||||||
booltrue = true
|
|
||||||
distance=13.42
|
|
||||||
iamsure = hopefully you're not mistaken
|
|
||||||
float = 13.1984
|
|
||||||
color`)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
r := bytes.NewReader(testConf) // creating io.Reader from []byte()
|
|
||||||
config := conf.ParseReader(r) // we could call conf.ParseFile("filename") here
|
|
||||||
|
|
||||||
// First of all we can access parsed values directly:
|
|
||||||
for key, value := range config.Settings {
|
|
||||||
fmt.Println(key, value)
|
|
||||||
}
|
|
||||||
for opt := range config.Options {
|
|
||||||
fmt.Println(opt)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Find( key string) returns instance of Setting and an
|
|
||||||
// error if key was not found
|
|
||||||
//
|
|
||||||
// type Setting struct {
|
|
||||||
// Value string // value if found
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// You can access Setting's Value field (type string) directly.
|
|
||||||
port, err := config.Find("port")
|
|
||||||
fmt.Printf("variable port has type: %T, port.Value == %v, type of port.Value is: %T, error returned: %v\n", port, port.Value, port.Value, err)
|
|
||||||
|
|
||||||
// We can cast Setting.Value to a desired type including int, float64,
|
|
||||||
// bool and string. Method will return an error if Setting Value field
|
|
||||||
// can not be interpreted as desired type.
|
|
||||||
n, err := port.Int()
|
|
||||||
fmt.Printf("n has value: %v, type: %T, err: %v\n", n, n, err)
|
|
||||||
|
|
||||||
// Another syntax for getting Setting instance is to use Get() method
|
|
||||||
// which never returns errors. Get will return Setting with empty string
|
|
||||||
// in Value filed if requested key was now found.
|
|
||||||
d := config.Get("distance")
|
|
||||||
distance, err := d.Float64()
|
|
||||||
fmt.Printf("var distance has value: %v, type: %T, error value: %v\n", distance, distance, err)
|
|
||||||
|
|
||||||
// Get() syntax can be is slightly shorter if we're "sure" that key exists in
|
|
||||||
// the config.
|
|
||||||
sure := config.Get("iamsure").String() // String method never returns errors
|
|
||||||
// or simply sure := config.Get("iamsure").Value:
|
|
||||||
fmt.Println(sure)
|
|
||||||
|
|
||||||
// or like this:
|
|
||||||
fl, _ := config.Get("float").Float64() // we're dropping error here. Bad if value fails to convert.
|
|
||||||
fmt.Println("fl, _ := config.Get(\"float\").Float64() Value of var fl is:", fl)
|
|
||||||
|
|
||||||
// We can access comma-separated values of key like this:
|
|
||||||
servers := config.Get("servers").StringSlice()
|
|
||||||
fmt.Println("Found servers:")
|
|
||||||
for i, s := range servers {
|
|
||||||
fmt.Println(i+1, "\t", s)
|
|
||||||
}
|
|
||||||
// There is also a GetDefault() method which sets output Setting value
|
|
||||||
// to requested default if key was not found.
|
|
||||||
def := config.GetDefault("non-existant-key", "myvalue")
|
|
||||||
fmt.Println(def.Value) // "myvalue"
|
|
||||||
|
|
||||||
// You can use HasOption method to find whether single-word options were
|
|
||||||
// present in the the config
|
|
||||||
if config.HasOption("color") {
|
|
||||||
fmt.Println("Hooray, we've found option \"color\"!")
|
|
||||||
// do something useful
|
|
||||||
}
|
|
||||||
|
|
||||||
// Below code finds two keys with bool values in the Config
|
|
||||||
// and outputs those.
|
|
||||||
var t, f bool
|
|
||||||
t, _ = config.Get("booltrue").Bool()
|
|
||||||
f, _ = config.Get("bool0").Bool()
|
|
||||||
fmt.Printf("t's type is: %T, value: %v, f's type is: %T, value: %v\n\n", t, t, f, f)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Here is the full output of the above code:
|
|
||||||
```
|
|
||||||
distance 13.42
|
|
||||||
iamsure hopefully you're not mistaken
|
|
||||||
float 13.1984
|
|
||||||
port 10000
|
|
||||||
servers 10.0.0.1, 10.0.0.2, 10.0.0.3
|
|
||||||
bool0 0
|
|
||||||
booltrue true
|
|
||||||
color
|
|
||||||
|
|
||||||
variable port has type: conf.Setting, port.Value == 10000, type of port.Value is: string, error returned: <nil>
|
|
||||||
n has value: 10000, type: int, err: <nil>
|
|
||||||
var distance has value: 13.42, type: float64, error value: <nil>
|
|
||||||
hopefully you're not mistaken
|
|
||||||
fl, _ := config.Get("float").Float64() Value of var fl is: 13.1984
|
|
||||||
Found servers:
|
|
||||||
1 10.0.0.1
|
|
||||||
2 10.0.0.2
|
|
||||||
3 10.0.0.3
|
|
||||||
Hooray, we've found option "color"!
|
|
||||||
t's type is: bool, value: true, f's type is: bool, value: false
|
|
||||||
|
|
||||||
```
|
|
210
conf.go
210
conf.go
@@ -1,210 +0,0 @@
|
|||||||
// module conf
|
|
||||||
//
|
|
||||||
// import "code.uint32.ru/tiny/conf"
|
|
||||||
//
|
|
||||||
// Module conf implements a very simple config parser with two types of
|
|
||||||
// values: key value pairs and single word options. Each of these must be
|
|
||||||
// put on separate line in a file like this:
|
|
||||||
//
|
|
||||||
// key1 = value
|
|
||||||
// key2 = value1, value2, value3
|
|
||||||
// key3=v1,v2,v3
|
|
||||||
// option1
|
|
||||||
// option2
|
|
||||||
//
|
|
||||||
// Values can also be read from any io.Reader or io.ReadCloser. Note that
|
|
||||||
// values in key value pairs mey either be separated with spaces or not.
|
|
||||||
// Typical use case would look like this:
|
|
||||||
//
|
|
||||||
// config, err := conf.ParseFile("filename")
|
|
||||||
// if err != nil {
|
|
||||||
// // Means we failed to read from file
|
|
||||||
// // config variable is now nil and unusable
|
|
||||||
// }
|
|
||||||
// value, err := config.Find("mykey")
|
|
||||||
// if err != nil {
|
|
||||||
// // Means that key has not been found
|
|
||||||
// }
|
|
||||||
// // value now holds conf.Setting.
|
|
||||||
// n, err := value.Float64() // tries to parse float from Setting.Value field.
|
|
||||||
// //if err is nil then n holds float64.
|
|
||||||
//
|
|
||||||
// There is also a quicker Get() method which returns no errors
|
|
||||||
// ("i'm feeling lucky" way to lookup values). If it does not find
|
|
||||||
// requested key, the returned Setting has empty string in Value field.
|
|
||||||
//
|
|
||||||
// value2 := config.Get("otherkey")
|
|
||||||
// mybool, err := value2.Bool() // tries to interpret Setting.Value field as bool
|
|
||||||
// // mybool now holds boolean if "otherkey" was found and error returned
|
|
||||||
// // by Bool() method is nil.
|
|
||||||
//
|
|
||||||
// Even shorter syntax would be:
|
|
||||||
//
|
|
||||||
// listnumbers, err := config.Get("numbers").IntSlice()
|
|
||||||
// // Note that we'd still like to check for errors even if
|
|
||||||
// // we're sure the key exists to make sure all values are converted.
|
|
||||||
// // listnumbers holds slice of ints. If err is nil all of found values
|
|
||||||
// // have been converted successfully.
|
|
||||||
//
|
|
||||||
// To check whether single-word options were found use:
|
|
||||||
//
|
|
||||||
// if config.HasOption("wordoption") {
|
|
||||||
// // do something
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// See description of module's other methods which are
|
|
||||||
// quite self-explanatory.
|
|
||||||
package conf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrFormat = errors.New("conf: line does not match \"key = value\" pattern")
|
|
||||||
ErrDuplicateKey = errors.New("conf: duplicate key found")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Conf holds key value pairs.
|
|
||||||
type Conf struct {
|
|
||||||
values []*kv // values store key value pairs ("key = value" in config file)
|
|
||||||
}
|
|
||||||
|
|
||||||
type kv struct {
|
|
||||||
k string
|
|
||||||
v Value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open reads values from file.
|
|
||||||
func Open(filename string) (*Conf, error) {
|
|
||||||
c := new(Conf)
|
|
||||||
if err := c.readFile(filename); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads from r and returns Conf.
|
|
||||||
func Read(r io.Reader) (*Conf, error) {
|
|
||||||
c := new(Conf)
|
|
||||||
if err := c.read(r); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find looks up a the key and returns Value associated with it.
|
|
||||||
// If bool is false then the returned Value would be zero.
|
|
||||||
func (c *Conf) Find(key string) (Value, bool) {
|
|
||||||
return c.get(key, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns a Value. If key was not found the returned Value
|
|
||||||
// will be empty.
|
|
||||||
func (c *Conf) Get(key string) Value {
|
|
||||||
v, _ := c.get(key, "")
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDefault looks up a Value for requested key.
|
|
||||||
// If lookup fails it returns Value set to def.
|
|
||||||
func (c *Conf) GetDefault(key, def string) Value {
|
|
||||||
v, _ := c.get(key, def)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conf) Keys() []string {
|
|
||||||
list := make([]string, 0, len(c.values))
|
|
||||||
|
|
||||||
for i := range c.values {
|
|
||||||
list = append(list, c.values[i].k)
|
|
||||||
}
|
|
||||||
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conf) Read(r io.Reader) error {
|
|
||||||
return c.read(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conf) ReadFile(name string) error {
|
|
||||||
return c.readFile(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conf) get(key string, def string) (Value, bool) {
|
|
||||||
var (
|
|
||||||
v Value
|
|
||||||
found bool
|
|
||||||
)
|
|
||||||
for i := range c.values {
|
|
||||||
if c.values[i].k == key {
|
|
||||||
v = c.values[i].v
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if def != "" && !found {
|
|
||||||
v = Value(def)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v, found
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conf) readFile(name string) error {
|
|
||||||
file, err := os.Open(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.read(file); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return file.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conf) read(r io.Reader) error {
|
|
||||||
uniq := make(map[string]struct{})
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(r)
|
|
||||||
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
|
|
||||||
line = trim(stripComment(line))
|
|
||||||
if line == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
key, value, ok := toKV(line, separatorKV)
|
|
||||||
if !ok {
|
|
||||||
return ErrFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := uniq[key]; ok {
|
|
||||||
return fmt.Errorf("%w: %s", ErrDuplicateKey, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
uniq[key] = struct{}{}
|
|
||||||
|
|
||||||
kv := &kv{
|
|
||||||
k: key,
|
|
||||||
v: Value(value),
|
|
||||||
}
|
|
||||||
|
|
||||||
c.values = append(c.values, kv)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
114
example/main.go
114
example/main.go
@@ -11,64 +11,78 @@ var testConf = []byte(`
|
|||||||
# commented
|
# commented
|
||||||
port=10000
|
port=10000
|
||||||
servers = 10.0.0.1, 10.0.0.2, 10.0.0.3
|
servers = 10.0.0.1, 10.0.0.2, 10.0.0.3
|
||||||
bool0=0
|
some_ints = 1, 2, 3, 4
|
||||||
booltrue = true
|
map = a: 1, b: 2, c:3
|
||||||
distance=13.42
|
`)
|
||||||
iamsure = hopefully you're not mistaken
|
|
||||||
float = 13.1984`)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
r := bytes.NewReader(testConf) // creating io.Reader from []byte()
|
r := bytes.NewReader(testConf) // creating io.Reader from []byte()
|
||||||
config, _ := conf.Read(r) // we could call conf.ParseFile("filename") here
|
config, _ := conf.Read(r) // we could call conf.Open("filename") here
|
||||||
|
|
||||||
// Find( key string) returns instance of Setting and an
|
// main methods if conf.Map are Find, Get, and GetDefault
|
||||||
// error if key was not found
|
// Find returns conf.Value and a bool indicating whether value has been found
|
||||||
//
|
_, ok := config.Find("port")
|
||||||
// type Setting struct {
|
if !ok {
|
||||||
// Value string // value if found
|
// key "port" was not found
|
||||||
// }
|
panic("not found")
|
||||||
//
|
}
|
||||||
// You can access Setting's Value field (type string) directly.
|
|
||||||
port, ok := config.Find("port")
|
// GetDefault will return Value set to provided default
|
||||||
fmt.Printf("variable port has type: %T, port.Value == %v, type of port.Value is: %T, error returned: %v\n", port, port.Value, port.Value, ok)
|
// if value is not in the map. In this example the returned
|
||||||
|
// value wiill be Value("vyvalue").
|
||||||
|
def := config.GetDefault("non-existant-key", "myvalue")
|
||||||
|
|
||||||
|
fmt.Println(def.String()) // "myvalue"
|
||||||
|
|
||||||
|
// Get will return empty conf.Value if it was not found.
|
||||||
|
// get values from line "servers = 10.0.0.1, 10.0.0.2, 10.0.0.3".
|
||||||
|
servers := config.Get("servers")
|
||||||
|
|
||||||
|
// the returned Value has many methods to convert the underlying string
|
||||||
|
for i, s := range servers.StringSlice() {
|
||||||
|
fmt.Println(i+1, "\t", s) // 10.0.0.1 10.0.0.2 10.0.0.3
|
||||||
|
}
|
||||||
|
|
||||||
// We can cast Setting.Value to a desired type including int, float64,
|
// We can cast Setting.Value to a desired type including int, float64,
|
||||||
// bool and string. Method will return an error if Setting Value field
|
// bool and string. Method will return an error if Setting Value field
|
||||||
// can not be interpreted as desired type.
|
// can not be interpreted as desired type.
|
||||||
n, err := port.Int()
|
n, err := config.Get("port").Int()
|
||||||
fmt.Printf("n has value: %v, type: %T, err: %v\n", n, n, err)
|
if err != nil {
|
||||||
|
// n is zero value for int and we failed to convert
|
||||||
// Another syntax for getting Setting instance is to use Get() method
|
// value from our file to int.
|
||||||
// which never returns errors. Get will return Setting with empty string
|
panic(err)
|
||||||
// in Value filed if requested key was now found.
|
|
||||||
d := config.Get("distance")
|
|
||||||
distance, err := d.Float64()
|
|
||||||
fmt.Printf("var distance has value: %v, type: %T, error value: %v\n", distance, distance, err)
|
|
||||||
|
|
||||||
// Get() syntax can be is slightly shorter if we're "sure" that key exists in
|
|
||||||
// the config.
|
|
||||||
sure := config.Get("iamsure").String() // String method never returns errors
|
|
||||||
// or simply sure := config.Get("iamsure").Value:
|
|
||||||
fmt.Println(sure)
|
|
||||||
|
|
||||||
// or like this:
|
|
||||||
fl, _ := config.Get("float").Float64() // we're dropping error here. Bad if value fails to convert.
|
|
||||||
fmt.Println("fl, _ := config.Get(\"float\").Float64() Value of var fl is:", fl)
|
|
||||||
|
|
||||||
// We can access comma-separated values of key like this:
|
|
||||||
servers := config.Get("servers").StringSlice()
|
|
||||||
fmt.Println("Found servers:")
|
|
||||||
for i, s := range servers {
|
|
||||||
fmt.Println(i+1, "\t", s)
|
|
||||||
}
|
}
|
||||||
// There is also a GetDefault() method
|
fmt.Println(n) // 10000
|
||||||
def := config.GetDefault("non-existant-key", "myvalue")
|
|
||||||
fmt.Println(def.Value) // "myvalue"
|
|
||||||
|
|
||||||
// Below code finds two keys with bool values in the Config
|
// The value methods are as follows:
|
||||||
// and outputs those.
|
v := config.Get("some_ints") // the config line is "some_ints = 1, 2, 3, 4"
|
||||||
var t, f bool
|
|
||||||
t, _ = config.Get("booltrue").Bool()
|
// String returns string as is (only trimmed from whitespace)
|
||||||
f, _ = config.Get("bool0").Bool()
|
s := v.String()
|
||||||
fmt.Printf("t's type is: %T, value: %v, f's type is: %T, value: %v\n\n", t, t, f, f)
|
fmt.Printf("%T, %v\n", s, s) // string, 1, 2, 3, 4
|
||||||
|
|
||||||
|
// StringSlice tries to interpret value as comma-separated list, trims each element from whitespace
|
||||||
|
ss := v.StringSlice()
|
||||||
|
fmt.Printf("%T, %v\n", ss, ss) // []string, [1 2 3 4]
|
||||||
|
|
||||||
|
// IntSlice parses value in the same way as StringSlice then tries to convert each element
|
||||||
|
// to int. Failure to convert any element is an error.
|
||||||
|
is, err := v.IntSlice()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%T, %v\n", is, is) // []int, [1 2 3 4]
|
||||||
|
|
||||||
|
// there are similar methods for bool and float64 types.
|
||||||
|
|
||||||
|
// Map method of Value tries to interpret value as mapping where
|
||||||
|
// keys are sepearated from values with ":".
|
||||||
|
// This example parses line for the example config
|
||||||
|
// "map = a: 1, b: 2, c: 3"
|
||||||
|
m, _ := config.Get("map").Map() // return conf.Map and error if any
|
||||||
|
fmt.Printf("%T, %v\n", m, m) // conf.Map, map[a:1 b:2 c:3]
|
||||||
|
|
||||||
|
// key "a" can be interpreted as int
|
||||||
|
x, _ := m.Get("c").Int()
|
||||||
|
fmt.Println(x) // prints 3
|
||||||
}
|
}
|
||||||
|
183
map.go
Normal file
183
map.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
// conf implements a very simple config parser with permissive input syntax.
|
||||||
|
//
|
||||||
|
// It parses input file or io.Reader looking for key value pairs separated by equal
|
||||||
|
// sign into conf.Map which is a map[string]conf.Value under the hood.
|
||||||
|
//
|
||||||
|
// Input syntax rules are as follows:
|
||||||
|
//
|
||||||
|
// 1. Lines starting with "#" are considered to be comments and are discarded. If a line starts with any number of whitespace characters followed by "#" it is also discarded as comment.
|
||||||
|
//
|
||||||
|
// 2. If a line is not a comment it must follow the "key = value" pattern. Any of the following is valid:
|
||||||
|
//
|
||||||
|
// key=value
|
||||||
|
// key= value
|
||||||
|
// key =value
|
||||||
|
//
|
||||||
|
// Leading and trailing whitespace characters are trimmed before parsing line. Whitespaces surrounding
|
||||||
|
// key and value strings are also trimmed.
|
||||||
|
//
|
||||||
|
// 3. Keys in the input must be unique.
|
||||||
|
//
|
||||||
|
// Map object has Find, Get and GetDefault mathod used to retrieve Value from
|
||||||
|
// Map by corresponding key.
|
||||||
|
//
|
||||||
|
// Value is essentially a string which can be converted into several types using
|
||||||
|
// Value's methods:
|
||||||
|
//
|
||||||
|
// String() returns Value as string
|
||||||
|
// StringSlice() interprets Value as a comma-separated list (whitespace around elements is trimmed)
|
||||||
|
// Int() converts Value to int
|
||||||
|
// IntSlice() tries to convert Value to []int
|
||||||
|
// Float64() converts Value to float64
|
||||||
|
// Float64Slice() tries to convert to []float64
|
||||||
|
// Map() tries to interpret Value as comma-separated list of key-value pairs like in "key1: value1, key2: value2, key3: value3"
|
||||||
|
// URL() calls url.Parse to convert Value to *url.URL
|
||||||
|
//
|
||||||
|
// See documentation for description of all available methods.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// m, _ := conf.ReadFile("myfile")
|
||||||
|
// retries, err := m.Get("retries").Int()
|
||||||
|
// if err != nil {
|
||||||
|
// // handle err
|
||||||
|
// }
|
||||||
|
// addr, err := m.Get("addr").URL()
|
||||||
|
// if err != nil {
|
||||||
|
// // handle err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// See example folder.
|
||||||
|
package conf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrFormat = errors.New("conf: line does not match \"key = value\" pattern")
|
||||||
|
ErrDuplicateKey = errors.New("conf: duplicate key found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Map holds key value pairs.
|
||||||
|
type Map map[string]Value
|
||||||
|
|
||||||
|
// Open reads values from file.
|
||||||
|
func Open(filename string) (Map, error) {
|
||||||
|
m := make(Map)
|
||||||
|
if err := m.readFile(filename); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads from r and returns Conf.
|
||||||
|
func Read(r io.Reader) (Map, error) {
|
||||||
|
m := make(Map)
|
||||||
|
if err := m.read(r); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Map) Read(r io.Reader) error {
|
||||||
|
return m.read(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Map) ReadFile(name string) error {
|
||||||
|
return m.readFile(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find looks up a the key and returns Value associated with it.
|
||||||
|
// If bool is false then the returned Value would be zero.
|
||||||
|
func (m Map) Find(key string) (Value, bool) {
|
||||||
|
return m.get(key, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a Value. If key was not found the returned Value
|
||||||
|
// will be empty.
|
||||||
|
func (m Map) Get(key string) Value {
|
||||||
|
v, _ := m.get(key, "")
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefault looks up a Value for requested key.
|
||||||
|
// If lookup fails it returns Value set to def.
|
||||||
|
func (m Map) GetDefault(key, def string) Value {
|
||||||
|
v, _ := m.get(key, def)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Map) Keys() []string {
|
||||||
|
list := make([]string, 0, len(m))
|
||||||
|
|
||||||
|
for k := range m {
|
||||||
|
list = append(list, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.Sort(list)
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Map) get(key string, def string) (Value, bool) {
|
||||||
|
v, ok := m[key]
|
||||||
|
if def != "" && !ok {
|
||||||
|
v = Value(def)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Map) readFile(name string) error {
|
||||||
|
file, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.read(file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Map) read(r io.Reader) error {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
|
||||||
|
lineN := 0
|
||||||
|
for scanner.Scan() {
|
||||||
|
lineN++
|
||||||
|
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
line = stripComment(trim(line))
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key, value, ok := toKV(line, separatorKV)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("line: %d, err: %w", lineN, ErrFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := m[key]; ok {
|
||||||
|
return fmt.Errorf("line: %d, err: %w %s", lineN, ErrDuplicateKey, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
m[key] = Value(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@@ -14,79 +14,57 @@ func TestRead(t *testing.T) {
|
|||||||
tc := []struct {
|
tc := []struct {
|
||||||
name string
|
name string
|
||||||
in []byte
|
in []byte
|
||||||
res *Conf
|
want Map
|
||||||
err error
|
err error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
in: []byte{},
|
in: []byte{},
|
||||||
res: &Conf{
|
want: Map{},
|
||||||
values: nil, // no reads, so the slice has not been initialized
|
|
||||||
},
|
|
||||||
err: nil, // if the reader just returned io.EOF, which isn't an error for us
|
err: nil, // if the reader just returned io.EOF, which isn't an error for us
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single key with spaces",
|
name: "single key with spaces",
|
||||||
in: []byte("key = value"),
|
in: []byte("key = value"),
|
||||||
res: &Conf{
|
want: Map{
|
||||||
values: []*kv{
|
"key": Value("value"),
|
||||||
{
|
|
||||||
k: "key",
|
|
||||||
v: Value("value"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
err: nil,
|
err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single key no spaces",
|
name: "single key no spaces",
|
||||||
in: []byte("key=value"),
|
in: []byte("key=value"),
|
||||||
res: &Conf{
|
want: Map{
|
||||||
values: []*kv{
|
"key": Value("value"),
|
||||||
{
|
|
||||||
k: "key",
|
|
||||||
v: Value("value"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
err: nil,
|
err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single key with newline",
|
name: "single key with newline",
|
||||||
in: []byte("key=value\n"),
|
in: []byte("key=value\n"),
|
||||||
res: &Conf{
|
want: Map{
|
||||||
values: []*kv{
|
"key": Value("value"),
|
||||||
{
|
|
||||||
k: "key",
|
|
||||||
v: Value("value"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
err: nil,
|
err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single key with comment and newline",
|
name: "single key with comment and newline",
|
||||||
in: []byte("key =value # this is a comment\n"),
|
in: []byte("key =value # something else \n"),
|
||||||
res: &Conf{
|
want: Map{
|
||||||
values: []*kv{
|
"key": Value("value # something else"),
|
||||||
{
|
|
||||||
k: "key",
|
|
||||||
v: Value("value"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
err: nil,
|
err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "incorrect k/v separator",
|
name: "incorrect k/v separator",
|
||||||
in: []byte("key ! value \n"),
|
in: []byte("key ! value \n"),
|
||||||
res: nil,
|
want: nil,
|
||||||
err: ErrFormat,
|
err: ErrFormat,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "duplicate key",
|
name: "duplicate key",
|
||||||
in: []byte("key=value1\nkey=value2\n"),
|
in: []byte("key=value1\nkey=value2\n"),
|
||||||
res: nil,
|
want: nil,
|
||||||
err: ErrDuplicateKey,
|
err: ErrDuplicateKey,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -95,13 +73,13 @@ func TestRead(t *testing.T) {
|
|||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
r := bytes.NewReader(c.in)
|
r := bytes.NewReader(c.in)
|
||||||
|
|
||||||
conf, err := Read(r)
|
have, err := Read(r)
|
||||||
if !errors.Is(err, c.err) {
|
if !errors.Is(err, c.err) {
|
||||||
t.Fatalf("want error: %v have: %v", c.err, err)
|
t.Fatalf("want error: %v have: %v", c.err, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(c.res, conf) {
|
if !reflect.DeepEqual(c.want, have) {
|
||||||
t.Fatalf("want: %+v, have: %+v", c.res, conf)
|
t.Fatalf("want: %+v, have: %+v", c.want, have)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -131,31 +109,31 @@ func TestMainMethods(t *testing.T) {
|
|||||||
b := []byte(fmt.Sprintf("%s = %s", key, value))
|
b := []byte(fmt.Sprintf("%s = %s", key, value))
|
||||||
r := bytes.NewReader(b)
|
r := bytes.NewReader(b)
|
||||||
|
|
||||||
c := new(Conf)
|
m := make(Map)
|
||||||
if err := c.Read(r); err != nil {
|
if err := m.Read(r); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Get(key).String() != value {
|
if m.Get(key).String() != value {
|
||||||
t.Error("Get fails for existing key")
|
t.Error("Get fails for existing key")
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, ok := c.Find("key"); !ok || v.String() != value {
|
if v, ok := m.Find("key"); !ok || v.String() != value {
|
||||||
t.Error("Find fails for existing key")
|
t.Error("Find fails for existing key")
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.GetDefault(key, "none").String() != value {
|
if m.GetDefault(key, "none").String() != value {
|
||||||
t.Error("GetDafault fails on existing value")
|
t.Error("GetDafault fails on existing value")
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.GetDefault("unknown", value).String() != value {
|
if m.GetDefault("unknown", value).String() != value {
|
||||||
t.Error("GetDafault fails to supply default")
|
t.Error("GetDafault fails to supply default")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadReadFile(t *testing.T) {
|
func TestReadReadFile(t *testing.T) {
|
||||||
conf := new(Conf)
|
m := make(Map)
|
||||||
if err := conf.ReadFile("./testdata/test.conf"); err != nil {
|
if err := m.ReadFile("./testdata/test.conf"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,31 +143,20 @@ func TestReadReadFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
conf = new(Conf)
|
m = make(Map)
|
||||||
if err := conf.Read(f); err != nil {
|
if err := m.Read(f); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKeys(t *testing.T) {
|
func TestKeys(t *testing.T) {
|
||||||
c := Conf{
|
m := Map{
|
||||||
values: []*kv{
|
"1": Value(""),
|
||||||
{
|
"2": Value(""),
|
||||||
k: "1",
|
"3": Value(""),
|
||||||
v: Value(""),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
k: "2",
|
|
||||||
v: Value(""),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
k: "3",
|
|
||||||
v: Value(""),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !slices.Equal([]string{"1", "2", "3"}, c.Keys()) {
|
if !slices.Equal([]string{"1", "2", "3"}, m.Keys()) {
|
||||||
t.Fatal("Keys method returns incorrect values")
|
t.Fatal("Keys method returns incorrect values")
|
||||||
}
|
}
|
||||||
}
|
}
|
2
testdata/test.conf
vendored
2
testdata/test.conf
vendored
@@ -2,4 +2,4 @@
|
|||||||
key1 = value1
|
key1 = value1
|
||||||
key2=value2
|
key2=value2
|
||||||
key3 = value3
|
key3 = value3
|
||||||
key4 = value4 # comment
|
key4 = value4
|
5
util.go
5
util.go
@@ -23,9 +23,8 @@ func trim(s string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stripComment(s string) string {
|
func stripComment(s string) string {
|
||||||
idx := strings.Index(s, separatorComment)
|
if strings.HasPrefix(s, separatorComment) {
|
||||||
if idx > -1 {
|
return ""
|
||||||
s = s[:idx]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
|
12
util_test.go
12
util_test.go
@@ -78,18 +78,18 @@ func Test_stripComment(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "comment",
|
name: "comment",
|
||||||
in: "text#some comment",
|
in: "text#some",
|
||||||
want: "text",
|
want: "text#some",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "comment",
|
name: "comment",
|
||||||
in: "text #some comment",
|
in: "text #some",
|
||||||
want: "text ",
|
want: "text #some",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "comment",
|
name: "comment",
|
||||||
in: " text #some comment",
|
in: " text #some ",
|
||||||
want: " text ",
|
want: " text #some ",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
32
value.go
32
value.go
@@ -2,7 +2,7 @@ package conf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -18,20 +18,23 @@ const (
|
|||||||
|
|
||||||
type Value string
|
type Value string
|
||||||
|
|
||||||
func (v Value) Map() (map[string]Value, error) {
|
// Map tries to interpret value as "key: value" pairs
|
||||||
|
// separated by comma like in the following string:
|
||||||
|
// "key1: value1, key2: value2, key3: value3".
|
||||||
|
func (v Value) Map() (Map, error) {
|
||||||
split := tidySplit(v, separatorSlice)
|
split := tidySplit(v, separatorSlice)
|
||||||
|
|
||||||
m := make(map[string]Value, len(split))
|
m := make(Map, len(split))
|
||||||
|
|
||||||
for i := range split {
|
for i := range split {
|
||||||
k, v, ok := toKV(split[i], separatorMap)
|
k, v, ok := toKV(split[i], separatorMap)
|
||||||
if !ok {
|
if !ok {
|
||||||
// TODO
|
return nil, ErrCouldNotConvert
|
||||||
return nil, fmt.Errorf("could not convert to map")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m[k] = Value(v)
|
m[k] = Value(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,37 +46,37 @@ func (v Value) String() string {
|
|||||||
// Int converts Value to int. Returned error
|
// Int converts Value to int. Returned error
|
||||||
// will be non nil if convesion failed.
|
// will be non nil if convesion failed.
|
||||||
func (v Value) Int() (int, error) {
|
func (v Value) Int() (int, error) {
|
||||||
return parseValue(v, strconv.Atoi)
|
return parseValue(v, strconv.Atoi) // Atoi in fact checks int size, no need to use ParseInt
|
||||||
}
|
}
|
||||||
|
|
||||||
// IntSlice splits Value (separator is ",") and adds
|
// IntSlice splits Value (separator is ",") and adds
|
||||||
// each of resulting values to []int if possible.
|
// each of resulting values to []int if possible.
|
||||||
// Returns non-nil error on first failure to convert.
|
// Returns nil and non-nil error on first failure to convert.
|
||||||
func (v Value) IntSlice() ([]int, error) {
|
func (v Value) IntSlice() ([]int, error) {
|
||||||
return parseSlice(v, strconv.Atoi)
|
return parseSlice(v, strconv.Atoi)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Float64 converts Setting Value to float64. Returned error
|
// Float64 converts Value to float64. Returned error
|
||||||
// will be non nil if convesion failed.
|
// will be non nil if convesion failed.
|
||||||
func (v Value) Float64() (float64, error) {
|
func (v Value) Float64() (float64, error) {
|
||||||
return parseValue(v, parseFloat64)
|
return parseValue(v, parseFloat64)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Float64Slice splits Setting Value (separator is ",") and adds
|
// Float64Slice splits Value (separator is ",") and adds
|
||||||
// each of resulting values to []float64 if possible.
|
// each of resulting values to []float64 if possible.
|
||||||
// Returns non-nil error on first failure to convert.
|
// Returns nil and non-nil error on first failure to convert.
|
||||||
func (v Value) Float64Slice() ([]float64, error) {
|
func (v Value) Float64Slice() ([]float64, error) {
|
||||||
return parseSlice(v, parseFloat64)
|
return parseSlice(v, parseFloat64)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringSlice splits Setting's Value (separator is ",") and adds
|
// StringSlice splits Value (separator is ",") and adds
|
||||||
// each of resulting values to []string trimming leading and trailing spaces
|
// each of resulting values to []string trimming leading and trailing spaces
|
||||||
// from each string.
|
// from each string.
|
||||||
func (v Value) StringSlice() []string {
|
func (v Value) StringSlice() []string {
|
||||||
return tidySplit(v, separatorSlice)
|
return tidySplit(v, separatorSlice)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bool tries to interpret Setting's Value as bool
|
// Bool tries to interpret Value as bool
|
||||||
// "1", "t", "T", true", "True", "TRUE" yields true
|
// "1", "t", "T", true", "True", "TRUE" yields true
|
||||||
// "0", "f", "F, "false", "False", "FALSE" yields false
|
// "0", "f", "F, "false", "False", "FALSE" yields false
|
||||||
// If nothing matches will return false and conf.ErrCouldNotConvert.
|
// If nothing matches will return false and conf.ErrCouldNotConvert.
|
||||||
@@ -81,6 +84,11 @@ func (v Value) Bool() (bool, error) {
|
|||||||
return parseValue(v, strconv.ParseBool)
|
return parseValue(v, strconv.ParseBool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// URL tries to interpret value as url.URL
|
||||||
|
func (v Value) URL() (*url.URL, error) {
|
||||||
|
return parseValue(v, url.Parse)
|
||||||
|
}
|
||||||
|
|
||||||
func parseSlice[T any, S ~string](s S, f func(string) (T, error)) ([]T, error) {
|
func parseSlice[T any, S ~string](s S, f func(string) (T, error)) ([]T, error) {
|
||||||
split := tidySplit(s, separatorSlice)
|
split := tidySplit(s, separatorSlice)
|
||||||
|
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package conf
|
package conf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -32,6 +34,19 @@ func TestValueMethods(t *testing.T) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "int",
|
||||||
|
f: func(t *testing.T) {
|
||||||
|
x, err := Value("010").Int()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if x != 10 {
|
||||||
|
t.Fatalf("want: %v, have: %v", 10, x)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "negative int",
|
name: "negative int",
|
||||||
f: func(t *testing.T) {
|
f: func(t *testing.T) {
|
||||||
@@ -189,11 +204,11 @@ func TestValueMethods(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bool",
|
name: "bad bool",
|
||||||
f: func(t *testing.T) {
|
f: func(t *testing.T) {
|
||||||
x, err := Value("unknown").Bool()
|
x, err := Value("unknown").Bool()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("bool doe not fail on incorrect values")
|
t.Fatal("bool does not fail on incorrect values")
|
||||||
}
|
}
|
||||||
|
|
||||||
if x != false {
|
if x != false {
|
||||||
@@ -272,7 +287,7 @@ func TestValueMethods(t *testing.T) {
|
|||||||
f: func(t *testing.T) {
|
f: func(t *testing.T) {
|
||||||
x, err := Value("0.1, a, 0.3").Float64Slice()
|
x, err := Value("0.1, a, 0.3").Float64Slice()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("float slice doe not fail on incorrect value")
|
t.Fatal("float slice does not fail on incorrect value")
|
||||||
}
|
}
|
||||||
|
|
||||||
var want []float64
|
var want []float64
|
||||||
@@ -281,6 +296,70 @@ func TestValueMethods(t *testing.T) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "map",
|
||||||
|
f: func(t *testing.T) {
|
||||||
|
x, err := Value("a: 1, b : 2, c :3").Map()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("map fails on correct values")
|
||||||
|
}
|
||||||
|
|
||||||
|
want := Map{
|
||||||
|
"a": Value("1"),
|
||||||
|
"b": Value("2"),
|
||||||
|
"c": Value("3"),
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(x, want) {
|
||||||
|
t.Fatalf("want: %v, have: %v", want, x)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "map invalid",
|
||||||
|
f: func(t *testing.T) {
|
||||||
|
x, err := Value("a:1, b-2, c: 3").Map()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("map does not fail on incorrect value")
|
||||||
|
}
|
||||||
|
|
||||||
|
var want Map
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(x, want) {
|
||||||
|
t.Fatalf("want: %v, have: %v", want, x)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "URL",
|
||||||
|
f: func(t *testing.T) {
|
||||||
|
x, err := Value("https://example.com:80").URL()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: "example.com:80",
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflect.DeepEqual(want, x) {
|
||||||
|
t.Fatalf("want: %v, have: %v", want, x)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad URL",
|
||||||
|
f: func(t *testing.T) {
|
||||||
|
x, err := Value(":malformed://url/").URL()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("url does not fail on incorrect input")
|
||||||
|
}
|
||||||
|
|
||||||
|
if x != nil {
|
||||||
|
t.Fatalf("want: %v, have: %v", nil, x)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range tc {
|
for _, c := range tc {
|
||||||
|
Reference in New Issue
Block a user