Files
conf/map.go
2025-04-20 21:05:14 +03:00

184 lines
4.0 KiB
Go

// 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
}