mox/vendor/github.com/mjl-/sconf/describe.go
Mechiel Lukkien cb229cb6cf
mox!
2023-01-30 14:27:06 +01:00

264 lines
4.9 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
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) {
_, err := w.out.WriteString(s)
w.check(err)
}
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 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 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 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" + w.prefix + "# " + doc
if optional {
opt := "(optional)"
if doc != "" {
opt = " " + opt
}
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))
}
}