rewrite
This commit is contained in:
210
conf.go
Normal file
210
conf.go
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
// 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
|
||||||
|
}
|
214
conf_test.go
214
conf_test.go
@@ -2,108 +2,134 @@ package conf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testConf = []byte(`port=10000
|
func TestRead(t *testing.T) {
|
||||||
# removed
|
tc := []struct {
|
||||||
two = two words
|
name string
|
||||||
commas = abc, def, ghi
|
in []byte
|
||||||
token=test
|
res *Conf
|
||||||
bool1=1
|
err error
|
||||||
bool2=true
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
in: []byte{},
|
||||||
|
res: &Conf{
|
||||||
|
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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single key with spaces",
|
||||||
|
in: []byte("key = value"),
|
||||||
|
res: &Conf{
|
||||||
|
values: []*kv{
|
||||||
|
{
|
||||||
|
k: "key",
|
||||||
|
v: Value("value"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single key no spaces",
|
||||||
|
in: []byte("key=value"),
|
||||||
|
res: &Conf{
|
||||||
|
values: []*kv{
|
||||||
|
{
|
||||||
|
k: "key",
|
||||||
|
v: Value("value"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single key with newline",
|
||||||
|
in: []byte("key=value\n"),
|
||||||
|
res: &Conf{
|
||||||
|
values: []*kv{
|
||||||
|
{
|
||||||
|
k: "key",
|
||||||
|
v: Value("value"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single key with comment and newline",
|
||||||
|
in: []byte("key =value # this is a comment\n"),
|
||||||
|
res: &Conf{
|
||||||
|
values: []*kv{
|
||||||
|
{
|
||||||
|
k: "key",
|
||||||
|
v: Value("value"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
editor = vim
|
for _, c := range tc {
|
||||||
distance=13.42
|
t.Run(c.name, func(t *testing.T) {
|
||||||
floats=0.5,2.37,6
|
r := bytes.NewReader(c.in)
|
||||||
floatswithstring = 0.5, hello, 0.9
|
|
||||||
false
|
|
||||||
color`)
|
|
||||||
|
|
||||||
func TestPackage(t *testing.T) {
|
conf, err := Read(r)
|
||||||
r := bytes.NewReader(testConf)
|
if !errors.Is(err, c.err) {
|
||||||
c, err := parseReader(r)
|
t.Fatalf("want error: %v have: %v", c.err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(c.res, conf) {
|
||||||
|
t.Fatalf("want: %+v, have: %+v", c.res, conf)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpen(t *testing.T) {
|
||||||
|
conf, err := Open("./testdata/test.conf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if _, err := c.Get("floatswithstring").Float64Slice(); err == nil {
|
|
||||||
t.Log("Float64Slice accepting incorrect values")
|
const (
|
||||||
t.Fail()
|
key = "key"
|
||||||
|
value = "value"
|
||||||
|
)
|
||||||
|
for _, n := range []string{"1", "2", "3", "4"} {
|
||||||
|
v := conf.Get(key + n)
|
||||||
|
if v.String() != value+n {
|
||||||
|
t.Errorf("want: %s got: %s", value+n, v)
|
||||||
}
|
}
|
||||||
if _, err := c.Get("floats").Float64Slice(); err != nil {
|
}
|
||||||
t.Log("Float64Slice failing on correct values")
|
}
|
||||||
t.Fail()
|
|
||||||
}
|
func TestKeys(t *testing.T) {
|
||||||
if v := c.Get("token").String(); v != "test" {
|
c := Conf{
|
||||||
t.Log("failed finding key value")
|
values: []*kv{
|
||||||
t.Fail()
|
{
|
||||||
}
|
k: "1",
|
||||||
if v := c.Get("editor").String(); v != "vim" {
|
v: Value(""),
|
||||||
t.Log("failed finding key value")
|
},
|
||||||
t.Fail()
|
{
|
||||||
}
|
k: "2",
|
||||||
if v, _ := c.Get("port").Int(); v != 10000 {
|
v: Value(""),
|
||||||
t.Log("failed finding int")
|
},
|
||||||
t.Fail()
|
{
|
||||||
}
|
k: "3",
|
||||||
if v, _ := c.Get("distance").Float64(); v != 13.42 {
|
v: Value(""),
|
||||||
t.Log("failed finding key value")
|
},
|
||||||
t.Fail()
|
},
|
||||||
}
|
}
|
||||||
if c.HasOption("color") != true {
|
|
||||||
t.Log("failed finding option")
|
if !slices.Equal([]string{"1", "2", "3"}, c.Keys()) {
|
||||||
t.Fail()
|
t.Fatal("Keys method returns incorrect values")
|
||||||
}
|
|
||||||
if v, _ := c.Get("bool1").Bool(); v != true {
|
|
||||||
t.Log("failed finding bool1 value")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if v, _ := c.Get("bool2").Bool(); v != true {
|
|
||||||
t.Log("failed finding bool2 value")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if v := c.Get("two").String(); v != "two words" {
|
|
||||||
t.Log("failed finding key value")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if v := c.Get("commas").String(); v != "abc, def, ghi" {
|
|
||||||
t.Log("failed finding key value")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if v := c.Get("nonexistent").String(); v != "" {
|
|
||||||
t.Log("returned non-empty string for nonexistent key")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if c.HasOption("removed") {
|
|
||||||
t.Log("commented out line shows up in config")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
splitted := c.Get("commas").StringSlice()
|
|
||||||
if len(splitted) != 3 {
|
|
||||||
t.Log("could not split string")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
abc := splitted[0]
|
|
||||||
ghi := splitted[2]
|
|
||||||
if abc != "abc" || ghi != "ghi" {
|
|
||||||
t.Log("Split() returned incorrect values")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if c.HasOption("no") != true {
|
|
||||||
t.Log("should capture one option per line even if line holds two")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if c.HasOption("missedme") == true {
|
|
||||||
t.Log("should only capture one option per line")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
st := Setting{}
|
|
||||||
if v, err := st.Float64(); v != 0.0 || err == nil {
|
|
||||||
t.Log("empty string erroneously converts to float")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if def := c.GetDefault("non-existant-key", "myvalue"); def.String() != "myvalue" {
|
|
||||||
t.Log("GetDefault fails to apply default value")
|
|
||||||
t.Fail()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
112
config.go
112
config.go
@@ -1,112 +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
|
|
||||||
|
|
||||||
// Config holds parsed keys and values.
|
|
||||||
type Conf struct {
|
|
||||||
// Settings store key value pairs ("key = value" in config file)
|
|
||||||
// all key value pairs found when parsing input are accumulated in this map.
|
|
||||||
settings map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find looks up a Setting and returns it. If returned error is not nil
|
|
||||||
// the requested key was not found and returned Setting has empty string in Value
|
|
||||||
// field.
|
|
||||||
func (c *Conf) Find(key string) (Setting, bool) {
|
|
||||||
return c.get(key, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns a Setting. If key was not found the returned Setting Value
|
|
||||||
// will be empty string.
|
|
||||||
func (c *Conf) Get(key string) Setting {
|
|
||||||
s, _ := c.get(key, "")
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDefault looks up a Setting with requested key.
|
|
||||||
// If lookup fails it returns Setting with Value field set to def.
|
|
||||||
func (c *Conf) GetDefault(key, def string) Setting {
|
|
||||||
s, _ := c.get(key, def)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conf) Settings() []Setting {
|
|
||||||
list := make([]Setting, 0, len(c.settings))
|
|
||||||
|
|
||||||
for k, v := range c.settings {
|
|
||||||
s := Setting{
|
|
||||||
Name: k,
|
|
||||||
Value: v,
|
|
||||||
}
|
|
||||||
|
|
||||||
list = append(list, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conf) get(key string, def string) (Setting, bool) {
|
|
||||||
v, ok := c.settings[key]
|
|
||||||
if def != "" && !ok {
|
|
||||||
v = def
|
|
||||||
}
|
|
||||||
|
|
||||||
return Setting{
|
|
||||||
Name: key,
|
|
||||||
Value: v,
|
|
||||||
}, ok
|
|
||||||
}
|
|
@@ -15,21 +15,11 @@ bool0=0
|
|||||||
booltrue = true
|
booltrue = true
|
||||||
distance=13.42
|
distance=13.42
|
||||||
iamsure = hopefully you're not mistaken
|
iamsure = hopefully you're not mistaken
|
||||||
float = 13.1984
|
float = 13.1984`)
|
||||||
color`)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
r := bytes.NewReader(testConf) // creating io.Reader from []byte()
|
r := bytes.NewReader(testConf) // creating io.Reader from []byte()
|
||||||
config := conf.ParseReader(r) // we could call conf.ParseFile("filename") here
|
config, _ := conf.Read(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
|
// Find( key string) returns instance of Setting and an
|
||||||
// error if key was not found
|
// error if key was not found
|
||||||
@@ -39,8 +29,8 @@ func main() {
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// You can access Setting's Value field (type string) directly.
|
// You can access Setting's Value field (type string) directly.
|
||||||
port, err := config.Find("port")
|
port, ok := 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)
|
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)
|
||||||
|
|
||||||
// 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
|
||||||
@@ -75,13 +65,6 @@ func main() {
|
|||||||
def := config.GetDefault("non-existant-key", "myvalue")
|
def := config.GetDefault("non-existant-key", "myvalue")
|
||||||
fmt.Println(def.Value) // "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
|
// Below code finds two keys with bool values in the Config
|
||||||
// and outputs those.
|
// and outputs those.
|
||||||
var t, f bool
|
var t, f bool
|
||||||
|
108
parser.go
108
parser.go
@@ -1,108 +0,0 @@
|
|||||||
package conf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
strComment = "#"
|
|
||||||
strEqSign = "="
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrFormat = errors.New("conf: line does not match \"key = value\" pattern")
|
|
||||||
ErrDuplicateKey = errors.New("conf: duplicate key found")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Open reads values from file. It returns nil and error if os.Open(filename) fails.
|
|
||||||
// It would be wise to always check returned error.
|
|
||||||
// ParseFile captures two types of values: "key = value" and "option". Either key value pair or
|
|
||||||
// option must be put in its own line in the file.
|
|
||||||
// Key or option must be a single word. In example line:
|
|
||||||
//
|
|
||||||
// "option1 option2"
|
|
||||||
//
|
|
||||||
// only option1 will be captured. option2 needs to be in a separate line in the file to take effect.
|
|
||||||
// In line "key = value1,value2,value3" all of value1, value2, and value3 will be
|
|
||||||
// captured. They can be later accesed separately with Setting's Split() method.
|
|
||||||
func Open(filename string) (*Conf, error) {
|
|
||||||
file, err := os.Open(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := parseReader(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, file.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads from r and returns Config.
|
|
||||||
func Read(r io.Reader) (*Conf, error) {
|
|
||||||
return parseReader(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseReader(r io.Reader) (*Conf, error) {
|
|
||||||
settings := make(map[string]string)
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(r)
|
|
||||||
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
|
|
||||||
line = trim(stripComment(line))
|
|
||||||
if line == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
key, value, ok := toKV(line)
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := settings[key]; ok {
|
|
||||||
return nil, fmt.Errorf("%w: %s", ErrDuplicateKey, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
settings[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Conf{
|
|
||||||
settings: settings,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func toKV(s string) (string, string, bool) {
|
|
||||||
k, v, ok := strings.Cut(s, strEqSign)
|
|
||||||
if !ok || k == "" || v == "" {
|
|
||||||
return "", "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
return trim(k), trim(v), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func trim(s string) string {
|
|
||||||
s = strings.TrimSpace(s)
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripComment(s string) string {
|
|
||||||
idx := strings.Index(s, strComment)
|
|
||||||
if idx > -1 {
|
|
||||||
s = s[:idx]
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
159
parser_test.go
159
parser_test.go
@@ -1,159 +0,0 @@
|
|||||||
package conf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_trim(t *testing.T) {
|
|
||||||
tc := []struct {
|
|
||||||
name string
|
|
||||||
in string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
in: "",
|
|
||||||
want: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "comment",
|
|
||||||
in: "# some comment",
|
|
||||||
want: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "comment",
|
|
||||||
in: "#some comment",
|
|
||||||
want: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "comment",
|
|
||||||
in: " #some comment",
|
|
||||||
want: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "key value",
|
|
||||||
in: "key = value",
|
|
||||||
want: "key = value",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "key value with whitespace",
|
|
||||||
in: " key = value ",
|
|
||||||
want: "key = value",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "key value with whitespace",
|
|
||||||
in: "\tkey = value \t",
|
|
||||||
want: "key = value",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "key value with whitespace",
|
|
||||||
in: "\tkey = value \t\n",
|
|
||||||
want: "key = value",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "key value with comment",
|
|
||||||
in: "key = value #comment",
|
|
||||||
want: "key = value",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "option",
|
|
||||||
in: "option \n",
|
|
||||||
want: "option",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "option with comment",
|
|
||||||
in: "option # comment \n",
|
|
||||||
want: "option",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range tc {
|
|
||||||
t.Run(c.name, func(t *testing.T) {
|
|
||||||
have := trim(c.in)
|
|
||||||
if have != c.want {
|
|
||||||
t.Errorf("want: %s, have: %s", have, c.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRead(t *testing.T) {
|
|
||||||
tc := []struct {
|
|
||||||
name string
|
|
||||||
in []byte
|
|
||||||
res *Conf
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
in: []byte{},
|
|
||||||
res: &Conf{
|
|
||||||
settings: make(map[string]string),
|
|
||||||
options: map[string]struct{}{},
|
|
||||||
},
|
|
||||||
err: nil, // if the reader is empty its not out fault
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single key with spaces",
|
|
||||||
in: []byte("key = value"),
|
|
||||||
res: &Conf{
|
|
||||||
settings: map[string]string{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
options: make(map[string]struct{}),
|
|
||||||
},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single key no spaces",
|
|
||||||
in: []byte("key=value"),
|
|
||||||
res: &Conf{
|
|
||||||
settings: map[string]string{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
options: make(map[string]struct{}),
|
|
||||||
},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single key with newline",
|
|
||||||
in: []byte("key=value\n"),
|
|
||||||
res: &Conf{
|
|
||||||
settings: map[string]string{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
options: make(map[string]struct{}),
|
|
||||||
},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single key with comment and newline",
|
|
||||||
in: []byte("key =value # this is a comment\n"),
|
|
||||||
res: &Conf{
|
|
||||||
settings: map[string]string{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
options: make(map[string]struct{}),
|
|
||||||
},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range tc {
|
|
||||||
t.Run(c.name, func(t *testing.T) {
|
|
||||||
r := bytes.NewReader(c.in)
|
|
||||||
|
|
||||||
conf, err := Read(r)
|
|
||||||
if !errors.Is(err, c.err) {
|
|
||||||
t.Fatalf("want error: %v have: %v", c.err, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(c.res, conf) {
|
|
||||||
t.Fatalf("want: %+v, have: %+v", c.res, conf)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
109
settings.go
109
settings.go
@@ -1,109 +0,0 @@
|
|||||||
package conf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNotFound = errors.New("conf: key not found")
|
|
||||||
ErrCouldNotConvert = errors.New("conf: could not cast one or more values to required type")
|
|
||||||
)
|
|
||||||
|
|
||||||
const valuesSeparator = ","
|
|
||||||
|
|
||||||
// Setting represents key-value pair read from config file.
|
|
||||||
// It's Value field holds the value of key parsed from the configuration
|
|
||||||
type Setting struct {
|
|
||||||
Name string
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Int converts Setting Value to int. Returned error
|
|
||||||
// will be non nil if convesion failed.
|
|
||||||
func (s Setting) Int() (int, error) {
|
|
||||||
return parseValue(s.Value, strconv.Atoi)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntSlice splits Setting Value (separator is ",") and adds
|
|
||||||
// each of resulting values to []int if possible.
|
|
||||||
// If one or more values can not be converted to float64 those will be dropped
|
|
||||||
// and method will return conf.ErrCouldNotConvert.
|
|
||||||
// Check error to be sure that all required values were parsed.
|
|
||||||
func (s Setting) IntSlice() ([]int, error) {
|
|
||||||
return parseSlice(s.Value, strconv.Atoi)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Float64 converts Setting Value to float64. Returned error
|
|
||||||
// will be non nil if convesion failed.
|
|
||||||
func (s Setting) Float64() (float64, error) {
|
|
||||||
return parseValue(s.Value, parseFloat64)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Float64Slice splits Setting Value (separator is ",") and adds
|
|
||||||
// each of resulting values to []float64 if possible.
|
|
||||||
// If one or more values can not be converted to float64 those will be dropped
|
|
||||||
// and method will return conf.ErrCouldNotConvert.
|
|
||||||
// Check error to be sure that all required values were parsed.
|
|
||||||
func (s Setting) Float64Slice() ([]float64, error) {
|
|
||||||
return parseSlice(s.Value, parseFloat64)
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns option Value as string
|
|
||||||
// This method also implements Stringer interface from fmt module
|
|
||||||
func (s Setting) String() string {
|
|
||||||
return s.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
// StringSlice splits Setting's Value (separator is ",") and adds
|
|
||||||
// each of resulting values to []string trimming leading and trailing spaces
|
|
||||||
// from each string.
|
|
||||||
func (s Setting) StringSlice() []string {
|
|
||||||
return tidySplit(s.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bool tries to interpret Setting's Value as bool
|
|
||||||
// "1", "t", "T", true", "True", "TRUE" yields true
|
|
||||||
// "0", "f", "F, "false", "False", "FALSE" yields false
|
|
||||||
// If nothing matches will return false and conf.ErrCouldNotConvert.
|
|
||||||
func (s Setting) Bool() (bool, error) {
|
|
||||||
return parseValue(s.Value, strconv.ParseBool)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseSlice[T any](s string, f func(string) (T, error)) ([]T, error) {
|
|
||||||
split := tidySplit(s)
|
|
||||||
|
|
||||||
list := make([]T, 0, len(split))
|
|
||||||
for _, str := range split {
|
|
||||||
v, err := parseValue(str, f)
|
|
||||||
if err != nil {
|
|
||||||
return list, err
|
|
||||||
}
|
|
||||||
|
|
||||||
list = append(list, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return list, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseValue[T any](s string, f func(string) (T, error)) (T, error) {
|
|
||||||
v, err := f(s)
|
|
||||||
if err != nil {
|
|
||||||
return v, errors.Join(ErrCouldNotConvert, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func tidySplit(s string) []string {
|
|
||||||
splitted := strings.Split(s, valuesSeparator)
|
|
||||||
for i, str := range splitted {
|
|
||||||
splitted[i] = strings.TrimSpace(str)
|
|
||||||
}
|
|
||||||
return splitted
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseFloat64(s string) (float64, error) {
|
|
||||||
return strconv.ParseFloat(s, 64)
|
|
||||||
}
|
|
5
testdata/test.conf
vendored
Normal file
5
testdata/test.conf
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# comment
|
||||||
|
key1 = value1
|
||||||
|
key2=value2
|
||||||
|
key3 = value3
|
||||||
|
key4 = value4 # comment
|
32
util.go
Normal file
32
util.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package conf
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const (
|
||||||
|
separatorComment = "#"
|
||||||
|
separatorKV = "="
|
||||||
|
)
|
||||||
|
|
||||||
|
func toKV(s string, sep string) (string, string, bool) {
|
||||||
|
k, v, ok := strings.Cut(s, sep)
|
||||||
|
if !ok || k == "" || v == "" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim(k), trim(v), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func trim(s string) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripComment(s string) string {
|
||||||
|
idx := strings.Index(s, separatorComment)
|
||||||
|
if idx > -1 {
|
||||||
|
s = s[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
209
util_test.go
Normal file
209
util_test.go
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
package conf
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func Test_trim(t *testing.T) {
|
||||||
|
tc := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
in: "",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spaces",
|
||||||
|
in: " text ",
|
||||||
|
want: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tabs",
|
||||||
|
in: "\ttext\t",
|
||||||
|
want: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "newline",
|
||||||
|
in: "text\n",
|
||||||
|
want: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "newline",
|
||||||
|
in: "text\r",
|
||||||
|
want: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "newline",
|
||||||
|
in: "text\r\n",
|
||||||
|
want: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed",
|
||||||
|
in: " \t text\n\r",
|
||||||
|
want: "text",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range tc {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
have := trim(c.in)
|
||||||
|
if have != c.want {
|
||||||
|
t.Errorf("want: %s, have: %s", have, c.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_stripComment(t *testing.T) {
|
||||||
|
tc := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
in: "",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comment",
|
||||||
|
in: "# some comment",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comment",
|
||||||
|
in: "#some comment",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comment",
|
||||||
|
in: "text#some comment",
|
||||||
|
want: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comment",
|
||||||
|
in: "text #some comment",
|
||||||
|
want: "text ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comment",
|
||||||
|
in: " text #some comment",
|
||||||
|
want: " text ",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range tc {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
have := stripComment(c.in)
|
||||||
|
if have != c.want {
|
||||||
|
t.Errorf("want: %s, have: %s", have, c.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_toKV(t *testing.T) {
|
||||||
|
tc := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
k string
|
||||||
|
v string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
in: "",
|
||||||
|
k: "",
|
||||||
|
v: "",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty value",
|
||||||
|
in: "key=",
|
||||||
|
k: "",
|
||||||
|
v: "",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty key",
|
||||||
|
in: "=value",
|
||||||
|
k: "",
|
||||||
|
v: "",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incorrect separator",
|
||||||
|
in: "key/value",
|
||||||
|
k: "",
|
||||||
|
v: "",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incorrect separator",
|
||||||
|
in: "key / value",
|
||||||
|
k: "",
|
||||||
|
v: "",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incorrect separator",
|
||||||
|
in: "key value",
|
||||||
|
k: "",
|
||||||
|
v: "",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no spaces",
|
||||||
|
in: "key=value",
|
||||||
|
k: "key",
|
||||||
|
v: "value",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with spaces",
|
||||||
|
in: "key = value",
|
||||||
|
k: "key",
|
||||||
|
v: "value",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "with spaces",
|
||||||
|
in: " key = value ",
|
||||||
|
k: "key",
|
||||||
|
v: "value",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with tabs",
|
||||||
|
in: "key\t=\tvalue",
|
||||||
|
k: "key",
|
||||||
|
v: "value",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with spaces and tabs",
|
||||||
|
in: " key\t=\tvalue ",
|
||||||
|
k: "key",
|
||||||
|
v: "value",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "more tabs",
|
||||||
|
in: "\tkey\t=\tvalue\t",
|
||||||
|
k: "key",
|
||||||
|
v: "value",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range tc {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
k, v, ok := toKV(c.in, separatorKV)
|
||||||
|
if k != c.k || v != c.v || ok != c.ok {
|
||||||
|
t.Errorf("want: %s %s %t, have: %s %s %t", c.k, c.v, c.ok, k, v, ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
119
value.go
Normal file
119
value.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package conf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCouldNotConvert = errors.New("conf: could not cast one or more values to required type")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
separatorSlice = ","
|
||||||
|
separatorMap = ":"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Value string
|
||||||
|
|
||||||
|
func (v Value) Map() (map[string]Value, error) {
|
||||||
|
split := tidySplit(v, separatorSlice)
|
||||||
|
|
||||||
|
m := make(map[string]Value, len(split))
|
||||||
|
|
||||||
|
for i := range split {
|
||||||
|
k, v, ok := toKV(split[i], separatorMap)
|
||||||
|
if !ok {
|
||||||
|
// TODO
|
||||||
|
return nil, fmt.Errorf("could not convert to map")
|
||||||
|
}
|
||||||
|
|
||||||
|
m[k] = Value(v)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns Value as string
|
||||||
|
func (v Value) String() string {
|
||||||
|
return string(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int converts Value to int. Returned error
|
||||||
|
// will be non nil if convesion failed.
|
||||||
|
func (v Value) Int() (int, error) {
|
||||||
|
return parseValue(v, strconv.Atoi)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntSlice splits Value (separator is ",") and adds
|
||||||
|
// each of resulting values to []int if possible.
|
||||||
|
// Returns non-nil error on first failure to convert.
|
||||||
|
func (v Value) IntSlice() ([]int, error) {
|
||||||
|
return parseSlice(v, strconv.Atoi)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 converts Setting Value to float64. Returned error
|
||||||
|
// will be non nil if convesion failed.
|
||||||
|
func (v Value) Float64() (float64, error) {
|
||||||
|
return parseValue(v, parseFloat64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64Slice splits Setting Value (separator is ",") and adds
|
||||||
|
// each of resulting values to []float64 if possible.
|
||||||
|
// Returns non-nil error on first failure to convert.
|
||||||
|
func (v Value) Float64Slice() ([]float64, error) {
|
||||||
|
return parseSlice(v, parseFloat64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringSlice splits Setting's Value (separator is ",") and adds
|
||||||
|
// each of resulting values to []string trimming leading and trailing spaces
|
||||||
|
// from each string.
|
||||||
|
func (v Value) StringSlice() []string {
|
||||||
|
return tidySplit(v, separatorSlice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool tries to interpret Setting's Value as bool
|
||||||
|
// "1", "t", "T", true", "True", "TRUE" yields true
|
||||||
|
// "0", "f", "F, "false", "False", "FALSE" yields false
|
||||||
|
// If nothing matches will return false and conf.ErrCouldNotConvert.
|
||||||
|
func (v Value) Bool() (bool, error) {
|
||||||
|
return parseValue(v, strconv.ParseBool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSlice[T any, S ~string](s S, f func(string) (T, error)) ([]T, error) {
|
||||||
|
split := tidySplit(s, separatorSlice)
|
||||||
|
|
||||||
|
list := make([]T, 0, len(split))
|
||||||
|
for _, str := range split {
|
||||||
|
v, err := parseValue(str, f)
|
||||||
|
if err != nil {
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
|
|
||||||
|
list = append(list, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseValue[T any, S ~string](s S, f func(string) (T, error)) (T, error) {
|
||||||
|
v, err := f(string(s))
|
||||||
|
if err != nil {
|
||||||
|
return v, errors.Join(ErrCouldNotConvert, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func tidySplit[S ~string](s S, sep string) []string {
|
||||||
|
splitted := strings.Split(string(s), sep)
|
||||||
|
for i, str := range splitted {
|
||||||
|
splitted[i] = strings.TrimSpace(str)
|
||||||
|
}
|
||||||
|
return splitted
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFloat64(s string) (float64, error) {
|
||||||
|
return strconv.ParseFloat(s, 64)
|
||||||
|
}
|
Reference in New Issue
Block a user