package walk import ( "io" "encoding/json" "fmt" "strings" "math" "unicode/utf8" ) // int or string type PathSegment interface {} type Path []PathSegment func (path Path) ToWalkValues() []WalkValue { var values []WalkValue for _, segment := range path { switch s := segment.(type) { case int: values = append(values, ValueNumber(s)) case string: values = append(values, ValueString(s)) default: panic("Invalid PathSegment") } } return values } func PathFromWalkValues(values []WalkValue) Path { var segments []PathSegment for _, value := range values { switch v := value.(type) { case ValueNumber: segments = append(segments, int(math.Round(float64(v)))) case ValueString: segments = append(segments, string(v)) default: panic("Invalid value in path") } } return segments } type TerminalValue int const ( ArrayBegin TerminalValue = iota ArrayEnd MapBegin MapEnd ) func (value TerminalValue) Atomise(in []Atom) []Atom { return append(in, NewAtomTerminal(value)) } func (value TerminalValue) String() string { switch value { case ArrayBegin: return "[" case ArrayEnd: return "]" case MapBegin: return "{" case MapEnd: return "}" default: panic("Unknown TerminalValue") } } type ValueNull struct {} func (value ValueNull) Atomise(in []Atom) []Atom { return append(in, NewAtomNull()) } func (value ValueNull) String() string { return "null" } type ValueBool bool func (value ValueBool) Atomise(in []Atom) []Atom { return append(in, NewAtomBool(bool(value))) } func (value ValueBool) String() string { if value { return "true" } else { return "false" } } type ValueNumber float64 func (value ValueNumber) Atomise(in []Atom) []Atom { return append(in, NewAtomNumber(float64(value))) } func (value ValueNumber) String() string { v := float64(value) return fmt.Sprintf("%f", v) } type ValueString string func (value ValueString) Atomise(in []Atom) []Atom { in = append(in, NewAtomStringTerminal()) for _, char := range value { in = append(in, NewAtomStringRune(char)) } in = append(in, NewAtomStringTerminal()) return in } func (value ValueString) String() string { return fmt.Sprintf("\"%s\"", string(value)) } type AtomType int64 const ( AtomNull AtomType = iota AtomBool AtomNumber AtomTerminal AtomStringTerminal AtomStringRune ) type Atom struct { Typ AtomType data uint64 } func NewAtomNull() Atom { return Atom { Typ: AtomNull, data: 0, } } func NewAtomBool(v bool) Atom { if v { return Atom { Typ: AtomBool, data: 1, } } else { return Atom { Typ: AtomBool, data: 0, } } } func NewAtomNumber(v float64) Atom { return Atom { Typ: AtomNumber, data: math.Float64bits(v), } } func NewAtomTerminal(v TerminalValue) Atom { return Atom { Typ: AtomTerminal, data: uint64(v), } } func NewAtomStringTerminal() Atom { return Atom { Typ: AtomStringTerminal, data: 0, } } func NewAtomStringRune(v rune) Atom { return Atom { Typ: AtomStringRune, data: uint64(v), } } func (v Atom) String() string { switch v.Typ { case AtomNull: return "null" case AtomBool: if v.data == 0 { return "false" } return "true" case AtomNumber: return fmt.Sprintf("%v", math.Float64frombits(v.data)) case AtomTerminal: switch TerminalValue(v.data) { case MapBegin: return "{" case MapEnd: return "}" case ArrayBegin: return "[" case ArrayEnd: return "]" default: panic("Invalid terminal atom") } case AtomStringTerminal: return "\"" case AtomStringRune: return string(rune(v.data)) default: panic("Invalid atom type") } } type WalkValue interface { // Append this values atoms to the input Atomise(in []Atom) []Atom String() string } type WalkItem struct { Value WalkValue Path Path } type WalkItemStream struct { channel chan WalkItem rewinds []WalkItem } func (stream *WalkItemStream) next() (WalkItem, bool) { if len(stream.rewinds) == 0 { item, hasItem := <- stream.channel return item, hasItem } item := stream.rewinds[len(stream.rewinds)-1] stream.rewinds = stream.rewinds[0:len(stream.rewinds)-1] return item, true } func (stream *WalkItemStream) rewind(item WalkItem) { stream.rewinds = append(stream.rewinds, item) } func (stream *WalkItemStream) peek() (WalkItem, bool) { item, hasItem := stream.next() if !hasItem { return item, false } stream.rewind(item) return item, true } func tokenToValue(token json.Token) WalkValue { switch token.(type) { case nil: return ValueNull {} case bool: return ValueBool(token.(bool)) case float64: return ValueNumber(token.(float64)) case string: return ValueString(token.(string)) default: panic("Can't convert JSON token to value") } } func readValue(dec *json.Decoder, path Path, out chan WalkItem) bool { if !dec.More() { return true } t, err := dec.Token() if err == io.EOF { return true } else if err != nil { panic("Invalid JSON") } switch t.(type) { case nil, string, float64, bool: v := tokenToValue(t) out <- WalkItem {v, path} return false case json.Delim: switch rune(t.(json.Delim)) { case '[': out <- WalkItem {ArrayBegin, path} index := 0 for dec.More() { empty := readValue(dec, append(path, index), out) if empty { break } index += 1 } t, err := dec.Token() if err != nil { panic("Invalid JSON") } delim, isDelim := t.(json.Delim) if !isDelim || delim != ']' { panic("Expected ] in JSON") } out <- WalkItem{ArrayEnd, path} return false case '{': out <- WalkItem {MapBegin, path} for dec.More() { t, _ := dec.Token() key, keyIsString := t.(string) if !keyIsString { panic("Invalid JSON") } empty := readValue(dec, append(path, key), out) if empty { panic("Invalid JSON") } } t, err := dec.Token() if err != nil { panic("Invalid JSON") } delim, isDelim := t.(json.Delim) if !isDelim || delim != '}' { panic("Expected } in JSON") } out <- WalkItem {MapEnd, path} return false default: panic("Error parsing JSON") } default: panic("Invalid JSON token") } } func startWalk(dec *json.Decoder, out chan WalkItem) { isEmpty := readValue(dec, nil, out) if isEmpty { panic("Missing JSON input") } close(out) } func Json(r io.Reader) chan WalkItem { dec := json.NewDecoder(r) out := make(chan WalkItem) go startWalk(dec, out) return out } func printIndent(indent int) { for i := 0; i < indent; i += 1 { fmt.Print("\t") } } func jsonOutArray(in *WalkItemStream, indent int) { fmt.Println("[") token, hasToken := in.next() if !hasToken { panic("Missing ] in output JSON") } terminal, isTerminal := token.Value.(TerminalValue) if isTerminal && terminal == ArrayEnd { fmt.Print("\n") printIndent(indent) fmt.Print("]") return } in.rewind(token) for { valueToken := jsonOutValue(in, indent + 1, true) if valueToken != nil { panic("Missing value in output JSON array") } token, hasToken := in.next() if !hasToken { panic("Missing ] in output JSON") } terminal, isTerminal := token.Value.(TerminalValue) if isTerminal && terminal == ArrayEnd { fmt.Print("\n") printIndent(indent) fmt.Print("]") return } in.rewind(token) fmt.Println(",") } } func jsonOutMap(in *WalkItemStream, indent int) { fmt.Println("{") token, hasToken := in.next() if !hasToken { panic("Missing } in output JSON") } terminal, isTerminal := token.Value.(TerminalValue) if isTerminal && terminal == MapEnd { fmt.Print("\n") printIndent(indent) fmt.Print("}") return } in.rewind(token) for { keyToken, hasKeyToken := in.peek() if !hasKeyToken { panic("Missing map element") } printIndent(indent + 1) if len(keyToken.Path) == 0 { panic("Map element missing key") } key := keyToken.Path[len(keyToken.Path)-1] switch key.(type) { case int: fmt.Print(key.(int)) case string: fmt.Printf("%q", key.(string)) default: panic("Invalid path segment") } fmt.Print(": ") valueToken := jsonOutValue(in, indent + 1, false) if valueToken != nil { panic("Missing value int output JSON map") } token, hasToken := in.next() if !hasToken { panic("Missing } in output JSON") } terminal, isTerminal := token.Value.(TerminalValue) if isTerminal && terminal == MapEnd { fmt.Print("\n") printIndent(indent) fmt.Print("}") return } in.rewind(token) fmt.Println(",") } } func jsonOutValue(in *WalkItemStream, indent int, doIndent bool) WalkValue { token, hasToken := in.next() if !hasToken { panic("Missing JSON token in output") } switch v := token.Value.(type) { case ValueNull: if doIndent { printIndent(indent) } fmt.Printf("null") return nil case ValueBool: if doIndent { printIndent(indent) } if token.Value.(ValueBool) { fmt.Print("true") } else { fmt.Print("false") } return nil case ValueNumber: if doIndent { printIndent(indent) } fmt.Printf("%v", token.Value) return nil case ValueString: if doIndent { printIndent(indent) } fmt.Printf("%q", string(v)) return nil case TerminalValue: switch token.Value.(TerminalValue) { case ArrayBegin: if doIndent { printIndent(indent) } jsonOutArray(in, indent) return nil case MapBegin: if doIndent { printIndent(indent) } jsonOutMap(in, indent) return nil default: return token.Value } default: panic("Invalid WalkValue") } } func JsonOut(in chan WalkItem) { stream := WalkItemStream { channel: in, rewinds: nil, } if jsonOutValue(&stream, 0, true) != nil { panic("Invalid output JSON") } fmt.Print("\n") } func ConcatData(first []Atom, second []Atom) []Atom { res := make([]Atom, 0, len(first) + len(second)) res = append(res, first...) res = append(res, second...) return res } func Atomise(in []WalkValue) (out []Atom) { numAtoms := 0 for _, value := range in { switch v := value.(type) { case TerminalValue, ValueNull, ValueBool, ValueNumber: numAtoms++ case ValueString: numAtoms += utf8.RuneCountInString(string(v)) + 2 default: panic("Invalid WalkValue") } } out = make([]Atom, 0, numAtoms) for _, value := range in { out = value.Atomise(out) } return out } type CompoundError int const ( CompoundRuneOutsideString CompoundError = iota CompoundUnknownAtom CompoundMissingEnd CompoundInvalidStringAtom ) func (err CompoundError) Error() string { switch err { case CompoundRuneOutsideString: return "Compound Error: Rune Outside String" case CompoundUnknownAtom: return "Compound Error: Unknown Atom" case CompoundMissingEnd: return "Compound Error: Missing End" case CompoundInvalidStringAtom: return "Compound Error: Invalid String Atom" default: panic("Invalid CompoundError") } } type CompoundResult struct { value WalkValue error error } func Compound(in []Atom) (out []WalkValue, error error) { numValues := 0 i := 0 inString := false for _, atom := range in { switch atom.Typ { case AtomNull, AtomBool, AtomNumber, AtomTerminal: if !inString { numValues++ } case AtomStringTerminal: if inString { numValues++ } inString = !inString } } i = 0 out = make([]WalkValue, 0, numValues) for { if i >= len(in) { break } atom := in[i] i++ switch atom.Typ { case AtomNull: out = append(out, ValueNull{}) continue case AtomBool: out = append(out, ValueBool(atom.data != 0)) continue case AtomNumber: out = append(out, ValueNumber(math.Float64frombits(atom.data))) continue case AtomTerminal: out = append(out, TerminalValue(atom.data)) continue case AtomStringRune: return nil, CompoundRuneOutsideString case AtomStringTerminal: default: return nil, CompoundUnknownAtom } // Handle string start var builder strings.Builder for { if i >= len(in) { return nil, CompoundMissingEnd } atom := in[i] i++ if atom.Typ == AtomStringTerminal { break } builder.WriteString(atom.String()) } out = append(out, ValueString(builder.String())) } return out, nil }