mirror of
https://github.com/mjl-/mox.git
synced 2024-12-28 17:33:47 +03:00
9534e464f9
hopefully this helps admins editing the file and prevent mistakes about config files. for issue #56 by kikoreis, thanks!
288 lines
5.6 KiB
Go
288 lines
5.6 KiB
Go
package sconf
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/mjl-/xfmt"
|
|
)
|
|
|
|
var errNoElem = errors.New("no elements")
|
|
|
|
type writeError struct{ error }
|
|
|
|
type writer struct {
|
|
out *bufio.Writer
|
|
wrote int
|
|
prefix string
|
|
keepZero bool // If set, we also write zero values.
|
|
docs bool // If set, we write comments.
|
|
}
|
|
|
|
func (w *writer) error(err error) {
|
|
panic(writeError{err})
|
|
}
|
|
|
|
func (w *writer) check(err error) {
|
|
if err != nil {
|
|
w.error(err)
|
|
}
|
|
}
|
|
|
|
func (w *writer) write(s string) {
|
|
n, err := w.out.WriteString(s)
|
|
w.check(err)
|
|
w.wrote += n
|
|
}
|
|
|
|
func (w *writer) flush() {
|
|
err := w.out.Flush()
|
|
w.check(err)
|
|
}
|
|
|
|
func (w *writer) indent() {
|
|
w.prefix += "\t"
|
|
}
|
|
|
|
func (w *writer) unindent() {
|
|
w.prefix = w.prefix[:len(w.prefix)-1]
|
|
}
|
|
|
|
func isOptional(sconfTag string) bool {
|
|
return hasTagWord(sconfTag, "optional")
|
|
}
|
|
|
|
func isIgnore(sconfTag string) bool {
|
|
return hasTagWord(sconfTag, "-") || hasTagWord(sconfTag, "ignore")
|
|
}
|
|
|
|
func hasTagWord(sconfTag, word string) bool {
|
|
l := strings.Split(sconfTag, ",")
|
|
for _, s := range l {
|
|
if s == word {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (w *writer) describeMap(v reflect.Value) {
|
|
t := v.Type()
|
|
if t.Key().Kind() != reflect.String {
|
|
w.error(fmt.Errorf("map key must be string"))
|
|
}
|
|
keys := v.MapKeys()
|
|
sort.Slice(keys, func(i, j int) bool {
|
|
return keys[i].String() < keys[j].String()
|
|
})
|
|
have := false
|
|
for _, k := range keys {
|
|
have = true
|
|
w.write(w.prefix)
|
|
w.write(k.String() + ":")
|
|
mv := v.MapIndex(k)
|
|
if !w.keepZero && mv.Kind() == reflect.Struct && isEmptyStruct(mv) {
|
|
w.write(" nil\n")
|
|
continue
|
|
}
|
|
w.describeValue(mv)
|
|
}
|
|
if have {
|
|
return
|
|
}
|
|
w.write(w.prefix)
|
|
w.write("x:")
|
|
w.describeValue(reflect.Zero(t.Elem()))
|
|
}
|
|
|
|
// whether v is a zero value of a struct type with all fields optional or
|
|
// ignored, causing it to write nothing when using Write.
|
|
func isEmptyStruct(v reflect.Value) bool {
|
|
if v.Kind() != reflect.Struct {
|
|
panic("not a struct")
|
|
}
|
|
t := v.Type()
|
|
n := t.NumField()
|
|
for i := 0; i < n; i++ {
|
|
ft := t.Field(i)
|
|
tag := ft.Tag.Get("sconf")
|
|
if !ft.IsExported() || isIgnore(tag) {
|
|
continue
|
|
}
|
|
if !isOptional(tag) {
|
|
return false
|
|
}
|
|
if !isZeroIgnored(v.Field(i)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// whether v is zero, taking ignored values into account.
|
|
func isZeroIgnored(v reflect.Value) bool {
|
|
switch v.Kind() {
|
|
case reflect.Slice, reflect.Map:
|
|
return v.Len() == 0
|
|
case reflect.Ptr:
|
|
return v.IsZero() || isZeroIgnored(v.Elem())
|
|
case reflect.Struct:
|
|
t := v.Type()
|
|
n := t.NumField()
|
|
for i := 0; i < n; i++ {
|
|
ft := t.Field(i)
|
|
tag := ft.Tag.Get("sconf")
|
|
if !ft.IsExported() || isIgnore(tag) {
|
|
continue
|
|
}
|
|
if !isZeroIgnored(v.Field(i)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
default:
|
|
return v.IsZero()
|
|
}
|
|
}
|
|
|
|
func (w *writer) describeStruct(v reflect.Value) {
|
|
t := v.Type()
|
|
n := t.NumField()
|
|
for i := 0; i < n; i++ {
|
|
f := t.Field(i)
|
|
fv := v.Field(i)
|
|
if !f.IsExported() || isIgnore(f.Tag.Get("sconf")) {
|
|
continue
|
|
}
|
|
if !w.keepZero && isOptional(f.Tag.Get("sconf")) && isZeroIgnored(fv) {
|
|
continue
|
|
}
|
|
if w.docs {
|
|
doc := f.Tag.Get("sconf-doc")
|
|
optional := isOptional(f.Tag.Get("sconf"))
|
|
if doc != "" || optional {
|
|
s := "\n"
|
|
if w.wrote == 0 {
|
|
// No empty line at start of file.
|
|
s = ""
|
|
}
|
|
// Treat two blank lines as section separator: the comments are not joined with
|
|
// lines with just "#", but instead with empty lines. To allow a hack where the
|
|
// first field of a config struct gives some context about the file.
|
|
sections := strings.Split(doc, "\n\n\n")
|
|
for si, section := range sections {
|
|
if si > 0 {
|
|
s += "\n\n\n"
|
|
}
|
|
for i, line := range strings.Split(section, "\n") {
|
|
if i > 0 {
|
|
s += "\n"
|
|
}
|
|
s += w.prefix + "#"
|
|
if line != "" {
|
|
s += " " + line
|
|
}
|
|
}
|
|
}
|
|
if optional {
|
|
opt := "(optional)"
|
|
if !strings.HasSuffix(doc, " ") {
|
|
s += " "
|
|
}
|
|
s += opt
|
|
}
|
|
s += "\n"
|
|
b := &strings.Builder{}
|
|
err := xfmt.Format(b, strings.NewReader(s), xfmt.Config{MaxWidth: 80})
|
|
w.check(err)
|
|
w.write(b.String())
|
|
}
|
|
}
|
|
w.write(w.prefix)
|
|
w.write(f.Name + ":")
|
|
w.describeValue(fv)
|
|
}
|
|
}
|
|
|
|
func (w *writer) describeValue(v reflect.Value) {
|
|
t := v.Type()
|
|
i := v.Interface()
|
|
|
|
if t == durationType {
|
|
w.write(fmt.Sprintf(" %s\n", i))
|
|
return
|
|
}
|
|
|
|
switch t.Kind() {
|
|
default:
|
|
w.error(fmt.Errorf("unsupported value %v", t.Kind()))
|
|
return
|
|
|
|
case reflect.Bool:
|
|
w.write(fmt.Sprintf(" %v\n", i))
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
w.write(fmt.Sprintf(" %d\n", i))
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
w.write(fmt.Sprintf(" %f\n", i))
|
|
|
|
case reflect.String:
|
|
if strings.Contains(v.String(), "\n") {
|
|
w.error(fmt.Errorf("unsupported multiline string"))
|
|
}
|
|
w.write(fmt.Sprintf(" %s\n", i))
|
|
|
|
case reflect.Slice:
|
|
w.write("\n")
|
|
w.indent()
|
|
w.describeSlice(v)
|
|
w.unindent()
|
|
|
|
case reflect.Ptr:
|
|
var pv reflect.Value
|
|
if v.IsNil() {
|
|
pv = reflect.New(t.Elem()).Elem()
|
|
} else {
|
|
pv = v.Elem()
|
|
}
|
|
w.describeValue(pv)
|
|
|
|
case reflect.Struct:
|
|
w.write("\n")
|
|
w.indent()
|
|
w.describeStruct(v)
|
|
w.unindent()
|
|
|
|
case reflect.Map:
|
|
w.write("\n")
|
|
w.indent()
|
|
w.describeMap(v)
|
|
w.unindent()
|
|
}
|
|
}
|
|
|
|
func (w *writer) describeSlice(v reflect.Value) {
|
|
describeElem := func(vv reflect.Value) {
|
|
w.write(w.prefix)
|
|
w.write("-")
|
|
w.describeValue(vv)
|
|
}
|
|
|
|
n := v.Len()
|
|
if n == 0 {
|
|
if w.keepZero {
|
|
describeElem(reflect.New(v.Type().Elem()))
|
|
} else {
|
|
w.error(errNoElem)
|
|
}
|
|
}
|
|
|
|
for i := 0; i < n; i++ {
|
|
describeElem(v.Index(i))
|
|
}
|
|
}
|