Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 269 additions & 13 deletions cmd/jsonpath/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,252 @@ import (
"os"

"github.com/bhmj/jsonslice"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
"github.com/spf13/cobra"
"github.com/tidwall/pretty"
)

const (
initialInputs = 2
maxInputs = 2
minInputs = 1
helpHeight = 5
)

var (
cursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212"))

cursorLineStyle = lipgloss.NewStyle().
Background(lipgloss.Color("57")).
Foreground(lipgloss.Color("230"))

placeholderStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("238"))

endOfBufferStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("235"))

focusedPlaceholderStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("99"))

focusedBorderStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("238"))

blurredBorderStyle = lipgloss.NewStyle().
Border(lipgloss.NormalBorder(), false, true, false, false)
)

type keymap = struct {
next, delete, quit key.Binding
}

func newTextarea() textarea.Model {
t := textarea.New()
t.Prompt = ""
t.Placeholder = "Type something"
t.ShowLineNumbers = true
t.Cursor.Style = cursorStyle
t.FocusedStyle.Placeholder = focusedPlaceholderStyle
t.BlurredStyle.Placeholder = placeholderStyle
t.FocusedStyle.CursorLine = cursorLineStyle
t.FocusedStyle.Base = focusedBorderStyle
t.BlurredStyle.Base = blurredBorderStyle
t.FocusedStyle.EndOfBuffer = endOfBufferStyle
t.BlurredStyle.EndOfBuffer = endOfBufferStyle
t.KeyMap.DeleteWordBackward.SetEnabled(false)
t.KeyMap.LineNext = key.NewBinding(key.WithKeys("down"))
t.KeyMap.LinePrevious = key.NewBinding(key.WithKeys("up"))
t.Blur()
return t
}

func newTextareaResults() textarea.Model {
t := textarea.New()
t.Prompt = ""
t.Placeholder = "Results"
t.ShowLineNumbers = false
t.Cursor.Style = cursorStyle
t.FocusedStyle.Placeholder = focusedPlaceholderStyle
t.BlurredStyle.Placeholder = placeholderStyle
t.FocusedStyle.CursorLine = cursorLineStyle
t.FocusedStyle.Base = focusedBorderStyle
t.BlurredStyle.Base = blurredBorderStyle
t.FocusedStyle.EndOfBuffer = endOfBufferStyle
t.BlurredStyle.EndOfBuffer = endOfBufferStyle
t.KeyMap.DeleteWordBackward.SetEnabled(false)
t.KeyMap.DeleteCharacterForward.SetEnabled(false)
t.KeyMap.LineNext = key.NewBinding(key.WithKeys("down"))
t.KeyMap.LinePrevious = key.NewBinding(key.WithKeys("up"))
t.Blur()
return t
}

type editorModel struct {
textInput textinput.Model
width int
height int
keymap keymap
help help.Model
initialInput []byte
input textarea.Model
result textarea.Model
}

func (m editorModel) Init() tea.Cmd {
return textarea.Blink
}

func (m editorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd

switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, m.keymap.quit):
m.textInput.Blur()
m.input.Blur()
m.result.Blur()
return m, tea.Quit
case key.Matches(msg, m.keymap.next):
if m.textInput.Focused() {
m.input.Focus()
m.textInput.Blur()
m.result.Blur()
m.keymap.next.SetHelp("tab", "(switch to results)")
} else if m.input.Focused() {
m.input.Blur()
m.textInput.Blur()
m.result.Focus()
m.keymap.next.SetHelp("tab", "(switch to jsonPath query)")
} else if m.result.Focused() {
m.input.Blur()
m.result.Blur()
m.textInput.Focus()
m.keymap.next.SetHelp("tab", "(switch to editor)")
}
case key.Matches(msg, m.keymap.delete):
m.input.SetValue("")
}

case tea.WindowSizeMsg:
m.height = msg.Height
m.width = msg.Width
}

m.sizeInputs()

if !m.result.Focused() {
result, err := jsonslice.Get([]byte(m.input.Value()), m.textInput.Value())

if err != nil {
m.result.SetValue(err.Error())
} else {
m.result.SetValue(string(result))
}
}

var cmd tea.Cmd

m.textInput, cmd = m.textInput.Update(msg)
cmds = append(cmds, cmd)

m.input, cmd = m.input.Update(msg)
cmds = append(cmds, cmd)

m.result, cmd = m.result.Update(msg)
cmds = append(cmds, cmd)

// Call jsonslice and evaluate path

return m, tea.Batch(cmds...)
}

func (m *editorModel) sizeInputs() {
m.input.SetWidth(m.width / 2)
m.input.SetHeight(m.height - helpHeight - 6)

m.result.SetWidth(m.width / 2)
m.result.SetHeight(m.height - helpHeight - 6)
}

func (m editorModel) View() string {
help := m.help.ShortHelpView([]key.Binding{
m.keymap.next,
m.keymap.delete,
m.keymap.quit,
})

title := lipgloss.NewStyle().Width(40).MaxWidth(40).Height(2).MarginTop(1).Render("Enter jsonPath Query")

input := lipgloss.NewStyle().Width(40).Height(2).Render(m.textInput.View())

leftAreaTitle := lipgloss.NewStyle().Bold(true).Height(1).Render("Input")

rightAreaTitle := lipgloss.NewStyle().Bold(true).Height(1).Render("Results")

left := lipgloss.NewStyle().Render(m.input.View())
right := lipgloss.NewStyle().Render(m.result.View())

leftSection := lipgloss.JoinVertical(lipgloss.Left, leftAreaTitle, left)
rightSection := lipgloss.JoinVertical(lipgloss.Left, rightAreaTitle, right)

splitStyle := lipgloss.NewStyle().Width(m.width / 2)
leftStyled := splitStyle.Render(leftSection)
rightStyled := splitStyle.Render(rightSection)

textAreas := lipgloss.JoinHorizontal(lipgloss.Top, leftStyled, rightStyled)

return lipgloss.JoinVertical(lipgloss.Left, title, input, textAreas, help)
}

func newModel(defaultJson []byte) editorModel {
ti := textinput.New()
ti.Placeholder = "$.requestedItemsStatus[0].id"
ti.CharLimit = 1000
ti.SetValue("$.requestedItemsStatus[0].id")
ti.Focus()
ti.Width = 500

m := editorModel{
textInput: ti,
input: newTextarea(),
result: newTextareaResults(),
help: help.New(),
keymap: keymap{
next: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "(switch to editor)"),
),
delete: key.NewBinding(
key.WithKeys("shift+tab"),
key.WithHelp("shift+tab", "Clear editor"),
),
quit: key.NewBinding(
key.WithKeys("esc", "ctrl+c"),
key.WithHelp("esc", "quit"),
),
},
}

if defaultJson != nil {
m.initialInput = defaultJson
m.input.SetValue(string(defaultJson))
m.textInput.SetValue("$")
} else {
m.input.SetValue("{\r \"accessRequestId\": \"2c91808b6ef1d43e016efba0ce470904\",\r \"requestedFor\": {\r \"type\": \"IDENTITY\",\r \"id\": \"2c91808568c529c60168cca6f90c1313\",\r \"name\": \"William Wilson\"\r },\r \"requestedItemsStatus\": [\r {\r \"id\": \"2c91808b6ef1d43e016efba0ce470904\",\r \"name\": \"Engineering Access\",\r \"description\": \"Access to engineering database\",\r \"type\": \"ACCESS_PROFILE\",\r \"operation\": \"Add\",\r \"comment\": \"William needs this access to do his job.\",\r \"clientMetadata\": {\r \"applicationName\": \"My application\"\r },\r \"approvalInfo\": [\r {\r \"approvalComment\": \"This access looks good. Approved.\",\r \"approvalDecision\": \"APPROVED\",\r \"approverName\": \"Stephen.Austin\",\r \"approver\": {\r \"type\": \"IDENTITY\",\r \"id\": \"2c91808568c529c60168cca6f90c1313\",\r \"name\": \"William Wilson\"\r }\r }\r ]\r }\r ],\r \"requestedBy\": {\r \"type\": \"IDENTITY\",\r \"id\": \"2c91808568c529c60168cca6f90c1313\",\r \"name\": \"William Wilson\"\r }\r}")
}

m.textInput.Focus()
return m
}

func newEvalCommand() *cobra.Command {
var filepath string
var path string
Expand All @@ -32,24 +273,39 @@ func newEvalCommand() *cobra.Command {
if err != nil {
return err
}
} else {
log.Error("You must provide a file to preview")
}

result, err := jsonslice.Get(data, path)
if path != "" {
result, err := jsonslice.Get(data, path)

if err != nil {
return err
}
if err != nil {
return err
}

// Format the JSON
formattedJSON := pretty.Pretty([]byte(result))

// Color the JSON
coloredJSON := pretty.Color(formattedJSON, nil)

// Print formatted and colored JSON
fmt.Print(string(coloredJSON))

// Format the JSON
formattedJSON := pretty.Pretty([]byte(result))
} else {

// Color the JSON
coloredJSON := pretty.Color(formattedJSON, nil)
if _, err := tea.NewProgram(newModel(data), tea.WithAltScreen()).Run(); err != nil {
fmt.Println("Error while running program:", err)
log.Fatal(err)
}
}

// Print formatted and colored JSON
fmt.Print(string(coloredJSON))
} else if filepath == "" && path != "" {
fmt.Println("You must provide a file to evaluate the path against")
} else {
if _, err := tea.NewProgram(newModel(nil), tea.WithAltScreen()).Run(); err != nil {
fmt.Println("Error while running program:", err)
log.Fatal(err)
}
}

return nil

Expand Down
23 changes: 13 additions & 10 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ go 1.21

require (
github.com/bhmj/jsonslice v1.1.3
github.com/charmbracelet/bubbles v0.16.1
github.com/charmbracelet/bubbletea v0.24.2
github.com/charmbracelet/bubbles v0.20.1-0.20241115133003-398e92c5ae72
github.com/charmbracelet/bubbletea v1.1.2
github.com/charmbracelet/glamour v0.6.0
github.com/charmbracelet/lipgloss v0.8.0
github.com/charmbracelet/lipgloss v1.0.0
github.com/charmbracelet/log v0.2.4
github.com/fatih/color v1.15.0
github.com/golang/mock v1.6.0
Expand All @@ -22,6 +22,7 @@ require (
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.18.1
github.com/tidwall/pretty v1.2.1
github.com/vbauerster/mpb/v8 v8.6.1
github.com/zalando/go-keyring v0.2.3
golang.org/x/crypto v0.21.0
Expand All @@ -47,9 +48,11 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bhmj/xpression v0.9.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/charmbracelet/x/ansi v0.4.2 // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/danieljoos/wincred v1.2.0 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
Expand All @@ -62,20 +65,20 @@ require (
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
Expand All @@ -84,8 +87,8 @@ require (
github.com/yuin/goldmark-emoji v1.0.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
Expand Down
Loading