tracker/ojson.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
)
// Ordered JSON: a minimal document model that keeps key order and unknown
// fields, so the server can change a few values in milestones.json and write
// the file back with a small, readable git diff.
type kv struct {
Key string
Val any
}
// object is a JSON object with its keys in file order.
type object []kv
func (o *object) get(key string) (any, bool) {
for _, p := range *o {
if p.Key == key {
return p.Val, true
}
}
return nil, false
}
// set replaces a value in place, or appends the key at the end.
func (o *object) set(key string, val any) {
for i := range *o {
if (*o)[i].Key == key {
(*o)[i].Val = val
return
}
}
*o = append(*o, kv{key, val})
}
func (o *object) getString(key string) string {
v, _ := o.get(key)
s, _ := v.(string)
return s
}
func parseOrdered(data []byte) (any, error) {
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
v, err := readValue(dec)
if err != nil {
return nil, err
}
if _, err := dec.Token(); err != io.EOF {
return nil, fmt.Errorf("unexpected data after JSON value")
}
return v, nil
}
func readValue(dec *json.Decoder) (any, error) {
tok, err := dec.Token()
if err != nil {
return nil, err
}
switch t := tok.(type) {
case json.Delim:
switch t {
case '{':
obj := &object{}
for dec.More() {
k, err := dec.Token()
if err != nil {
return nil, err
}
v, err := readValue(dec)
if err != nil {
return nil, err
}
*obj = append(*obj, kv{k.(string), v})
}
_, err := dec.Token() // }
return obj, err
case '[':
arr := []any{}
for dec.More() {
v, err := readValue(dec)
if err != nil {
return nil, err
}
arr = append(arr, v)
}
_, err := dec.Token() // ]
return arr, err
}
return nil, fmt.Errorf("unexpected delimiter %v", t)
default:
return t, nil // string, json.Number, bool or nil
}
}
// writeOrdered formats like the rest of the data files: two-space indent,
// one item per line, and Thai text and <, >, & left unescaped.
func writeOrdered(v any) ([]byte, error) {
var b bytes.Buffer
if err := writeValue(&b, v, 0); err != nil {
return nil, err
}
b.WriteByte('\n')
return b.Bytes(), nil
}
func writeValue(b *bytes.Buffer, v any, depth int) error {
indent := func(d int) {
b.WriteByte('\n')
for range d {
b.WriteString(" ")
}
}
switch t := v.(type) {
case *object:
if len(*t) == 0 {
b.WriteString("{}")
return nil
}
b.WriteByte('{')
for i, p := range *t {
if i > 0 {
b.WriteByte(',')
}
indent(depth + 1)
writeScalar(b, p.Key)
b.WriteString(": ")
if err := writeValue(b, p.Val, depth+1); err != nil {
return err
}
}
indent(depth)
b.WriteByte('}')
case []any:
if len(t) == 0 {
b.WriteString("[]")
return nil
}
b.WriteByte('[')
for i, item := range t {
if i > 0 {
b.WriteByte(',')
}
indent(depth + 1)
if err := writeValue(b, item, depth+1); err != nil {
return err
}
}
indent(depth)
b.WriteByte(']')
default:
return writeScalar(b, t)
}
return nil
}
func writeScalar(b *bytes.Buffer, v any) error {
var tmp bytes.Buffer
enc := json.NewEncoder(&tmp)
enc.SetEscapeHTML(false)
if err := enc.Encode(v); err != nil {
return err
}
b.Write(bytes.TrimRight(tmp.Bytes(), "\n"))
return nil
}