package walk import ( "io" "encoding/json" "fmt" "strings" "math" ) // int or string type PathSegment interface {} type Path []PathSegment func (path Path) ToWalkValues() []WalkValue { var values []WalkValue for _, segment := range path { switch s := segment.(type) { case int: values = append(values, ValueNumber(s)) case string: values = append(values, ValueString(s)) default: panic("Invalid PathSegment") } } return values } func PathFromWalkValues(values []WalkValue) Path { var segments []PathSegment for _, value := range values { switch v := value.(type) { case ValueNumber: segments = append(segments, int(math.Round(float64(v)))) case ValueString: segments = append(segments, string(v)) default: panic("Invalid value in path") } } return segments } type TerminalValue int const ( ArrayBegin TerminalValue = iota ArrayEnd MapBegin MapEnd ) func (value TerminalValue) Atomise() []Atom { return []Atom{value} } func (value TerminalValue) String() string { switch value { case ArrayBegin: return "[" case ArrayEnd: return "]" case MapBegin: return "{" case MapEnd: return "}" default: panic("Unknown TerminalValue") } } func (value TerminalValue) atomness() {} type ValueNull struct {} func (value ValueNull) Atomise() []Atom { return []Atom{ValueNull{}} } func (value ValueNull) String() string { return "null" } func (value ValueNull) atomness() {} type ValueBool bool func (value ValueBool) Atomise() []Atom { return []Atom{value} } func (value ValueBool) String() string { if value { return "true" } else { return "false" } } func (value ValueBool) atomness() {} type ValueNumber float64 func (value ValueNumber) Atomise() []Atom { return []Atom{value} } func (value ValueNumber) String() string { v := float64(value) return fmt.Sprintf("%f", v) } func (value ValueNumber) atomness() {} type StringTerminal struct {} func (value StringTerminal) atomness() {} type StringAtom rune func (value StringAtom) atomness() {} type ValueString string func (value ValueString) Atomise() (out []Atom) { out = append(out, StringTerminal{}) for _, char := range value { out = append(out, StringAtom(char)) } out = append(out, StringTerminal{}) return out } func (value ValueString) String() string { return fmt.Sprintf("\"%s\"", string(value)) } type Atom interface { // Something so the compiler will check that only certain types are being cast into Atoms atomness() } type WalkValue interface { Atomise() []Atom String() string } 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 v := 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", string(v)) 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.Value } 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") } func ConcatData(first []Atom, second []Atom) []Atom { return append(append([]Atom(nil), first...), second...) } func Atomise(in []WalkValue) (out []Atom) { for _, value := range in { out = append(out, value.Atomise()...) } return out } type CompoundError int const ( CompoundRuneOutsideString CompoundError = iota CompoundUnknownAtom CompoundMissingEnd CompoundInvalidStringAtom ) func (err CompoundError) Error() string { switch err { case CompoundRuneOutsideString: return "Compound Error: Rune Outside String" case CompoundUnknownAtom: return "Compound Error: Unknown Atom" case CompoundMissingEnd: return "Compound Error: Missing End" case CompoundInvalidStringAtom: return "Compound Error: Invalid String Atom" default: panic("Invalid CompoundError") } } type CompoundResult struct { value WalkValue error error } func Compound(in []Atom) (out []WalkValue, error error) { i := 0 for { if i >= len(in) { break } atom := in[i] i++ switch v := atom.(type) { case TerminalValue: out = append(out, v) continue case ValueNull: out = append(out, v) continue case ValueBool: out = append(out, v) continue case ValueNumber: out = append(out, v) continue case StringAtom: return nil, CompoundRuneOutsideString case StringTerminal: default: return nil, CompoundUnknownAtom } // Handle string start var builder strings.Builder loop: for { if i >= len(in) { return nil, CompoundMissingEnd } atom := in[i] i++ switch v := atom.(type) { case StringTerminal: break loop case StringAtom: builder.WriteRune(rune(v)) case ValueNull: builder.WriteString(v.String()) case ValueBool: builder.WriteString(v.String()) case ValueNumber: builder.WriteString(v.String()) case TerminalValue: builder.WriteString(v.String()) default: return nil, CompoundInvalidStringAtom } } out = append(out, ValueString(builder.String())) } return out, nil }