From ececdecdaf6c6f6295d31a92f0663d703e7760dd Mon Sep 17 00:00:00 2001 From: Charlie Stanton Date: Tue, 23 Aug 2022 22:09:14 +0100 Subject: Initial commit No parsing yet, but the execution is not bad Commands: - Print value - Toggle terminal (switch between array and map) - Filter command Filters: - Path filter Path filters are compiled from a regex like AST --- go.mod | 3 + main/command.go | 38 +++++++ main/filter.go | 34 ++++++ main/json.go | 299 +++++++++++++++++++++++++++++++++++++++++++++++++++++ main/main.go | 139 +++++++++++++++++++++++++ main/pathfilter.go | 78 ++++++++++++++ 6 files changed, 591 insertions(+) create mode 100644 go.mod create mode 100644 main/command.go create mode 100644 main/filter.go create mode 100644 main/json.go create mode 100644 main/main.go create mode 100644 main/pathfilter.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6967dfd --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module main + +go 1.18 diff --git a/main/command.go b/main/command.go new file mode 100644 index 0000000..560d3c3 --- /dev/null +++ b/main/command.go @@ -0,0 +1,38 @@ +package main + +type PrintValueCommand struct {} +func (cmd PrintValueCommand) exec(state *ProgramState) { + state.out <- state.space +} + +type ToggleTerminalCommand struct {} +func (cmd ToggleTerminalCommand) exec(state *ProgramState) { + terminal, isTerminal := state.space.value.(TerminalValue) + if !isTerminal { + return + } + switch terminal { + case ArrayBegin: + state.space.value = MapBegin + case ArrayEnd: + state.space.value = MapEnd + case MapBegin: + state.space.value = ArrayBegin + case MapEnd: + state.space.value = ArrayEnd + } +} + +type FilteredCommand struct { + filter Filter + command Command +} +func (cmd FilteredCommand) exec(state *ProgramState) { + if cmd.filter.exec(state) { + cmd.command.exec(state) + } +} + +type Command interface { + exec(*ProgramState) +} \ No newline at end of file diff --git a/main/filter.go b/main/filter.go new file mode 100644 index 0000000..95e6d82 --- /dev/null +++ b/main/filter.go @@ -0,0 +1,34 @@ +package main + +type PathFilter struct { + initial PathFilterState +} +func (filter PathFilter) exec(state *ProgramState) bool { + pathFilterState := make(map[PathFilterState]struct{}) + pathFilterState[filter.initial] = struct{}{} + for _, segment := range state.space.path { + nextPathFilterState := make(map[PathFilterState]struct{}) + for curState := range pathFilterState { + for nextState := range curState.eat(segment) { + nextPathFilterState[nextState] = struct{}{} + } + } + pathFilterState = nextPathFilterState + } + for pathState := range pathFilterState { + if pathState.accept() { + return true + } + } + return false +} + +type RangeFilter struct { + start Filter + end Filter + active bool +} + +type Filter interface { + exec(*ProgramState) bool +} \ No newline at end of file diff --git a/main/json.go b/main/json.go new file mode 100644 index 0000000..66ca5d5 --- /dev/null +++ b/main/json.go @@ -0,0 +1,299 @@ +package main + +import ( + "io" + "encoding/json" + "fmt" +) + +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") +} diff --git a/main/main.go b/main/main.go new file mode 100644 index 0000000..31e46c6 --- /dev/null +++ b/main/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "fmt" + "os" + "bufio" +) + +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 Program []Command + +type ProgramState struct { + space WalkItem + in chan WalkItem + out chan WalkItem + program []Command +} + +type StringSegmentPathFilterAST struct { + index string +} +func (ast StringSegmentPathFilterAST) compileWith(next PathFilterState) PathFilterState { + return StringSegmentPathFilter { + index: ast.index, + next: next, + } +} + +type RepeatPathFilterAST struct { + content PathFilterAST +} +func (ast RepeatPathFilterAST) compileWith(next PathFilterState) PathFilterState { + nextGroup := &GroupPathFilter{} + repeatStart := ast.content.compileWith(nextGroup) + nextGroup.filters = []PathFilterState{next, repeatStart} + return nextGroup +} + +type SequencePathFilterAST struct { + sequence []PathFilterAST +} +func (ast SequencePathFilterAST) compileWith(next PathFilterState) PathFilterState { + for i := len(ast.sequence) - 1; i >= 0; i -= 1 { + next = ast.sequence[i].compileWith(next) + } + return next +} + +type AnySegmentPathFilterAST struct {} +func (ast AnySegmentPathFilterAST) compileWith(next PathFilterState) PathFilterState { + return AnySegmentPathFilter{next: next} +} + +type PathFilterAST interface { + compileWith(PathFilterState) PathFilterState +} + +func compilePathFilterAST(ast PathFilterAST) PathFilter { + return PathFilter{ + initial: ast.compileWith(NonePathFilter{}), + } +} + +func main() { + if len(os.Args) < 2 { + fmt.Println("Missing program arg") + return + } + //input := os.Args[1] + //tokens := Lex(input) + //program := Parse(tokens) + + stdin := bufio.NewReader(os.Stdin) + dataStream := Json(stdin) + + var allRemainingPathFilter AnySegmentPathFilter + { + g := GroupPathFilter { + filters: []PathFilterState{NonePathFilter{}}, + } + allRemainingPathFilter = AnySegmentPathFilter { + next: PathFilterState(&g), + } + g.filters = append(g.filters, PathFilterState(&allRemainingPathFilter)) + } + + state := ProgramState { + in: dataStream, + out: make(chan WalkItem), + program: []Command { + FilteredCommand { + filter: compilePathFilterAST( + StringSegmentPathFilterAST {"people"}, + ), + command: PrintValueCommand{}, + }, + FilteredCommand { + filter: compilePathFilterAST( + SequencePathFilterAST { + []PathFilterAST{ + StringSegmentPathFilterAST {"people"}, + AnySegmentPathFilterAST{}, + StringSegmentPathFilterAST {"age"}, + }, + }, + ), + command: PrintValueCommand{}, + }, + }, + } + + go func () { + for walkItem := range dataStream { + state.space = walkItem + for _, cmd := range state.program { + cmd.exec(&state) + } + } + close(state.out) + }() + + JsonOut(state.out) +} diff --git a/main/pathfilter.go b/main/pathfilter.go new file mode 100644 index 0000000..7b6c64f --- /dev/null +++ b/main/pathfilter.go @@ -0,0 +1,78 @@ +package main + +type MapTerminalFilter struct {} +func (filter MapTerminalFilter) exec(state *ProgramState) bool { + terminal, isTerminal := state.space.value.(TerminalValue) + if !isTerminal { + return false + } + return terminal == MapBegin || terminal == MapEnd +} + +type NonTerminalFilter struct {} +func (filter NonTerminalFilter) exec(state *ProgramState) bool { + _, isTerminal := state.space.value.(TerminalValue) + return !isTerminal +} + +type AnySegmentPathFilter struct { + next PathFilterState +} +func (filter AnySegmentPathFilter) eat(segment PathSegment) map[PathFilterState]struct{} { + res := make(map[PathFilterState]struct{}) + res[filter.next] = struct{}{} + return res +} +func (filter AnySegmentPathFilter) accept() bool { + return false +} + +type GroupPathFilter struct { + filters []PathFilterState +} +func (filter GroupPathFilter) eat(segment PathSegment) map[PathFilterState]struct{} { + res := make(map[PathFilterState]struct{}) + for _, f := range filter.filters { + for r := range f.eat(segment) { + res[r] = struct{}{} + } + } + return res +} +func (filter GroupPathFilter) accept() bool { + for _, f := range filter.filters { + if f.accept() { + return true + } + } + return false +} + +type NonePathFilter struct {} +func (filter NonePathFilter) eat(segment PathSegment) map[PathFilterState]struct{} { + return make(map[PathFilterState]struct{}) +} +func (filter NonePathFilter) accept() bool { + return true +} + +type StringSegmentPathFilter struct { + index string + next PathFilterState +} +func (filter StringSegmentPathFilter) eat(segment PathSegment) map[PathFilterState]struct{} { + s, isString := segment.(string) + res := make(map[PathFilterState]struct{}) + if isString && s == filter.index { + res[filter.next] = struct{}{} + } + return res +} +func (filter StringSegmentPathFilter) accept() bool { + return false +} + +type PathFilterState interface { + eat(PathSegment) map[PathFilterState]struct{} + accept() bool +} -- cgit v1.2.3