v1
This commit is contained in:
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
|
||||
}
|
Reference in New Issue
Block a user