feat: add OrderedMap, the table that keeps its key order
Test / test (push) Successful in 1m30s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-22 00:15:17 +02:00
parent aefff80a28
commit eaa69dc6f6
8 changed files with 567 additions and 8 deletions
+98 -3
View File
@@ -184,6 +184,13 @@ func (e *encoder) encode(v any) error {
return fmt.Errorf("interpres: cannot marshal a Document; marshal its Map() to write the values")
case Document:
return fmt.Errorf("interpres: cannot marshal a Document; marshal its Map() to write the values")
case OrderedMap:
return e.encodeOrderedMap(&x)
case *OrderedMap:
if x == nil {
return fmt.Errorf("interpres: cannot marshal nil value")
}
return e.encodeOrderedMap(x)
}
rv := reflect.ValueOf(v)
if !rv.IsValid() {
@@ -212,6 +219,63 @@ func (e *encoder) encode(v any) error {
return e.emitDoc(doc, nil)
}
// encodeOrderedMap emits an OrderedMap as a document, its keys in the order
// they were set, which is the reason the type exists.
func (e *encoder) encodeOrderedMap(om *OrderedMap) error {
if err := e.checkCtx(); err != nil {
return err
}
doc := &tomlDoc{ctx: e.ctx, opts: e.opts}
if err := buildOrderedDoc(om, doc, encPath{}); err != nil {
return err
}
return e.emitDoc(doc, nil)
}
// buildOrderedDoc adds the entries of om to doc in the order its keys were
// set. Nested OrderedMaps recurse; every other value goes through addField,
// which resolves Marshaler, text and the ordinary kinds the same way it does
// for a struct field.
func buildOrderedDoc(om *OrderedMap, doc *tomlDoc, path encPath) error {
if cap(doc.entries) == 0 {
doc.entries = make([]entry, 0, om.Len())
}
for _, key := range om.Keys() {
if err := doc.checkCtx(); err != nil {
return err
}
val := om.values[key]
switch x := val.(type) {
case OrderedMap:
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts}
if err := buildOrderedDoc(&x, sub, path.key(key)); err != nil {
return err
}
doc.addTable(key, sub)
continue
case *OrderedMap:
if x == nil {
continue
}
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts}
if err := buildOrderedDoc(x, sub, path.key(key)); err != nil {
return err
}
doc.addTable(key, sub)
continue
}
rv := reflect.ValueOf(val)
if !rv.IsValid() {
// A nil value has no TOML form, the rule nil pointer fields follow.
continue
}
if err := addField(doc, key, rv, path); err != nil {
return err
}
}
return nil
}
// --- intermediate representation -----------------------------------------
// entryKind discriminates the three forms an entry in a tomlDoc may take.
@@ -518,6 +582,15 @@ func addField(doc *tomlDoc, name string, v reflect.Value, path encPath) error {
}
v = v.Elem()
}
if v.Type() == orderedMapType {
om := v.Interface().(OrderedMap)
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts}
if err := buildOrderedDoc(&om, sub, path.key(name)); err != nil {
return err
}
doc.addTable(name, sub)
return nil
}
switch v.Kind() {
case reflect.Struct:
if isScalarStruct(v.Type()) {
@@ -620,15 +693,20 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, path encPath) err
}
}
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts}
switch ev.Kind() {
case reflect.Struct:
switch {
case ev.Type() == orderedMapType:
om := ev.Interface().(OrderedMap)
if err := buildOrderedDoc(&om, sub, apath.elem(i)); err != nil {
return err
}
case ev.Kind() == reflect.Struct:
if isScalarStruct(ev.Type()) {
return &EncodeError{Path: apath.elem(i).String(), Err: errors.New("heterogeneous array contains scalar")}
}
if err := buildStructDoc(ev, sub, apath.elem(i)); err != nil {
return err
}
case reflect.Map:
case ev.Kind() == reflect.Map:
if err := buildMapDoc(ev, sub, apath.elem(i)); err != nil {
return err
}
@@ -717,6 +795,9 @@ func resolveElement(v reflect.Value, path encPath) (reflect.Value, error) {
// nested-array representations the emitter understands. Slices and arrays are
// recursively normalised so that nested arrays (e.g. [][]int) work.
func normaliseValue(v reflect.Value) (any, error) {
if !v.IsValid() {
return nil, fmt.Errorf("cannot encode nil value")
}
// Map and slice elements arrive wrapped in interface{}; look through them.
for v.Kind() == reflect.Interface && !v.IsNil() {
v = v.Elem()
@@ -755,6 +836,20 @@ func normaliseValue(v reflect.Value) (any, error) {
if v.Type() == numberType {
return Number(v.String()), nil
}
// An OrderedMap in a value array has no header form, so it renders as an
// ordinary inline table, whose keys come out sorted.
if v.Type() == orderedMapType {
om := v.Interface().(OrderedMap)
out := make(map[string]any, om.Len())
for _, k := range om.Keys() {
val, err := normaliseValue(reflect.ValueOf(om.values[k]))
if err != nil {
return nil, fmt.Errorf("[%s]: %w", k, err)
}
out[k] = val
}
return out, nil
}
// A type that renders itself as text becomes a TOML string, scalar kinds
// and structs alike.
s, isText, err := textValue(v)