From 96a10b92057319b2c30782ea19f5427a333e6bc3 Mon Sep 17 00:00:00 2001 From: Charlie Stanton Date: Tue, 25 Apr 2023 14:20:20 +0100 Subject: Separates JSON parsing code into its own file --- walk/read.go | 285 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ walk/walk.go | 278 --------------------------------------------------------- 2 files changed, 285 insertions(+), 278 deletions(-) create mode 100644 walk/read.go diff --git a/walk/read.go b/walk/read.go new file mode 100644 index 0000000..6f3acfe --- /dev/null +++ b/walk/read.go @@ -0,0 +1,285 @@ +package walk + +import ( + "bufio" + "math" + "strings" + "strconv" +) + +type JSONInStructure int +const ( + JSONInRoot JSONInStructure = iota + JSONInMap + JSONInArray + JSONInValueEnd +) + +type JSONIn struct { + path []Atom + reader *bufio.Reader + structure []JSONInStructure +} + +func NewJSONIn(reader *bufio.Reader) JSONIn { + return JSONIn { + path: make([]Atom, 0, 256), + reader: reader, + structure: []JSONInStructure{JSONInRoot}, + } +} + +func isWhitespace(r rune) bool { + for _, ws := range " \t\r\n" { + if r == ws { + return true + } + } + return false +} + +func isNumberRune(r rune) bool { + return '0' <= r && r <= '9' || r == '.' +} + +func (in *JSONIn) popPath() { + if len(in.path) == 0 { + panic("Tried to pop from empty path") + } + finalAtom := in.path[len(in.path) - 1] + if finalAtom.Typ != AtomStringTerminal { + in.path = in.path[:len(in.path) - 1] + return + } + i := len(in.path) - 2 + for { + if i < 0 { + panic("Missing string begin in path") + } + if in.path[i].Typ == AtomStringTerminal { + break + } + i-- + } + in.path = in.path[:i] +} + +func (in *JSONIn) nextNonWsRune() (rune, error) { + for { + r, _, err := in.reader.ReadRune() + if err != nil { + return 0, err + } + if !isWhitespace(r) { + return r, nil + } + } +} + +func (in *JSONIn) requireString(criteria string) { + for _, r := range criteria { + in.require(r) + } +} + +func (in *JSONIn) require(criterion rune) { + r, _, err := in.reader.ReadRune() + if err != nil { + panic("Error while reading required rune: " + err.Error()) + } + if r != criterion { + panic("Required rune not read") + } +} + +func (in *JSONIn) readString(out []Atom) []Atom { + // TODO: improve + out = append(out, NewAtomStringTerminal()) + for { + r, _, err := in.reader.ReadRune() + if err != nil { + panic("Missing closing terminal in string input: " + err.Error()) + } + if r == '"' { + break + } + if r == '\\' { + r, _, err = in.reader.ReadRune() + if err != nil { + panic("Missing rune after \\") + } + if len(out) == cap(out) { + newOut := make([]Atom, len(out), cap(out) * 2) + copy(newOut, out) + out = newOut + } + out = append(out, NewAtomStringRune(r)) + continue + } + if len(out) == cap(out) { + newOut := make([]Atom, len(out), cap(out) * 2) + copy(newOut, out) + out = newOut + } + out = append(out, NewAtomStringRune(r)) + } + out = append(out, NewAtomStringTerminal()) + return out +} + +func (in *JSONIn) Read() (WalkItem, error) { + restart: + // TODO: Escaping + // TODO: Don't allow trailing commas + // TODO: Proper float parsing with e and stuff + r, err := in.nextNonWsRune() + if err != nil { + return WalkItem {}, err + } + state := in.structure[len(in.structure) - 1] + switch state { + case JSONInMap: + in.popPath() + if r == '}' { + in.structure[len(in.structure) - 1] = JSONInValueEnd + return WalkItem { + Value: []Atom{NewAtomTerminal(MapEnd)}, + Path: in.path, + }, nil + } + if r != '"' { + panic("Expected key, found something else") + } + in.path = in.readString(in.path) + r, err = in.nextNonWsRune() + if err != nil { + panic("Expected : got: " + err.Error()) + } + if r != ':' { + panic("Expected : after key") + } + r, err = in.nextNonWsRune() + if err != nil { + panic("Missing map value after key: " + err.Error()) + } + case JSONInArray: + if r == ']' { + in.structure[len(in.structure) - 1] = JSONInValueEnd + in.popPath() + return WalkItem { + Value: []Atom{NewAtomTerminal(ArrayEnd)}, + Path: in.path, + }, nil + } + prevIndex := in.path[len(in.path) - 1] + if prevIndex.Typ == AtomNull { + prevIndex.Typ = AtomNumber + prevIndex.data = math.Float64bits(0) + } else if prevIndex.Typ == AtomNumber { + prevIndex.data = math.Float64bits(math.Float64frombits(prevIndex.data) + 1) + } else { + panic("Invalid index in array input") + } + in.path[len(in.path) - 1] = prevIndex + case JSONInRoot: + case JSONInValueEnd: + in.structure = in.structure[:len(in.structure) - 1] + underState := in.structure[len(in.structure) - 1] + if underState == JSONInRoot { + panic("More input after root JSON object ends") + } else if underState == JSONInMap && r == '}' { + in.structure[len(in.structure) - 1] = JSONInValueEnd + in.popPath() + return WalkItem { + Value: []Atom{NewAtomTerminal(MapEnd)}, + Path: in.path, + }, nil + } else if underState == JSONInArray && r == ']' { + in.structure[len(in.structure) - 1] = JSONInValueEnd + in.popPath() + return WalkItem { + Value: []Atom{NewAtomTerminal(ArrayEnd)}, + Path: in.path, + }, nil + } + if r != ',' { + panic("Expected , after JSON value, found: \"" + string(r) + "\"") + } + goto restart + default: + panic("Invalid JSONIn state") + } + switch r { + case 'n': + in.requireString("ull") + in.structure = append(in.structure, JSONInValueEnd) + return WalkItem { + Value: []Atom{NewAtomNull()}, + Path: in.path, + }, nil + case 'f': + in.requireString("alse") + in.structure = append(in.structure, JSONInValueEnd) + return WalkItem { + Value: []Atom{NewAtomBool(false)}, + Path: in.path, + }, nil + case 't': + in.requireString("rue") + in.structure = append(in.structure, JSONInValueEnd) + return WalkItem { + Value: []Atom{NewAtomBool(true)}, + Path: in.path, + }, nil + case '"': + value := make([]Atom, 0, 64) + value = in.readString(value) + in.structure = append(in.structure, JSONInValueEnd) + return WalkItem { + Value: value, + Path: in.path, + }, nil + case '{': + in.structure = append(in.structure, JSONInMap) + in.path = append(in.path, NewAtomNull()) + return WalkItem { + Value: []Atom{NewAtomTerminal(MapBegin)}, + Path: in.path[:len(in.path) - 1], + }, nil + case '[': + in.structure = append(in.structure, JSONInArray) + in.path = append(in.path, NewAtomNull()) + return WalkItem { + Value: []Atom{NewAtomTerminal(ArrayBegin)}, + Path: in.path[:len(in.path) - 1], + }, nil + } + if isNumberRune(r) { + var builder strings.Builder + builder.WriteRune(r) + for { + r, _, err = in.reader.ReadRune() + if err != nil || !isNumberRune(r) { + break + } + builder.WriteRune(r) + } + in.reader.UnreadRune() + number, parseError := strconv.ParseFloat(builder.String(), 64) + if parseError != nil { + panic("Invalid number") + } + in.structure = append(in.structure, JSONInValueEnd) + return WalkItem { + Value: []Atom{NewAtomNumber(number)}, + Path: in.path, + }, nil + } + panic("Invalid JSON value") +} + +func (in *JSONIn) AssertDone() { + if len(in.structure) != 2 || in.structure[0] != JSONInRoot || in.structure[1] != JSONInValueEnd { + panic("Input ended on incomplete JSON root") + } +} diff --git a/walk/walk.go b/walk/walk.go index 20eac38..6e86877 100644 --- a/walk/walk.go +++ b/walk/walk.go @@ -6,7 +6,6 @@ import ( "math" "unicode/utf8" "bufio" - "strconv" ) // int or string @@ -50,283 +49,6 @@ type WalkItem struct { Path []Atom } -type JSONInStructure int -const ( - JSONInRoot JSONInStructure = iota - JSONInMap - JSONInArray - JSONInValueEnd -) - -type JSONIn struct { - path []Atom - reader *bufio.Reader - structure []JSONInStructure -} - -func NewJSONIn(reader *bufio.Reader) JSONIn { - return JSONIn { - path: make([]Atom, 0, 256), - reader: reader, - structure: []JSONInStructure{JSONInRoot}, - } -} - -func isWhitespace(r rune) bool { - for _, ws := range " \t\r\n" { - if r == ws { - return true - } - } - return false -} - -func isNumberRune(r rune) bool { - return '0' <= r && r <= '9' || r == '.' -} - -func (in *JSONIn) popPath() { - if len(in.path) == 0 { - panic("Tried to pop from empty path") - } - finalAtom := in.path[len(in.path) - 1] - if finalAtom.Typ != AtomStringTerminal { - in.path = in.path[:len(in.path) - 1] - return - } - i := len(in.path) - 2 - for { - if i < 0 { - panic("Missing string begin in path") - } - if in.path[i].Typ == AtomStringTerminal { - break - } - i-- - } - in.path = in.path[:i] -} - -func (in *JSONIn) nextNonWsRune() (rune, error) { - for { - r, _, err := in.reader.ReadRune() - if err != nil { - return 0, err - } - if !isWhitespace(r) { - return r, nil - } - } -} - -func (in *JSONIn) requireString(criteria string) { - for _, r := range criteria { - in.require(r) - } -} - -func (in *JSONIn) require(criterion rune) { - r, _, err := in.reader.ReadRune() - if err != nil { - panic("Error while reading required rune: " + err.Error()) - } - if r != criterion { - panic("Required rune not read") - } -} - -func (in *JSONIn) readString(out []Atom) []Atom { - // TODO: improve - out = append(out, NewAtomStringTerminal()) - for { - r, _, err := in.reader.ReadRune() - if err != nil { - panic("Missing closing terminal in string input: " + err.Error()) - } - if r == '"' { - break - } - if r == '\\' { - r, _, err = in.reader.ReadRune() - if err != nil { - panic("Missing rune after \\") - } - if len(out) == cap(out) { - newOut := make([]Atom, len(out), cap(out) * 2) - copy(newOut, out) - out = newOut - } - out = append(out, NewAtomStringRune(r)) - continue - } - if len(out) == cap(out) { - newOut := make([]Atom, len(out), cap(out) * 2) - copy(newOut, out) - out = newOut - } - out = append(out, NewAtomStringRune(r)) - } - out = append(out, NewAtomStringTerminal()) - return out -} - -func (in *JSONIn) Read() (WalkItem, error) { - restart: - // TODO: Escaping - // TODO: Don't allow trailing commas - // TODO: Proper float parsing with e and stuff - r, err := in.nextNonWsRune() - if err != nil { - return WalkItem {}, err - } - state := in.structure[len(in.structure) - 1] - switch state { - case JSONInMap: - in.popPath() - if r == '}' { - in.structure[len(in.structure) - 1] = JSONInValueEnd - return WalkItem { - Value: []Atom{NewAtomTerminal(MapEnd)}, - Path: in.path, - }, nil - } - if r != '"' { - panic("Expected key, found something else") - } - in.path = in.readString(in.path) - r, err = in.nextNonWsRune() - if err != nil { - panic("Expected : got: " + err.Error()) - } - if r != ':' { - panic("Expected : after key") - } - r, err = in.nextNonWsRune() - if err != nil { - panic("Missing map value after key: " + err.Error()) - } - case JSONInArray: - if r == ']' { - in.structure[len(in.structure) - 1] = JSONInValueEnd - in.popPath() - return WalkItem { - Value: []Atom{NewAtomTerminal(ArrayEnd)}, - Path: in.path, - }, nil - } - prevIndex := in.path[len(in.path) - 1] - if prevIndex.Typ == AtomNull { - prevIndex.Typ = AtomNumber - prevIndex.data = math.Float64bits(0) - } else if prevIndex.Typ == AtomNumber { - prevIndex.data = math.Float64bits(math.Float64frombits(prevIndex.data) + 1) - } else { - panic("Invalid index in array input") - } - in.path[len(in.path) - 1] = prevIndex - case JSONInRoot: - case JSONInValueEnd: - in.structure = in.structure[:len(in.structure) - 1] - underState := in.structure[len(in.structure) - 1] - if underState == JSONInRoot { - panic("More input after root JSON object ends") - } else if underState == JSONInMap && r == '}' { - in.structure[len(in.structure) - 1] = JSONInValueEnd - in.popPath() - return WalkItem { - Value: []Atom{NewAtomTerminal(MapEnd)}, - Path: in.path, - }, nil - } else if underState == JSONInArray && r == ']' { - in.structure[len(in.structure) - 1] = JSONInValueEnd - in.popPath() - return WalkItem { - Value: []Atom{NewAtomTerminal(ArrayEnd)}, - Path: in.path, - }, nil - } - if r != ',' { - panic("Expected , after JSON value, found: \"" + string(r) + "\"") - } - goto restart - default: - panic("Invalid JSONIn state") - } - switch r { - case 'n': - in.requireString("ull") - in.structure = append(in.structure, JSONInValueEnd) - return WalkItem { - Value: []Atom{NewAtomNull()}, - Path: in.path, - }, nil - case 'f': - in.requireString("alse") - in.structure = append(in.structure, JSONInValueEnd) - return WalkItem { - Value: []Atom{NewAtomBool(false)}, - Path: in.path, - }, nil - case 't': - in.requireString("rue") - in.structure = append(in.structure, JSONInValueEnd) - return WalkItem { - Value: []Atom{NewAtomBool(true)}, - Path: in.path, - }, nil - case '"': - value := make([]Atom, 0, 64) - value = in.readString(value) - in.structure = append(in.structure, JSONInValueEnd) - return WalkItem { - Value: value, - Path: in.path, - }, nil - case '{': - in.structure = append(in.structure, JSONInMap) - in.path = append(in.path, NewAtomNull()) - return WalkItem { - Value: []Atom{NewAtomTerminal(MapBegin)}, - Path: in.path[:len(in.path) - 1], - }, nil - case '[': - in.structure = append(in.structure, JSONInArray) - in.path = append(in.path, NewAtomNull()) - return WalkItem { - Value: []Atom{NewAtomTerminal(ArrayBegin)}, - Path: in.path[:len(in.path) - 1], - }, nil - } - if isNumberRune(r) { - var builder strings.Builder - builder.WriteRune(r) - for { - r, _, err = in.reader.ReadRune() - if err != nil || !isNumberRune(r) { - break - } - builder.WriteRune(r) - } - in.reader.UnreadRune() - number, parseError := strconv.ParseFloat(builder.String(), 64) - if parseError != nil { - panic("Invalid number") - } - in.structure = append(in.structure, JSONInValueEnd) - return WalkItem { - Value: []Atom{NewAtomNumber(number)}, - Path: in.path, - }, nil - } - panic("Invalid JSON value") -} - -func (in *JSONIn) AssertDone() { - if len(in.structure) != 2 || in.structure[0] != JSONInRoot || in.structure[1] != JSONInValueEnd { - panic("Input ended on incomplete JSON root") - } -} - type JSONOutStructure int const ( JSONOutRoot JSONOutStructure = iota -- cgit v1.2.3