feat(users/Profpatsch/struct-edit): add support for maps

This makes it possible to pipe json dicts to the program and fully
navigate them.

Change-Id: I18dd8683d6f00c8ea967eb0c8dc89d1e0735fbcb
Reviewed-on: https://cl.tvl.fyi/c/depot/+/2863
Tested-by: BuildkiteCI
Reviewed-by: Profpatsch <mail@profpatsch.de>
This commit is contained in:
Profpatsch 2021-04-05 23:30:48 +02:00
parent 22a8bf93f7
commit f57f1e4489

View file

@ -6,6 +6,7 @@ import (
"log" "log"
"os" "os"
"strings" "strings"
"sort"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
lipgloss "github.com/charmbracelet/lipgloss" lipgloss "github.com/charmbracelet/lipgloss"
@ -37,6 +38,7 @@ type val struct {
// tagString -> string // tagString -> string
// tagFloat -> float64 // tagFloat -> float64
// tagList -> []val // tagList -> []val
// tagMap -> map[string]val
val interface{} val interface{}
} }
@ -46,6 +48,7 @@ const (
tagString tag = "string" tagString tag = "string"
tagFloat tag = "float" tagFloat tag = "float"
tagList tag = "list" tagList tag = "list"
tagMap tag = "map"
) )
// print a value, flat // print a value, flat
@ -59,11 +62,19 @@ func (v val) Render() string {
case tagList: case tagList:
s += "[ " s += "[ "
vs := []string{} vs := []string{}
for _, v := range v.val.([]val) { for _, enum := range v.enumerate() {
vs = append(vs, v.Render()) vs = append(vs, enum.v.Render())
} }
s += strings.Join(vs, ", ") s += strings.Join(vs, ", ")
s += " ]" s += " ]"
case tagMap:
s += "{ "
vs := []string{}
for _, enum := range v.enumerate() {
vs = append(vs, fmt.Sprintf("%s: %s", enum.i.(string), enum.v.Render()))
}
s += strings.Join(vs, ", ")
s += " }"
default: default:
s += fmt.Sprintf("<unknown: %v>", v) s += fmt.Sprintf("<unknown: %v>", v)
} }
@ -111,6 +122,16 @@ func makeVal(i interface{}) val {
doc: "", doc: "",
val: ls, val: ls,
} }
case map[string]interface{}:
ls := map[string]val{}
for k, i := range i {
ls[k] = makeVal(i)
}
v = val{
tag: tagMap,
doc: "",
val: ls,
}
default: default:
log.Fatalf("makeVal: cannot read json of type %T", i) log.Fatalf("makeVal: cannot read json of type %T", i)
} }
@ -119,12 +140,7 @@ func makeVal(i interface{}) val {
// return an index that points at the first entry in val // return an index that points at the first entry in val
func (v val) pos1() index { func (v val) pos1() index {
switch v.tag { return v.enumerate()[0].i
case tagList:
return index(uint(0))
default:
return index(nil)
}
} }
type enumerate struct { type enumerate struct {
@ -145,6 +161,18 @@ func (v val) enumerate() (e []enumerate) {
for i, v := range v.val.([]val) { for i, v := range v.val.([]val) {
e = append(e, enumerate{i: index(uint(i)), v: v}) e = append(e, enumerate{i: index(uint(i)), v: v})
} }
case tagMap:
// map sorting order is not stable (actually randomized thank jabber)
// so lets sort them
keys := []string{}
m := v.val.(map[string]val)
for k, _ := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
e = append(e, enumerate{i: index(k), v: m[k]})
}
default: default:
log.Fatalf("unknown val tag %s, %v", v.tag, v) log.Fatalf("unknown val tag %s, %v", v.tag, v)
} }
@ -163,7 +191,7 @@ func (m model) PathString() string {
// walk the given path down in data, to get the value at that point. // walk the given path down in data, to get the value at that point.
// Assumes that all path indexes are valid indexes into data. // Assumes that all path indexes are valid indexes into data.
func walk(data val, path []index) (val, error) { func walk(data val, path []index) (val, bool, error) {
atPath := func(index int) string { atPath := func(index int) string {
return fmt.Sprintf("at path %v", path[:index+1]) return fmt.Sprintf("at path %v", path[:index+1])
} }
@ -173,34 +201,49 @@ func walk(data val, path []index) (val, error) {
for i, p := range path { for i, p := range path {
switch data.tag { switch data.tag {
case tagString: case tagString:
return data, errf("string", data.val, i) return data, true, nil
case tagFloat: case tagFloat:
return data, errf("float", data.val, i) return data, true, nil
case tagList: case tagList:
switch p := p.(type) { switch p := p.(type) {
case uint: case uint:
list := data.val.([]val) list := data.val.([]val)
if int(p) >= len(list) || p < 0 { if int(p) >= len(list) || p < 0 {
return data, fmt.Errorf("index out of bounds " + atPath(i)) return data, false, fmt.Errorf("index out of bounds %s", atPath(i))
} }
data = list[p] data = list[p]
default: default:
return data, fmt.Errorf("not a list index " + atPath(i)) return data, false, fmt.Errorf("not a list index %s", atPath(i))
} }
case tagMap:
switch p := p.(type) {
case string:
m := data.val.(map[string]val)
var ok bool
if data, ok = m[p]; !ok {
return data, false, fmt.Errorf("index %s not in map %s", p, atPath(i))
}
default:
return data, false, fmt.Errorf("not a map index %v %s", p, atPath(i))
}
default: default:
return data, errf(string(data.tag), data.val, i) return data, false, errf(string(data.tag), data.val, i)
} }
} }
return data, nil return data, false, nil
} }
// descend into the selected index. Assumes that the index is valid. // descend into the selected index. Assumes that the index is valid.
// Will not descend into scalars. // Will not descend into scalars.
func (m model) descend() (model, error) { func (m model) descend() (model, error) {
newPath := append(m.path, m.selectedIndex) newPath := append(m.path, m.selectedIndex)
lower, err := walk(m.data, newPath) lower, bounce, err := walk(m.data, newPath)
// only descend if we *can* (TODO: can we distinguish bad errors from scalar?) if err != nil {
if err == nil { return m, err
}
// only descend if we *can*
if !bounce {
m.path = newPath m.path = newPath
m.selectedIndex = lower.pos1() m.selectedIndex = lower.pos1()
} }
@ -211,7 +254,7 @@ func (m model) descend() (model, error) {
func (m model) ascend() (model, error) { func (m model) ascend() (model, error) {
if len(m.path) > 0 { if len(m.path) > 0 {
m.path = m.path[:len(m.path)-1] m.path = m.path[:len(m.path)-1]
upper, err := walk(m.data, m.path) upper, _, err := walk(m.data, m.path)
m.selectedIndex = upper.pos1() m.selectedIndex = upper.pos1()
return m, err return m, err
} }
@ -222,7 +265,10 @@ func (m model) ascend() (model, error) {
func (min model) next() (m model, err error) { func (min model) next() (m model, err error) {
m = min m = min
var this val var this val
this, err = walk(m.data, m.path) this, _, err = walk(m.data, m.path)
if err != nil {
return
}
enumL := this.enumerate() enumL := this.enumerate()
setNext := false setNext := false
for _, enum := range enumL { for _, enum := range enumL {
@ -246,7 +292,10 @@ func (min model) next() (m model, err error) {
func (min model) prev() (m model, err error) { func (min model) prev() (m model, err error) {
m = min m = min
var this val var this val
this, err = walk(m.data, m.path) this, _, err = walk(m.data, m.path)
if err != nil {
return
}
enumL := this.enumerate() enumL := this.enumerate()
// last element, wraparound // last element, wraparound
prevIndex := enumL[len(enumL)-1].i prevIndex := enumL[len(enumL)-1].i
@ -320,7 +369,7 @@ var selectedColor = lipgloss.NewStyle().
func (m model) View() string { func (m model) View() string {
s := pathColor.Render(m.PathString()) s := pathColor.Render(m.PathString())
cur, err := walk(m.data, m.path) cur, _, err := walk(m.data, m.path)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }