<- Back to shtanton's homepage
aboutsummaryrefslogtreecommitdiff
path: root/walk
diff options
context:
space:
mode:
authorCharlie Stanton <charlie@shtanton.xyz>2023-02-19 09:27:55 +0000
committerCharlie Stanton <charlie@shtanton.xyz>2023-02-19 09:27:55 +0000
commit3bd45dc49a35b82dcc4ae93796c3e152d799bc0b (patch)
tree3a681ac5dbd777d2b6b116429cfbd934815661ce /walk
parenta5a4db8283fda88c5bd42272de0258e5d134c5bd (diff)
downloadstred-go-3bd45dc49a35b82dcc4ae93796c3e152d799bc0b.tar
Move JSON serialising, deserialising and walking code into a separate package
Diffstat (limited to 'walk')
-rw-r--r--walk/walk.go316
1 files changed, 316 insertions, 0 deletions
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")
+}