Files
interpres/orderedmap.go
T
petrbalvin eaa69dc6f6
Test / test (push) Successful in 1m30s
feat: add OrderedMap, the table that keeps its key order
Assisted-by: GLM 5.3 Flash
2026-09-22 00:15:17 +02:00

200 lines
5.8 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package interpres
import (
"fmt"
"maps"
"reflect"
"slices"
"sync"
)
// An OrderedMap is a string-keyed table that remembers the order its keys
// were set in, the shape a map[string]any cannot carry. Marshal writes a
// table of its own kind in that order, and decoding a document into one
// fills it in the order the document wrote the keys, where a map
// destination carries no order at all. The values are untyped, the shape
// the parser produces, so a nested table inside an OrderedMap is a plain
// map[string]any; the order is kept at the level the OrderedMap sits at.
//
// The zero value is an empty table ready for use.
type OrderedMap struct {
keys []string
values map[string]any
}
var orderedMapType = reflect.TypeFor[OrderedMap]()
// NewOrderedMap returns an empty OrderedMap.
func NewOrderedMap() *OrderedMap { return &OrderedMap{} }
// Set stores value under key. A key the table already has keeps its position
// and takes the new value; a new one joins the end.
func (m *OrderedMap) Set(key string, value any) {
if m.values == nil {
m.values = make(map[string]any, 4)
}
if _, ok := m.values[key]; !ok {
m.keys = append(m.keys, key)
}
m.values[key] = value
}
// Get returns the value under key, and whether the table has one.
func (m *OrderedMap) Get(key string) (any, bool) {
v, ok := m.values[key]
return v, ok
}
// Delete removes key. A later Set of the same key appends it to the end
// again.
func (m *OrderedMap) Delete(key string) {
if _, ok := m.values[key]; !ok {
return
}
delete(m.values, key)
m.keys = slices.DeleteFunc(m.keys, func(k string) bool { return k == key })
}
// Keys returns the keys in the order they were set.
func (m *OrderedMap) Keys() []string { return m.keys }
// Len returns the number of keys.
func (m *OrderedMap) Len() int { return len(m.keys) }
// Range calls f for every key in order, stopping when f returns false.
func (m *OrderedMap) Range(f func(key string, value any) bool) {
for _, k := range m.keys {
if !f(k, m.values[k]) {
return
}
}
}
// Map returns the values as a plain map, which carries no order. It is the
// view Marshal's Document-free callers need.
func (m *OrderedMap) Map() map[string]any { return m.values }
// --- decode: the order the document wrote ----------------------------------
// wantsOrderCache holds whether a destination type mentions OrderedMap
// anywhere a decode can reach. One computed answer per type, the same
// trade-off structSchemaCache makes.
var wantsOrderCache sync.Map // reflect.Type -> bool
// typeWantsOrder reports whether decoding into t can reach an OrderedMap, in
// which case the parse has to build the node tree the key order is read
// from. Structs walk their exported fields, and pointers, slices, arrays and
// maps walk their element; anything else holds no OrderedMap.
func typeWantsOrder(t reflect.Type) bool {
if t == nil {
return false
}
if v, ok := wantsOrderCache.Load(t); ok {
return v.(bool)
}
r := scanWantsOrder(t, make(map[reflect.Type]bool))
v, _ := wantsOrderCache.LoadOrStore(t, r)
return v.(bool)
}
func scanWantsOrder(t reflect.Type, seen map[reflect.Type]bool) bool {
for {
if t == orderedMapType {
return true
}
if seen[t] {
return false
}
seen[t] = true
switch t.Kind() {
case reflect.Pointer, reflect.Slice, reflect.Array, reflect.Map:
t = t.Elem()
case reflect.Struct:
for f := range t.Fields() {
if f.PkgPath != "" {
continue
}
if scanWantsOrder(f.Type, seen) {
return true
}
}
return false
default:
return false
}
}
}
// nodes maps a table's value map to its node, the index the decoder reads
// the written key order from. The key is the map header's runtime pointer,
// the one identity a map value offers; the nodes share their maps with the
// value tree, so one lookup per table is exact.
type nodeIndex map[uintptr]*Table
// indexNodeIndex walks a document's node tree into an index. A nil tree
// gives a nil index, which every lookup answers with nil.
func indexNodes(t *Table) nodeIndex {
if t == nil {
return nil
}
idx := nodeIndex{}
var walk func(t *Table)
walk = func(t *Table) {
idx[reflect.ValueOf(t.values).Pointer()] = t
for _, e := range t.entries {
if e.child != nil {
walk(e.child)
}
// The elements of a value array carry a node only where an element
// is an inline table; the rest are nil.
for _, el := range e.elements {
if el != nil {
walk(el)
}
}
}
}
walk(t)
return idx
}
// nodeOf returns the node a value table was parsed into, or nil when the
// parse built no node tree, which is the ordinary decode's shape. A tree
// built by hand carries no nodes either.
func (d *decoder) nodeOf(tbl map[string]any) *Table {
return d.nodes[reflect.ValueOf(tbl).Pointer()]
}
// fillOrderedMap decodes a parsed table into an OrderedMap destination,
// taking the keys in the order the document wrote them. A table with no
// node, which is what a hand-built tree or a ParseMap result offers, fills
// in sorted key order, the deterministic order a map can offer.
func (d *decoder) fillOrderedMap(tbl map[string]any, dst reflect.Value) error {
if !dst.CanAddr() {
return fmt.Errorf("interpres: cannot decode into an OrderedMap that is not addressable")
}
om := dst.Addr().Interface().(*OrderedMap)
if om.values == nil {
om.values = make(map[string]any, len(tbl))
}
keys := slices.Sorted(maps.Keys(tbl))
if node := d.nodeOf(tbl); node != nil {
keys = node.Keys()
}
for _, key := range keys {
val, ok := tbl[key]
if !ok {
continue
}
elem := reflect.New(reflect.TypeFor[any]()).Elem()
if err := d.assign(val, elem); err != nil {
return newDecodeError(key, err)
}
om.Set(key, elem.Interface())
}
return nil
}