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:
parent
22a8bf93f7
commit
f57f1e4489
1 changed files with 71 additions and 22 deletions
|
@ -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 let’s 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:
|
default:
|
||||||
return data, errf(string(data.tag), data.val, i)
|
return data, false, fmt.Errorf("not a map index %v %s", p, atPath(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue