Skip to content

Commit 24700e8

Browse files
feat: improved behavior for exploring paginated/streamed endpoints
1 parent 1027f9c commit 24700e8

File tree

3 files changed

+222
-14
lines changed

3 files changed

+222
-14
lines changed

internal/jsonview/explorer.go

Lines changed: 198 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package jsonview
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
67
"math"
8+
"os"
79
"strings"
810

911
"github.com/charmbracelet/bubbles/help"
@@ -12,6 +14,7 @@ import (
1214
"github.com/charmbracelet/bubbles/viewport"
1315
tea "github.com/charmbracelet/bubbletea"
1416
"github.com/charmbracelet/lipgloss"
17+
"github.com/charmbracelet/x/term"
1518
"github.com/muesli/reflow/truncate"
1619
"github.com/muesli/reflow/wordwrap"
1720
"github.com/tidwall/gjson"
@@ -100,29 +103,99 @@ var (
100103
type JSONView interface {
101104
GetPath() string
102105
GetData() gjson.Result
103-
Update(tea.Msg) tea.Cmd
106+
Update(tea.Msg, bool) tea.Cmd
104107
View() string
105108
Resize(width, height int)
106109
}
107110

108111
type TableView struct {
109-
path string
110-
data gjson.Result
111-
table table.Model
112-
rowData []gjson.Result
112+
width int
113+
height int
114+
path string
115+
data gjson.Result
116+
table table.Model
117+
rowData []gjson.Result
118+
iterator AnyIterator
119+
isLoading bool
120+
columns []table.Column
113121
}
114122

115123
func (tv *TableView) GetPath() string { return tv.path }
116124
func (tv *TableView) GetData() gjson.Result { return tv.data }
117125
func (tv *TableView) View() string { return tv.table.View() }
118126

119-
func (tv *TableView) Update(msg tea.Msg) tea.Cmd {
127+
func (tv *TableView) Update(msg tea.Msg, raw bool) tea.Cmd {
120128
var cmd tea.Cmd
121129
tv.table, cmd = tv.table.Update(msg)
130+
131+
// Check if we need to load more data
132+
if tv.iterator != nil && !tv.isLoading && tv.data.IsArray() {
133+
cursor := tv.table.Cursor()
134+
totalRows := len(tv.table.Rows())
135+
136+
// Load more when we're at the last row
137+
if cursor == totalRows-1 {
138+
tv.isLoading = true
139+
return tv.loadMoreData(raw)
140+
}
141+
}
142+
122143
return cmd
123144
}
124145

146+
func (tv *TableView) loadMoreData(raw bool) tea.Cmd {
147+
return func() tea.Msg {
148+
if tv.iterator == nil {
149+
return nil
150+
}
151+
152+
if !tv.iterator.Next() {
153+
tv.isLoading = false
154+
return tv.iterator.Err()
155+
}
156+
157+
obj := tv.iterator.Current()
158+
var result gjson.Result
159+
if jsonBytes, err := json.Marshal(obj); err != nil {
160+
return err
161+
} else {
162+
result = gjson.ParseBytes(jsonBytes)
163+
}
164+
165+
if !result.Exists() {
166+
tv.isLoading = false
167+
return nil
168+
}
169+
170+
// Add the new item to our data
171+
tv.rowData = append(tv.rowData, result)
172+
173+
// Add new row to the table
174+
newRow := table.Row{formatValue(result, raw)}
175+
176+
// For array of objects, we need to format according to columns
177+
if len(tv.columns) > 1 && result.IsObject() {
178+
newRow = make(table.Row, len(tv.columns))
179+
for i, col := range tv.columns {
180+
newRow[i] = formatValue(result.Get(col.Title), raw)
181+
}
182+
}
183+
184+
rows := tv.table.Rows()
185+
rows = append(rows, newRow)
186+
tv.table.SetRows(rows)
187+
188+
// Resize columns to accommodate the new data
189+
tv.Resize(tv.width, tv.height)
190+
191+
tv.isLoading = false
192+
return nil
193+
}
194+
}
195+
125196
func (tv *TableView) Resize(width, height int) {
197+
tv.width = width
198+
tv.height = height
126199
tv.updateColumnWidths(width)
127200
tv.table.SetHeight(min(height-heightOffset, tableMinHeight+len(tv.table.Rows())))
128201
}
@@ -190,7 +263,8 @@ type TextView struct {
190263
func (tv *TextView) GetPath() string { return tv.path }
191264
func (tv *TextView) GetData() gjson.Result { return tv.data }
192265
func (tv *TextView) View() string { return tv.viewport.View() }
193-
func (tv *TextView) Update(msg tea.Msg) tea.Cmd {
266+
267+
func (tv *TextView) Update(msg tea.Msg, raw bool) tea.Cmd {
194268
var cmd tea.Cmd
195269
tv.viewport, cmd = tv.viewport.Update(msg)
196270
return cmd
@@ -218,12 +292,57 @@ type JSONViewer struct {
218292
help help.Model
219293
}
220294

295+
// ExploreJSON explores a single JSON value known ahead of time
221296
func ExploreJSON(title string, json gjson.Result) error {
222297
view, err := newView("", json, false)
223298
if err != nil {
224299
return err
225300
}
226301

302+
viewer := &JSONViewer{stack: []JSONView{view}, root: title, rawMode: false, help: help.New()}
303+
304+
_, err = tea.NewProgram(viewer).Run()
305+
if viewer.message != "" {
306+
_, msgErr := fmt.Println("\n" + viewer.message)
307+
err = errors.Join(err, msgErr)
308+
}
309+
return err
310+
}
311+
312+
// ExploreJSONStream explores JSON data loaded incrementally via an iterator
313+
func ExploreJSONStream[T any](title string, it Iterator[T]) error {
314+
anyIt := genericToAnyIterator(it)
315+
316+
preloadCount := 20
317+
if termHeight, _, err := term.GetSize(os.Stdout.Fd()); err == nil {
318+
preloadCount = termHeight
319+
}
320+
321+
items := make([]any, 0, preloadCount)
322+
for i := 0; i < preloadCount && anyIt.Next(); i++ {
323+
items = append(items, anyIt.Current())
324+
}
325+
326+
if err := anyIt.Err(); err != nil {
327+
return err
328+
}
329+
330+
// Convert items to JSON array
331+
jsonBytes, err := json.Marshal(items)
332+
if err != nil {
333+
return err
334+
}
335+
arrayJSON := gjson.ParseBytes(jsonBytes)
336+
view, err := newTableView("", arrayJSON, false)
337+
if err != nil {
338+
return err
339+
}
340+
341+
// Set iterator if there might be more data
342+
if len(items) == preloadCount {
343+
view.iterator = anyIt
344+
}
345+
227346
viewer := &JSONViewer{stack: []JSONView{view}, root: title, rawMode: false, help: help.New()}
228347
_, err = tea.NewProgram(viewer).Run()
229348
if viewer.message != "" {
@@ -265,7 +384,7 @@ func (v *JSONViewer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
265384
}
266385
}
267386

268-
return v, v.current().Update(msg)
387+
return v, v.current().Update(msg, v.rawMode)
269388
}
270389

271390
func (v *JSONViewer) getSelectedContent() string {
@@ -343,11 +462,16 @@ func (v *JSONViewer) toggleRaw() (tea.Model, tea.Cmd) {
343462
v.rawMode = !v.rawMode
344463

345464
for i, view := range v.stack {
346-
rawView, err := newView(view.GetPath(), view.GetData(), v.rawMode)
465+
viewWithRaw, err := newView(view.GetPath(), view.GetData(), v.rawMode)
347466
if err != nil {
348467
return v, tea.Printf("Error: %s", err)
349468
}
350-
v.stack[i] = rawView
469+
if newTV, ok := viewWithRaw.(*TableView); ok {
470+
if tv, ok := view.(*TableView); ok && tv.iterator != nil {
471+
newTV.iterator = tv.iterator
472+
}
473+
}
474+
v.stack[i] = viewWithRaw
351475
}
352476

353477
v.resize(v.width, v.height)
@@ -431,7 +555,13 @@ func newArrayTableView(path string, data gjson.Result, array []gjson.Result, raw
431555
}
432556

433557
t := createTable(columns, rows, arrayColor)
434-
return &TableView{path: path, data: data, table: t, rowData: rowData}
558+
return &TableView{
559+
path: path,
560+
data: data,
561+
table: t,
562+
rowData: rowData,
563+
columns: columns,
564+
}
435565
}
436566

437567
func newArrayOfObjectsTableView(path string, data gjson.Result, array []gjson.Result, raw bool) *TableView {
@@ -462,7 +592,13 @@ func newArrayOfObjectsTableView(path string, data gjson.Result, array []gjson.Re
462592
}
463593

464594
t := createTable(columns, rows, arrayColor)
465-
return &TableView{path: path, data: data, table: t, rowData: rowData}
595+
return &TableView{
596+
path: path,
597+
data: data,
598+
table: t,
599+
rowData: rowData,
600+
columns: columns,
601+
}
466602
}
467603

468604
func newObjectTableView(path string, data gjson.Result, raw bool) *TableView {
@@ -489,7 +625,13 @@ func newObjectTableView(path string, data gjson.Result, raw bool) *TableView {
489625
}
490626

491627
t := createTable(columns, rows, objectColor)
492-
return &TableView{path: path, data: data, table: t, rowData: rowData}
628+
return &TableView{
629+
path: path,
630+
data: data,
631+
table: t,
632+
rowData: rowData,
633+
columns: columns,
634+
}
493635
}
494636

495637
func createTable(columns []table.Column, rows []table.Row, bgColor lipgloss.Color) table.Model {
@@ -588,3 +730,46 @@ func sum(ints []int) int {
588730
}
589731
return total
590732
}
733+
734+
// An iterator over `any` values
735+
type AnyIterator interface {
736+
Next() bool
737+
Err() error
738+
Current() any
739+
}
740+
741+
// A generic iterator interface that is used by the `genericIterator` struct
742+
// below to convert iterators over specific types to an AnyIterator
743+
type Iterator[T any] interface {
744+
Next() bool
745+
Err() error
746+
Current() T
747+
}
748+
749+
// genericIterator adapts a generic Iterator[T] to an AnyIterator.
750+
type genericIterator[T any] struct {
751+
iterator Iterator[T]
752+
current any
753+
}
754+
755+
func (g *genericIterator[T]) Next() bool {
756+
if !g.iterator.Next() {
757+
return false
758+
}
759+
g.current = g.iterator.Current()
760+
return true
761+
}
762+
763+
func (g *genericIterator[T]) Err() error {
764+
return g.iterator.Err()
765+
}
766+
767+
func (g *genericIterator[T]) Current() any {
768+
return g.current
769+
}
770+
771+
func genericToAnyIterator[T any](it Iterator[T]) AnyIterator {
772+
return &genericIterator[T]{
773+
iterator: it,
774+
}
775+
}

pkg/cmd/cmdutil.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"io"
67
"log"
@@ -213,6 +214,7 @@ func shouldUseColors(w io.Writer) bool {
213214
return isTerminal(w)
214215
}
215216

217+
// Display JSON to the user in various different formats
216218
func ShowJSON(out *os.File, title string, res gjson.Result, format string, transform string) error {
217219
if format != "raw" && transform != "" {
218220
transformed := res.Get(transform)
@@ -265,3 +267,25 @@ func ShowJSON(out *os.File, title string, res gjson.Result, format string, trans
265267
return fmt.Errorf("Invalid format: %s, valid formats are: %s", format, strings.Join(OutputFormats, ", "))
266268
}
267269
}
270+
271+
// For an iterator over different value types, display its values to the user in
272+
// different formats.
273+
func ShowJSONIterator[T any](out *os.File, title string, iter jsonview.Iterator[T], format string, transform string) error {
274+
if format == "explore" {
275+
return jsonview.ExploreJSONStream(title, iter)
276+
}
277+
return streamOutput(title, func(w *os.File) error {
278+
for iter.Next() {
279+
item := iter.Current()
280+
jsonData, err := json.Marshal(item)
281+
if err != nil {
282+
return err
283+
}
284+
obj := gjson.ParseBytes(jsonData)
285+
if err := ShowJSON(out, title, obj, format, transform); err != nil {
286+
return err
287+
}
288+
}
289+
return iter.Err()
290+
})
291+
}

pkg/cmd/instance.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ package cmd
44

55
import (
66
"context"
7-
"encoding/json"
87
"fmt"
98
"os"
109

0 commit comments

Comments
 (0)