<- Back to shtanton's homepage
aboutsummaryrefslogtreecommitdiff
path: root/walk/read.go
diff options
context:
space:
mode:
authorCharlie Stanton <charlie@shtanton.xyz>2023-04-25 14:20:20 +0100
committerCharlie Stanton <charlie@shtanton.xyz>2023-04-25 14:20:20 +0100
commit96a10b92057319b2c30782ea19f5427a333e6bc3 (patch)
treeb34fc1d164e7ed8331d31718ee0d0e2c8e2aee15 /walk/read.go
parent6a4e25b1c691846185e9dd698c1558981089738c (diff)
downloadstred-go-96a10b92057319b2c30782ea19f5427a333e6bc3.tar
Separates JSON parsing code into its own file
Diffstat (limited to 'walk/read.go')
-rw-r--r--walk/read.go285
1 files changed, 285 insertions, 0 deletions
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")
+ }
+}