diff options
| author | Charlie Stanton <charlie@shtanton.xyz> | 2023-02-19 09:27:55 +0000 | 
|---|---|---|
| committer | Charlie Stanton <charlie@shtanton.xyz> | 2023-02-19 09:27:55 +0000 | 
| commit | 3bd45dc49a35b82dcc4ae93796c3e152d799bc0b (patch) | |
| tree | 3a681ac5dbd777d2b6b116429cfbd934815661ce /walk | |
| parent | a5a4db8283fda88c5bd42272de0258e5d134c5bd (diff) | |
| download | stred-go-3bd45dc49a35b82dcc4ae93796c3e152d799bc0b.tar | |
Move JSON serialising, deserialising and walking code into a separate package
Diffstat (limited to 'walk')
| -rw-r--r-- | walk/walk.go | 316 | 
1 files changed, 316 insertions, 0 deletions
| diff --git a/walk/walk.go b/walk/walk.go new file mode 100644 index 0000000..19180b4 --- /dev/null +++ b/walk/walk.go @@ -0,0 +1,316 @@ +package walk + +import ( +	"io" +	"encoding/json" +	"fmt" +) + +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 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") +} | 
