From 3bd45dc49a35b82dcc4ae93796c3e152d799bc0b Mon Sep 17 00:00:00 2001 From: Charlie Stanton Date: Sun, 19 Feb 2023 09:27:55 +0000 Subject: Move JSON serialising, deserialising and walking code into a separate package --- walk/walk.go | 316 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 walk/walk.go (limited to 'walk/walk.go') diff --git a/walk/walk.go b/walk/walk.go new file mode 100644 index 0000000..19180b4 --- /dev/null +++ b/walk/walk.go @@ -0,0 +1,316 @@ +package walk + +import ( + "io" + "encoding/json" + "fmt" +) + +type PathSegment interface {} +type Path []PathSegment + +type TerminalValue int +const ( + ArrayBegin TerminalValue = iota + ArrayEnd + MapBegin + MapEnd +) +type ValueNull struct {} +type ValueBool bool +type ValueNumber float64 +type ValueString string + +type WalkValue interface {} + +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 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", token.Value) + 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 + } + 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") +} -- cgit v1.2.3