mirror of
https://github.com/mjl-/mox.git
synced 2024-12-28 17:33:47 +03:00
dde2258f69
based on chat with niklas/broitzer
330 lines
6.9 KiB
Go
330 lines
6.9 KiB
Go
package sconf
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type parser struct {
|
|
prefix string // indented string
|
|
input *bufio.Reader // for reading lines at a time
|
|
line string // last read line
|
|
linenumber int
|
|
}
|
|
|
|
type parseError struct {
|
|
err error
|
|
}
|
|
|
|
func parse(path string, src io.Reader, dst interface{}) (err error) {
|
|
p := &parser{
|
|
input: bufio.NewReader(src),
|
|
}
|
|
defer func() {
|
|
x := recover()
|
|
if x == nil {
|
|
return
|
|
}
|
|
perr, ok := x.(parseError)
|
|
if ok {
|
|
err = fmt.Errorf("%s:%d: %v", path, p.linenumber, perr.err)
|
|
return
|
|
}
|
|
panic(x)
|
|
}()
|
|
v := reflect.ValueOf(dst)
|
|
if v.Kind() != reflect.Ptr {
|
|
p.stop("destination not a pointer")
|
|
}
|
|
p.parseStruct0(v.Elem())
|
|
return
|
|
}
|
|
|
|
func (p *parser) stop(err string) {
|
|
panic(parseError{errors.New(err)})
|
|
}
|
|
|
|
func (p *parser) check(err error, action string) {
|
|
if err != nil {
|
|
p.stop(fmt.Sprintf("%s: %s", action, err))
|
|
}
|
|
}
|
|
|
|
func (p *parser) string() string {
|
|
return p.line
|
|
}
|
|
|
|
func (p *parser) leave(s string) {
|
|
p.line = s
|
|
}
|
|
|
|
func (p *parser) consume() string {
|
|
s := p.line
|
|
p.line = ""
|
|
return s
|
|
}
|
|
|
|
// Next returns whether the next line is properly indented, reading data as necessary.
|
|
func (p *parser) next() bool {
|
|
for p.line == "" {
|
|
s, err := p.input.ReadString('\n')
|
|
if s == "" {
|
|
if err == io.EOF {
|
|
return false
|
|
}
|
|
p.stop(err.Error())
|
|
}
|
|
p.linenumber++
|
|
if strings.HasPrefix(strings.TrimSpace(s), "#") {
|
|
continue
|
|
}
|
|
p.line = strings.TrimSuffix(s, "\n")
|
|
}
|
|
|
|
// Less indenting than expected. Let caller stop, returning to its caller for lower-level indent.
|
|
r := strings.HasPrefix(p.line, p.prefix)
|
|
return r
|
|
}
|
|
|
|
func (p *parser) indent() {
|
|
p.prefix += "\t"
|
|
if !p.next() {
|
|
p.stop("expected indent")
|
|
}
|
|
}
|
|
|
|
func (p *parser) unindent() {
|
|
p.prefix = p.prefix[1:]
|
|
}
|
|
|
|
var durationType = reflect.TypeOf(time.Duration(0))
|
|
|
|
func (p *parser) parseValue(v reflect.Value) reflect.Value {
|
|
t := v.Type()
|
|
|
|
if t == durationType {
|
|
s := p.consume()
|
|
d, err := time.ParseDuration(s)
|
|
p.check(err, "parsing duration")
|
|
v.Set(reflect.ValueOf(d))
|
|
return v
|
|
}
|
|
|
|
switch t.Kind() {
|
|
default:
|
|
p.stop(fmt.Sprintf("cannot parse type %v", t.Kind()))
|
|
|
|
case reflect.Bool:
|
|
s := p.consume()
|
|
switch s {
|
|
case "false":
|
|
v.SetBool(false)
|
|
case "true":
|
|
v.SetBool(true)
|
|
default:
|
|
p.stop(fmt.Sprintf("bad boolean value %q", s))
|
|
}
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
s := p.consume()
|
|
x, err := strconv.ParseInt(s, 10, 64)
|
|
p.check(err, "parsing integer")
|
|
v.SetInt(x)
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
s := p.consume()
|
|
x, err := strconv.ParseUint(s, 10, 64)
|
|
p.check(err, "parsing integer")
|
|
v.SetUint(x)
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
s := p.consume()
|
|
x, err := strconv.ParseFloat(s, 64)
|
|
p.check(err, "parsing float")
|
|
v.SetFloat(x)
|
|
|
|
case reflect.String:
|
|
v.SetString(p.consume())
|
|
|
|
case reflect.Slice:
|
|
v = p.parseSlice(v)
|
|
|
|
case reflect.Ptr:
|
|
vv := reflect.New(t.Elem())
|
|
p.parseValue(vv.Elem())
|
|
v.Set(vv)
|
|
|
|
case reflect.Struct:
|
|
p.parseStruct(v)
|
|
|
|
case reflect.Map:
|
|
v = reflect.MakeMap(t)
|
|
p.parseMap(v)
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (p *parser) parseSlice(v reflect.Value) reflect.Value {
|
|
if v.Type().Elem().Kind() == reflect.Uint8 {
|
|
s := p.consume()
|
|
buf, err := base64.StdEncoding.DecodeString(s)
|
|
p.check(err, "parsing base64")
|
|
v.SetBytes(buf)
|
|
return v
|
|
}
|
|
|
|
p.indent()
|
|
defer p.unindent()
|
|
return p.parseSlice0(v)
|
|
}
|
|
|
|
func (p *parser) parseSlice0(v reflect.Value) reflect.Value {
|
|
for p.next() {
|
|
s := p.string()
|
|
prefix := p.prefix + "-"
|
|
if !strings.HasPrefix(s, prefix) {
|
|
p.stop(fmt.Sprintf("expected item, prefix %q, saw %q", prefix, s))
|
|
}
|
|
s = s[len(prefix):]
|
|
if s != "" {
|
|
if !strings.HasPrefix(s, " ") {
|
|
p.stop("missing space after -")
|
|
}
|
|
s = s[1:]
|
|
}
|
|
p.leave(s)
|
|
vv := reflect.New(v.Type().Elem()).Elem()
|
|
vv = p.parseValue(vv)
|
|
v = reflect.Append(v, vv)
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (p *parser) parseStruct(v reflect.Value) {
|
|
p.indent()
|
|
defer p.unindent()
|
|
p.parseStruct0(v)
|
|
}
|
|
|
|
func (p *parser) parseStruct0(v reflect.Value) {
|
|
seen := map[string]struct{}{}
|
|
var zeroValue reflect.Value
|
|
t := v.Type()
|
|
for p.next() {
|
|
origs := p.string()
|
|
s := origs[len(p.prefix):]
|
|
l := strings.SplitN(s, ":", 2)
|
|
if len(l) != 2 {
|
|
var more string
|
|
if strings.TrimSpace(s) == "" {
|
|
more = " (perhaps stray whitespace)"
|
|
} else if strings.HasPrefix(l[0], " ") {
|
|
more = " (perhaps mixed tab/space indenting)"
|
|
}
|
|
p.stop(fmt.Sprintf("missing colon for struct key/value on non-empty line %q%s", origs, more))
|
|
}
|
|
k := l[0]
|
|
if k == "" {
|
|
p.stop("empty key in struct")
|
|
} else if strings.HasPrefix(k, " ") {
|
|
p.stop("key in struct starting with space (perhaps mixed tab/space indenting)")
|
|
}
|
|
if _, ok := seen[k]; ok {
|
|
p.stop("duplicate key in struct")
|
|
}
|
|
seen[k] = struct{}{}
|
|
s = l[1]
|
|
if s != "" && !strings.HasPrefix(s, " ") {
|
|
p.stop("missing space after colon in struct")
|
|
}
|
|
if s != "" {
|
|
s = s[1:]
|
|
}
|
|
p.leave(s)
|
|
|
|
vv := v.FieldByName(k)
|
|
if vv == zeroValue {
|
|
var more string
|
|
if strings.TrimSpace(k) != k {
|
|
more = " (perhaps stray whitespace in key)"
|
|
}
|
|
p.stop(fmt.Sprintf("unknown key %q%s", k, more))
|
|
}
|
|
if ft, _ := t.FieldByName(k); !ft.IsExported() || isIgnore(ft.Tag.Get("sconf")) {
|
|
p.stop(fmt.Sprintf("unknown key %q (has ignore tag or not exported)", k))
|
|
}
|
|
vv.Set(p.parseValue(vv))
|
|
}
|
|
|
|
n := t.NumField()
|
|
for i := 0; i < n; i++ {
|
|
f := t.Field(i)
|
|
if !f.IsExported() || isIgnore(f.Tag.Get("sconf")) || isOptional(f.Tag.Get("sconf")) {
|
|
continue
|
|
}
|
|
if _, ok := seen[f.Name]; !ok {
|
|
p.stop(fmt.Sprintf("missing required key %q", f.Name))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseMap(v reflect.Value) {
|
|
p.indent()
|
|
defer p.unindent()
|
|
p.parseMap0(v)
|
|
}
|
|
|
|
func (p *parser) parseMap0(v reflect.Value) {
|
|
seen := map[string]struct{}{}
|
|
t := v.Type()
|
|
for p.next() {
|
|
origs := p.string()
|
|
s := origs[len(p.prefix):]
|
|
l := strings.SplitN(s, ":", 2)
|
|
if len(l) != 2 {
|
|
var more string
|
|
if strings.TrimSpace(s) == "" {
|
|
more = " (perhaps stray whitespace)"
|
|
} else if strings.HasPrefix(l[0], " ") {
|
|
more = " (perhaps mixed tab/space indenting)"
|
|
}
|
|
p.stop(fmt.Sprintf("missing colon for map key/value on non-empty line %q%s", origs, more))
|
|
}
|
|
k := l[0]
|
|
if k == "" {
|
|
p.stop("empty key in map")
|
|
}
|
|
if _, ok := seen[k]; ok {
|
|
p.stop("duplicate key in map")
|
|
}
|
|
seen[k] = struct{}{}
|
|
s = l[1]
|
|
if s != "" && !strings.HasPrefix(s, " ") {
|
|
var more string
|
|
if strings.HasPrefix(k, " ") {
|
|
more = " (key starts with space, perhaps mixed tab/space indenting)"
|
|
}
|
|
p.stop("missing space after colon in map" + more)
|
|
}
|
|
if s != "" {
|
|
s = s[1:]
|
|
}
|
|
|
|
vv := reflect.New(t.Elem()).Elem()
|
|
if s == "nil" {
|
|
// Special value "nil" means the zero value, no further parsing of a value.
|
|
p.leave("")
|
|
} else {
|
|
p.leave(s)
|
|
vv = p.parseValue(vv)
|
|
}
|
|
v.SetMapIndex(reflect.ValueOf(k), vv)
|
|
}
|
|
}
|