From 3c34366bdd5d817a184d6b1c901d03a16b6faa4b Mon Sep 17 00:00:00 2001 From: Charlie Stanton Date: Fri, 12 May 2023 14:25:32 +0100 Subject: Adds the json_array IO format --- json_array/read.go | 118 +++++++++++++++++++++++++++++++++++++++ json_array/write.go | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++ main/main.go | 6 +- walk/atom.go | 12 ++++ 4 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 json_array/read.go create mode 100644 json_array/write.go diff --git a/json_array/read.go b/json_array/read.go new file mode 100644 index 0000000..6334197 --- /dev/null +++ b/json_array/read.go @@ -0,0 +1,118 @@ +package json_array + +import ( + "main/walk" + "encoding/json" + "errors" + "bufio" +) + +type state int +const ( + stateStart state = iota + stateValueStart + stateEnd + stateDead +) + +func atomiseValue(value interface{}) []walk.Atom { + switch v := value.(type) { + case nil: + return []walk.Atom{walk.NewAtomNull()} + case bool: + return []walk.Atom{walk.NewAtomBool(v)} + case float64: + return []walk.Atom{walk.NewAtomNumber(v)} + case string: + atoms := []walk.Atom{walk.NewAtomStringTerminal()} + for _, r := range v { + atoms = append(atoms, walk.NewAtomStringRune(r)) + } + atoms = append(atoms, walk.NewAtomStringTerminal()) + return atoms + case []interface{}: + atoms := []walk.Atom{walk.NewAtomTerminal(walk.ArrayBegin)} + for _, element := range v { + atoms = append(atoms, atomiseValue(element)...) + } + atoms = append(atoms, walk.NewAtomTerminal(walk.ArrayEnd)) + return atoms + case map[string]interface{}: + atoms := []walk.Atom{walk.NewAtomTerminal(walk.MapBegin)} + for key, element := range v { + atoms = append(atoms, atomiseValue(key)...) + atoms = append(atoms, atomiseValue(element)...) + } + atoms = append(atoms, walk.NewAtomTerminal(walk.MapEnd)) + return atoms + default: + panic("Invalid JSON value type") + } +} + +func NewJSONArrayReader(reader *bufio.Reader) *JSONArrayReader { + return &JSONArrayReader { + decoder: json.NewDecoder(reader), + state: stateStart, + index: 0, + } +} + +type JSONArrayReader struct { + decoder *json.Decoder + state state + index int +} + +func (in *JSONArrayReader) Read() (walk.WalkItem, error) { + restart: + switch in.state { + case stateStart: + arrayStart, err := in.decoder.Token() + if err != nil { + panic("Error reading start of JSON array") + } + delim, isDelim := arrayStart.(json.Delim) + if !isDelim || delim != '[' { + panic("JSON input is not an array!") + } + in.state = stateValueStart + goto restart + case stateValueStart: + if !in.decoder.More() { + in.state = stateEnd + goto restart + } + var m interface{} + err := in.decoder.Decode(&m) + if err != nil { + panic("Error decoding array value") + } + in.index += 1 + return walk.WalkItem { + Path: []walk.Atom{walk.NewAtomNumber(float64(in.index - 1))}, + Value: atomiseValue(m), + }, nil + case stateEnd: + arrayEnd, err := in.decoder.Token() + if err != nil { + panic("Error reading end of JSON array") + } + delim, isDelim := arrayEnd.(json.Delim) + if !isDelim || delim != ']' { + panic("JSON array wasn't ended") + } + in.state = stateDead + return walk.WalkItem{}, errors.New("eof") + case stateDead: + return walk.WalkItem{}, errors.New("eof") + default: + panic("Unreachable!!!") + } +} + +func (in *JSONArrayReader) AssertDone() { + if in.state != stateDead || in.decoder.More() { + panic("More JSON after array value") + } +} diff --git a/json_array/write.go b/json_array/write.go new file mode 100644 index 0000000..4d202c4 --- /dev/null +++ b/json_array/write.go @@ -0,0 +1,156 @@ +package json_array + +import ( + "bufio" + "strings" + "main/walk" + "encoding/json" +) + +func assembleValue(atoms []walk.Atom) (interface{}, []walk.Atom) { + if len(atoms) == 0 { + panic("Missing JSON value in output") + } + switch atoms[0].Typ { + case walk.AtomNull: + return nil, atoms[1:] + case walk.AtomBool: + return atoms[0].Bool(), atoms[1:] + case walk.AtomNumber: + return atoms[0].Number(), atoms[1:] + case walk.AtomStringTerminal: + var builder strings.Builder + atoms = atoms[1:] + for { + if len(atoms) == 0 { + panic("Missing closing string terminal") + } + if atoms[0].Typ == walk.AtomStringTerminal { + break + } + if atoms[0].Typ != walk.AtomStringRune { + panic("Non string rune atom inside string") + } + builder.WriteRune(atoms[0].StringRune()) + atoms = atoms[1:] + } + atoms = atoms[1:] + return builder.String(), atoms + case walk.AtomStringRune: + panic("String rune used outside of string terminals") + case walk.AtomTerminal: + terminal := atoms[0].Terminal() + switch terminal { + case walk.ArrayEnd, walk.MapEnd: + panic("Tried to extract value from end terminal") + case walk.ArrayBegin: + var arr []interface{} + var element interface{} + atoms = atoms[1:] + for { + if len(atoms) == 0 { + panic("Missing array end terminal") + } + if atoms[0].Typ == walk.AtomTerminal && atoms[0].Terminal() == walk.ArrayEnd { + atoms = atoms[1:] + break + } + element, atoms = assembleValue(atoms) + arr = append(arr, element) + } + return arr, atoms + case walk.MapBegin: + obj := make(map[string]interface{}) + var key interface{} + var element interface{} + atoms = atoms[1:] + for { + if len(atoms) == 0 { + panic("Missing map end terminal") + } + if atoms[0].Typ == walk.AtomTerminal && atoms[0].Terminal() == walk.MapEnd { + atoms = atoms[1:] + break + } + key, atoms = assembleValue(atoms) + element, atoms = assembleValue(atoms) + keyString, keyIsString := key.(string) + if !keyIsString { + panic("Key is not string") + } + obj[keyString] = element + } + return obj, atoms + default: + panic("Invalid terminal") + } + default: + panic("Invalid atom") + } +} + +func outputValue(atoms []walk.Atom, writer *bufio.Writer) { + if len(atoms) == 0 { + return + } + value, atoms := assembleValue(atoms) + if len(atoms) != 0 { + panic("Tried to output more than one JSON value") + } + bytes, err := json.MarshalIndent(value, "\t", "\t") + if err != nil { + panic("Error marshalling json into bytes") + } + _, err = writer.Write(bytes) + if err != nil { + panic("Error writing value") + } +} + +type writerState int +const ( + writerStateStart writerState = iota + writerStateValue +) + +func NewJSONArrayWriter(writer *bufio.Writer) *JSONArrayWriter { + return &JSONArrayWriter { + writer: writer, + state: writerStateStart, + } +} + +type JSONArrayWriter struct { + writer *bufio.Writer + state writerState +} + +func (out *JSONArrayWriter) Write(item walk.WalkItem) error { + switch out.state { + case writerStateStart: + _, err := out.writer.WriteString("[\n\t") + if err != nil { + panic("Error outputting [ at beginning of array") + } + outputValue(item.Value, out.writer) + out.state = writerStateValue + return nil + case writerStateValue: + _, err := out.writer.WriteString(",\n\t") + if err != nil { + panic("Error outputting comma at the end of a value") + } + outputValue(item.Value, out.writer) + return nil + default: + panic("Invalid writer state") + } +} + +func (out *JSONArrayWriter) AssertDone() { + if out.state == writerStateStart { + out.writer.WriteString("[") + } + out.writer.WriteString("\n]") + out.writer.Flush() +} diff --git a/main/main.go b/main/main.go index 668253d..a506954 100644 --- a/main/main.go +++ b/main/main.go @@ -4,7 +4,7 @@ import ( "os" "bufio" "main/walk" - "main/json_tokens" + "main/json_array" ) type Program []Command @@ -45,8 +45,8 @@ func main() { stdout := bufio.NewWriter(os.Stdout) state := ProgramState { - in: json_tokens.NewJSONIn(stdin), - out: json_tokens.NewJSONOut(stdout), + in: json_array.NewJSONArrayReader(stdin), + out: json_array.NewJSONArrayWriter(stdout), program: program, } diff --git a/walk/atom.go b/walk/atom.go index dfe5fe4..299c39d 100644 --- a/walk/atom.go +++ b/walk/atom.go @@ -37,6 +37,12 @@ func NewAtomBool(v bool) Atom { } } } +func (v Atom) Bool() bool { + if v.Typ != AtomBool { + panic("Tried to use non-bool as bool") + } + return v.data == 1 +} func NewAtomNumber(v float64) Atom { return Atom { Typ: AtomNumber, @@ -73,6 +79,12 @@ func NewAtomStringRune(v rune) Atom { data: uint64(v), } } +func (v Atom) StringRune() rune { + if v.Typ != AtomStringRune { + panic("Tried to use non-stringrune as stringrune") + } + return rune(v.data) +} func (v Atom) String() string { switch v.Typ { case AtomNull: -- cgit v1.2.3