aboutsummaryrefslogtreecommitdiff
path: root/scfg/unmarshal.go
diff options
context:
space:
mode:
authorRunxi Yu <me@runxiyu.org>2024-12-29 18:40:22 +0000
committerRunxi Yu <me@runxiyu.org>2024-12-29 18:40:22 +0000
commitb6c8304364d1457fa7454da257f7ad3d71131b91 (patch)
treede85e19971aa6ac127abf35b80421ff8f6907727 /scfg/unmarshal.go
parentInitial commit (diff)
downloadgo-lindenii-common-b6c8304364d1457fa7454da257f7ad3d71131b91.tar.gz
go-lindenii-common-b6c8304364d1457fa7454da257f7ad3d71131b91.tar.zst
go-lindenii-common-b6c8304364d1457fa7454da257f7ad3d71131b91.zip
Add ~emersion/go-scfg as a subdirectory
Diffstat (limited to 'scfg/unmarshal.go')
-rw-r--r--scfg/unmarshal.go349
1 files changed, 349 insertions, 0 deletions
diff --git a/scfg/unmarshal.go b/scfg/unmarshal.go
new file mode 100644
index 0000000..a99910f
--- /dev/null
+++ b/scfg/unmarshal.go
@@ -0,0 +1,349 @@
+package scfg
+
+import (
+ "encoding"
+ "fmt"
+ "io"
+ "reflect"
+ "strconv"
+)
+
+// Decoder reads and decodes an scfg document from an input stream.
+type Decoder struct {
+ r io.Reader
+}
+
+// NewDecoder returns a new decoder which reads from r.
+func NewDecoder(r io.Reader) *Decoder {
+ return &Decoder{r}
+}
+
+// Decode reads scfg document from the input and stores it in the value pointed
+// to by v.
+//
+// If v is nil or not a pointer, Decode returns an error.
+//
+// Blocks can be unmarshaled to:
+//
+// - Maps. Each directive is unmarshaled into a map entry. The map key must
+// be a string.
+// - Structs. Each directive is unmarshaled into a struct field.
+//
+// Duplicate directives are not allowed, unless the struct field or map value
+// is a slice of values representing a directive: structs or maps.
+//
+// Directives can be unmarshaled to:
+//
+// - Maps. The children block is unmarshaled into the map. Parameters are not
+// allowed.
+// - Structs. The children block is unmarshaled into the struct. Parameters
+// are allowed if one of the struct fields contains the "param" option in
+// its tag.
+// - Slices. Parameters are unmarshaled into the slice. Children blocks are
+// not allowed.
+// - Arrays. Parameters are unmarshaled into the array. The number of
+// parameters must match exactly the length of the array. Children blocks
+// are not allowed.
+// - Strings, booleans, integers, floating-point values, values implementing
+// encoding.TextUnmarshaler. Only a single parameter is allowed and is
+// unmarshaled into the value. Children blocks are not allowed.
+//
+// The decoding of each struct field can be customized by the format string
+// stored under the "scfg" key in the struct field's tag. The tag contains the
+// name of the field possibly followed by a comma-separated list of options.
+// The name may be empty in order to specify options without overriding the
+// default field name. As a special case, if the field name is "-", the field
+// is ignored. The "param" option specifies that directive parameters are
+// stored in this field (the name must be empty).
+func (dec *Decoder) Decode(v interface{}) error {
+ block, err := Read(dec.r)
+ if err != nil {
+ return err
+ }
+
+ rv := reflect.ValueOf(v)
+ if rv.Kind() != reflect.Ptr || rv.IsNil() {
+ return fmt.Errorf("scfg: invalid value for unmarshaling")
+ }
+
+ return unmarshalBlock(block, rv)
+}
+
+func unmarshalBlock(block Block, v reflect.Value) error {
+ v = unwrapPointers(v)
+ t := v.Type()
+
+ dirsByName := make(map[string][]*Directive, len(block))
+ for _, dir := range block {
+ dirsByName[dir.Name] = append(dirsByName[dir.Name], dir)
+ }
+
+ switch v.Kind() {
+ case reflect.Map:
+ if t.Key().Kind() != reflect.String {
+ return fmt.Errorf("scfg: map key type must be string")
+ }
+ if v.IsNil() {
+ v.Set(reflect.MakeMap(t))
+ } else if v.Len() > 0 {
+ clearMap(v)
+ }
+
+ for name, dirs := range dirsByName {
+ mv := reflect.New(t.Elem()).Elem()
+ if err := unmarshalDirectiveList(dirs, mv); err != nil {
+ return err
+ }
+ v.SetMapIndex(reflect.ValueOf(name), mv)
+ }
+ case reflect.Struct:
+ si, err := getStructInfo(t)
+ if err != nil {
+ return err
+ }
+
+ for name, dirs := range dirsByName {
+ fieldIndex, ok := si.children[name]
+ if !ok {
+ return newUnmarshalDirectiveError(dirs[0], "unknown directive")
+ }
+ fv := v.Field(fieldIndex)
+ if err := unmarshalDirectiveList(dirs, fv); err != nil {
+ return err
+ }
+ }
+ default:
+ return fmt.Errorf("scfg: unsupported type for unmarshaling blocks: %v", t)
+ }
+
+ return nil
+}
+
+func unmarshalDirectiveList(dirs []*Directive, v reflect.Value) error {
+ v = unwrapPointers(v)
+ t := v.Type()
+
+ if v.Kind() != reflect.Slice || !isDirectiveType(t.Elem()) {
+ if len(dirs) > 1 {
+ return newUnmarshalDirectiveError(dirs[1], "directive must not be specified more than once")
+ }
+ return unmarshalDirective(dirs[0], v)
+ }
+
+ sv := reflect.MakeSlice(t, len(dirs), len(dirs))
+ for i, dir := range dirs {
+ if err := unmarshalDirective(dir, sv.Index(i)); err != nil {
+ return err
+ }
+ }
+ v.Set(sv)
+ return nil
+}
+
+// isDirectiveType checks whether a type can only be unmarshaled as a
+// directive, not as a parameter. Accepting too many types here would result in
+// ambiguities, see:
+// https://lists.sr.ht/~emersion/public-inbox/%3C20230629132458.152205-1-contact%40emersion.fr%3E#%3Ch4Y2peS_YBqY3ar4XlmPDPiNBFpYGns3EBYUx3_6zWEhV2o8_-fBQveRujGADWYhVVCucHBEryFGoPtpC3d3mQ-x10pWnFogfprbQTSvtxc=@emersion.fr%3E
+func isDirectiveType(t reflect.Type) bool {
+ for t.Kind() == reflect.Ptr {
+ t = t.Elem()
+ }
+
+ textUnmarshalerType := reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
+ if reflect.PtrTo(t).Implements(textUnmarshalerType) {
+ return false
+ }
+
+ switch t.Kind() {
+ case reflect.Struct, reflect.Map:
+ return true
+ default:
+ return false
+ }
+}
+
+func unmarshalDirective(dir *Directive, v reflect.Value) error {
+ v = unwrapPointers(v)
+ t := v.Type()
+
+ if v.CanAddr() {
+ if _, ok := v.Addr().Interface().(encoding.TextUnmarshaler); ok {
+ if len(dir.Children) != 0 {
+ return newUnmarshalDirectiveError(dir, "directive requires zero children")
+ }
+ return unmarshalParamList(dir, v)
+ }
+ }
+
+ switch v.Kind() {
+ case reflect.Map:
+ if len(dir.Params) > 0 {
+ return newUnmarshalDirectiveError(dir, "directive requires zero parameters")
+ }
+ if err := unmarshalBlock(dir.Children, v); err != nil {
+ return err
+ }
+ case reflect.Struct:
+ si, err := getStructInfo(t)
+ if err != nil {
+ return err
+ }
+
+ if si.param >= 0 {
+ if err := unmarshalParamList(dir, v.Field(si.param)); err != nil {
+ return err
+ }
+ } else {
+ if len(dir.Params) > 0 {
+ return newUnmarshalDirectiveError(dir, "directive requires zero parameters")
+ }
+ }
+
+ if err := unmarshalBlock(dir.Children, v); err != nil {
+ return err
+ }
+ default:
+ if len(dir.Children) != 0 {
+ return newUnmarshalDirectiveError(dir, "directive requires zero children")
+ }
+ if err := unmarshalParamList(dir, v); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func unmarshalParamList(dir *Directive, v reflect.Value) error {
+ switch v.Kind() {
+ case reflect.Slice:
+ t := v.Type()
+ sv := reflect.MakeSlice(t, len(dir.Params), len(dir.Params))
+ for i, param := range dir.Params {
+ if err := unmarshalParam(param, sv.Index(i)); err != nil {
+ return newUnmarshalParamError(dir, i, err)
+ }
+ }
+ v.Set(sv)
+ case reflect.Array:
+ if len(dir.Params) != v.Len() {
+ return newUnmarshalDirectiveError(dir, fmt.Sprintf("directive requires exactly %v parameters", v.Len()))
+ }
+ for i, param := range dir.Params {
+ if err := unmarshalParam(param, v.Index(i)); err != nil {
+ return newUnmarshalParamError(dir, i, err)
+ }
+ }
+ default:
+ if len(dir.Params) != 1 {
+ return newUnmarshalDirectiveError(dir, "directive requires exactly one parameter")
+ }
+ if err := unmarshalParam(dir.Params[0], v); err != nil {
+ return newUnmarshalParamError(dir, 0, err)
+ }
+ }
+
+ return nil
+}
+
+func unmarshalParam(param string, v reflect.Value) error {
+ v = unwrapPointers(v)
+ t := v.Type()
+
+ // TODO: improve our logic following:
+ // https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/encoding/json/decode.go;drc=b9b8cecbfc72168ca03ad586cc2ed52b0e8db409;l=421
+ if v.CanAddr() {
+ if v, ok := v.Addr().Interface().(encoding.TextUnmarshaler); ok {
+ return v.UnmarshalText([]byte(param))
+ }
+ }
+
+ switch v.Kind() {
+ case reflect.String:
+ v.Set(reflect.ValueOf(param))
+ case reflect.Bool:
+ switch param {
+ case "true":
+ v.Set(reflect.ValueOf(true))
+ case "false":
+ v.Set(reflect.ValueOf(false))
+ default:
+ return fmt.Errorf("invalid bool parameter %q", param)
+ }
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ i, err := strconv.ParseInt(param, 10, t.Bits())
+ if err != nil {
+ return fmt.Errorf("invalid %v parameter: %v", t, err)
+ }
+ v.Set(reflect.ValueOf(i).Convert(t))
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ u, err := strconv.ParseUint(param, 10, t.Bits())
+ if err != nil {
+ return fmt.Errorf("invalid %v parameter: %v", t, err)
+ }
+ v.Set(reflect.ValueOf(u).Convert(t))
+ case reflect.Float32, reflect.Float64:
+ f, err := strconv.ParseFloat(param, t.Bits())
+ if err != nil {
+ return fmt.Errorf("invalid %v parameter: %v", t, err)
+ }
+ v.Set(reflect.ValueOf(f).Convert(t))
+ default:
+ return fmt.Errorf("unsupported type for unmarshaling parameter: %v", t)
+ }
+
+ return nil
+}
+
+func unwrapPointers(v reflect.Value) reflect.Value {
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ v.Set(reflect.New(v.Type().Elem()))
+ }
+ v = v.Elem()
+ }
+ return v
+}
+
+func clearMap(v reflect.Value) {
+ for _, k := range v.MapKeys() {
+ v.SetMapIndex(k, reflect.Value{})
+ }
+}
+
+type unmarshalDirectiveError struct {
+ lineno int
+ name string
+ msg string
+}
+
+func newUnmarshalDirectiveError(dir *Directive, msg string) *unmarshalDirectiveError {
+ return &unmarshalDirectiveError{
+ name: dir.Name,
+ lineno: dir.lineno,
+ msg: msg,
+ }
+}
+
+func (err *unmarshalDirectiveError) Error() string {
+ return fmt.Sprintf("line %v, directive %q: %v", err.lineno, err.name, err.msg)
+}
+
+type unmarshalParamError struct {
+ lineno int
+ directive string
+ paramIndex int
+ err error
+}
+
+func newUnmarshalParamError(dir *Directive, paramIndex int, err error) *unmarshalParamError {
+ return &unmarshalParamError{
+ directive: dir.Name,
+ lineno: dir.lineno,
+ paramIndex: paramIndex,
+ err: err,
+ }
+}
+
+func (err *unmarshalParamError) Error() string {
+ return fmt.Sprintf("line %v, directive %q, parameter %v: %v", err.lineno, err.directive, err.paramIndex+1, err.err)
+}