ความคืบหน้างาน ไฟล์ต้นฉบับ

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
}