Add HTTP headers parsing

pull/297/head
Alex Kunin 3 months ago
parent 8f35139a5e
commit eabbebbdb2

@ -6,6 +6,16 @@ import (
jsonpath "github.com/antonmedv/fx/path"
)
type NodeKind uint8
const (
Unspecified NodeKind = iota
HttpStatus
HttpHeader
HttpHeaderContinuation
HttpBlank
)
type Node struct {
Prev, Next, End *Node
directParent *Node
@ -19,6 +29,7 @@ type Node struct {
ChunkEnd *Node
Comma bool
Index int
Kind NodeKind
}
// append ands a node as a child to the current node (body of {...} or [...]).

@ -8,7 +8,7 @@ import (
func DropWrapAll(n *Node) {
for n != nil {
if n.Value != nil && n.Value[0] == '"' {
if n.Value != nil && (n.Value[0] == '"' || n.Kind == HttpHeader) {
n.dropChunks()
}
if n.IsCollapsed() {
@ -24,7 +24,7 @@ func WrapAll(n *Node, termWidth int) {
return
}
for n != nil {
if n.Value != nil && n.Value[0] == '"' {
if n.Value != nil && (n.Value[0] == '"' || n.Kind == HttpHeader) {
n.dropChunks()
lines, count := doWrap(n, termWidth)
if count > 1 {

@ -15,17 +15,20 @@ import (
)
type Theme struct {
Cursor Color
Syntax Color
Preview Color
StatusBar Color
Search Color
Key Color
String Color
Null Color
Boolean Color
Number Color
Size Color
Cursor Color
Syntax Color
Preview Color
StatusBar Color
Search Color
Key Color
String Color
Null Color
Boolean Color
Number Color
Size Color
HttpStatus Color
HttpHeaderName Color
HttpHeaderValue Color
}
type Color func(s []byte) []byte
@ -119,150 +122,183 @@ var (
var themes = map[string]Theme{
"0": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: noColor,
StatusBar: noColor,
Search: defaultSearch,
Key: noColor,
String: noColor,
Null: noColor,
Boolean: noColor,
Number: noColor,
Size: noColor,
Cursor: defaultCursor,
Syntax: noColor,
Preview: noColor,
StatusBar: noColor,
Search: defaultSearch,
Key: noColor,
String: noColor,
Null: noColor,
Boolean: noColor,
Number: noColor,
Size: noColor,
HttpStatus: noColor,
HttpHeaderName: noColor,
HttpHeaderValue: noColor,
},
"1": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: boldFg("4"),
String: fg("2"),
Null: defaultNull,
Boolean: fg("5"),
Number: fg("6"),
Size: defaultSize,
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: boldFg("4"),
String: fg("2"),
Null: defaultNull,
Boolean: fg("5"),
Number: fg("6"),
Size: defaultSize,
HttpStatus: fg("2"),
HttpHeaderName: boldFg("4"),
HttpHeaderValue: fg("2"),
},
"2": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: fg("2"),
String: fg("4"),
Null: defaultNull,
Boolean: fg("5"),
Number: fg("6"),
Size: defaultSize,
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: fg("2"),
String: fg("4"),
Null: defaultNull,
Boolean: fg("5"),
Number: fg("6"),
Size: defaultSize,
HttpStatus: fg("4"),
HttpHeaderName: fg("2"),
HttpHeaderValue: fg("4"),
},
"3": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: fg("13"),
String: fg("11"),
Null: defaultNull,
Boolean: fg("1"),
Number: fg("14"),
Size: defaultSize,
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: fg("13"),
String: fg("11"),
Null: defaultNull,
Boolean: fg("1"),
Number: fg("14"),
Size: defaultSize,
HttpStatus: fg("11"),
HttpHeaderName: fg("13"),
HttpHeaderValue: fg("11"),
},
"4": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: fg("#00F5D4"),
String: fg("#00BBF9"),
Null: defaultNull,
Boolean: fg("#F15BB5"),
Number: fg("#9B5DE5"),
Size: defaultSize,
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: fg("#00F5D4"),
String: fg("#00BBF9"),
Null: defaultNull,
Boolean: fg("#F15BB5"),
Number: fg("#9B5DE5"),
Size: defaultSize,
HttpStatus: fg("#00BBF9"),
HttpHeaderName: fg("#00F5D4"),
HttpHeaderValue: fg("#00BBF9"),
},
"5": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: fg("#faf0ca"),
String: fg("#f4d35e"),
Null: defaultNull,
Boolean: fg("#ee964b"),
Number: fg("#ee964b"),
Size: defaultSize,
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: fg("#faf0ca"),
String: fg("#f4d35e"),
Null: defaultNull,
Boolean: fg("#ee964b"),
Number: fg("#ee964b"),
Size: defaultSize,
HttpStatus: fg("#f4d35e"),
HttpHeaderName: fg("#faf0ca"),
HttpHeaderValue: fg("#f4d35e"),
},
"6": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: fg("#4D96FF"),
String: fg("#6BCB77"),
Null: defaultNull,
Boolean: fg("#FF6B6B"),
Number: fg("#FFD93D"),
Size: defaultSize,
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: fg("#4D96FF"),
String: fg("#6BCB77"),
Null: defaultNull,
Boolean: fg("#FF6B6B"),
Number: fg("#FFD93D"),
Size: defaultSize,
HttpStatus: fg("#6BCB77"),
HttpHeaderName: fg("#4D96FF"),
HttpHeaderValue: fg("#6BCB77"),
},
"7": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: boldFg("42"),
String: boldFg("213"),
Null: defaultNull,
Boolean: boldFg("201"),
Number: boldFg("201"),
Size: defaultSize,
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: boldFg("42"),
String: boldFg("213"),
Null: defaultNull,
Boolean: boldFg("201"),
Number: boldFg("201"),
Size: defaultSize,
HttpStatus: boldFg("213"),
HttpHeaderName: boldFg("42"),
HttpHeaderValue: boldFg("213"),
},
"8": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: boldFg("51"),
String: fg("195"),
Null: defaultNull,
Boolean: fg("50"),
Number: fg("123"),
Size: defaultSize,
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: boldFg("51"),
String: fg("195"),
Null: defaultNull,
Boolean: fg("50"),
Number: fg("123"),
Size: defaultSize,
HttpStatus: fg("195"),
HttpHeaderName: boldFg("51"),
HttpHeaderValue: fg("195"),
},
"🔵": {
Cursor: toColor(lipgloss.NewStyle().
Foreground(lipgloss.Color("15")).
Background(lipgloss.Color("33")).
Render),
Syntax: boldFg("33"),
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: fg("33"),
String: noColor,
Null: noColor,
Boolean: noColor,
Number: noColor,
Size: defaultSize,
Syntax: boldFg("33"),
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: fg("33"),
String: noColor,
Null: noColor,
Boolean: noColor,
Number: noColor,
Size: defaultSize,
HttpStatus: noColor,
HttpHeaderName: fg("33"),
HttpHeaderValue: noColor,
},
"🥝": {
Cursor: defaultCursor,
Syntax: fg("179"),
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: boldFg("154"),
String: fg("82"),
Null: fg("230"),
Boolean: fg("226"),
Number: fg("226"),
Size: defaultSize,
Cursor: defaultCursor,
Syntax: fg("179"),
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: boldFg("154"),
String: fg("82"),
Null: fg("230"),
Boolean: fg("226"),
Number: fg("226"),
Size: defaultSize,
HttpStatus: fg("82"),
HttpHeaderName: boldFg("154"),
HttpHeaderValue: fg("82"),
},
}

@ -1,10 +1,12 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"github.com/antonmedv/fx/http_response"
"io"
"io/fs"
"math"
@ -136,6 +138,8 @@ func main() {
panic(err)
}
var head *Node
if flagYaml {
data, err = yaml.YAMLToJSON(data)
if err != nil {
@ -143,13 +147,26 @@ func main() {
os.Exit(1)
return
}
}
head, err := Parse(data)
if err != nil {
fmt.Print(err.Error())
os.Exit(1)
return
head, err = Parse(data)
if err != nil {
fmt.Print(err.Error())
os.Exit(1)
return
}
} else if string(data[0:5]) == "HTTP/" {
head, err = formatHttpResponse(data)
if err != nil {
fmt.Print(err.Error())
os.Exit(1)
return
}
} else {
head, err = Parse(data)
if err != nil {
fmt.Print(err.Error())
os.Exit(1)
return
}
}
digInput := textinput.New()
@ -200,6 +217,59 @@ func main() {
}
}
func formatHttpResponse(data []byte) (*Node, error) {
response, err := http_response.NewHttpResponseFromText(data)
if err != nil {
return nil, err
}
result := &Node{
Value: response.Status(),
Kind: HttpStatus,
}
lastNode := result
for _, header := range response.Headers() {
for i, value := range bytes.Split(header.Value(), []byte{'\n'}) {
var key []byte
kind := HttpHeaderContinuation
if i == 0 {
key = header.Name()
kind = HttpHeader
}
child := &Node{
Key: key,
Value: value,
Kind: kind,
}
lastNode.Next = child
child.Prev = lastNode
lastNode = child
}
}
blank := &Node{
Kind: HttpBlank,
}
lastNode.Next = blank
blank.Prev = lastNode
lastNode = blank
body, err := Parse(response.Body())
if err != nil {
return nil, err
}
lastNode.Next = body
body.Prev = lastNode
return result, nil
}
type model struct {
termWidth, termHeight int
head, top *Node
@ -706,37 +776,55 @@ func (m *model) View() string {
isSelected = false // don't highlight the cursor while iterating search results
}
if n.Key != nil {
screen = append(screen, m.prettyKey(n, isSelected)...)
switch true {
case n.Kind == HttpStatus:
screen = append(screen, m.prettyHttpElement(n.Value, theme.CurrentTheme.HttpStatus, isSelected, m.search.values[n])...)
case n.Kind == HttpHeader:
screen = append(screen, m.prettyHttpElement(n.Key, theme.CurrentTheme.HttpHeaderName, isSelected, m.search.keys[n])...)
screen = append(screen, theme.Colon...)
isSelected = false // don't highlight the key's value
}
screen = append(screen, m.prettyHttpElement(n.Value, theme.CurrentTheme.HttpHeaderValue, false, m.search.values[n])...)
case n.Kind == HttpHeaderContinuation:
screen = append(screen, m.prettyHttpElement([]byte{' '}, theme.CurrentTheme.HttpHeaderName, isSelected, m.search.keys[n])...)
screen = append(screen, m.prettyHttpElement(n.Value[1:], theme.CurrentTheme.HttpHeaderValue, false, m.search.values[n])...)
case n.Kind == HttpBlank:
if isSelected {
screen = append(screen, theme.CurrentTheme.Cursor([]byte{' '})...)
}
case n.Kind == Unspecified:
fallthrough
default:
if n.Key != nil {
screen = append(screen, m.prettyKey(n, isSelected)...)
screen = append(screen, theme.Colon...)
isSelected = false // don't highlight the key's value
}
screen = append(screen, m.prettyPrint(n, isSelected)...)
screen = append(screen, m.prettyPrint(n, isSelected)...)
if n.IsCollapsed() {
if n.Value[0] == '{' {
if n.Collapsed.Key != nil {
screen = append(screen, theme.CurrentTheme.Preview(n.Collapsed.Key)...)
screen = append(screen, theme.ColonPreview...)
if n.IsCollapsed() {
if n.Value[0] == '{' {
if n.Collapsed.Key != nil {
screen = append(screen, theme.CurrentTheme.Preview(n.Collapsed.Key)...)
screen = append(screen, theme.ColonPreview...)
}
screen = append(screen, theme.Dot3...)
screen = append(screen, theme.CloseCurlyBracket...)
} else if n.Value[0] == '[' {
screen = append(screen, theme.Dot3...)
screen = append(screen, theme.CloseSquareBracket...)
}
if n.End != nil && n.End.Comma {
screen = append(screen, theme.Comma...)
}
screen = append(screen, theme.Dot3...)
screen = append(screen, theme.CloseCurlyBracket...)
} else if n.Value[0] == '[' {
screen = append(screen, theme.Dot3...)
screen = append(screen, theme.CloseSquareBracket...)
}
if n.End != nil && n.End.Comma {
if n.Comma {
screen = append(screen, theme.Comma...)
}
}
if n.Comma {
screen = append(screen, theme.Comma...)
}
if theme.ShowSizes && len(n.Value) > 0 && (n.Value[0] == '{' || n.Value[0] == '[') {
if n.IsCollapsed() || n.Size > 1 {
screen = append(screen, theme.CurrentTheme.Size([]byte(fmt.Sprintf(" // %d", n.Size)))...)
if theme.ShowSizes && len(n.Value) > 0 && (n.Value[0] == '{' || n.Value[0] == '[') {
if n.IsCollapsed() || n.Size > 1 {
screen = append(screen, theme.CurrentTheme.Size([]byte(fmt.Sprintf(" // %d", n.Size)))...)
}
}
}
@ -839,6 +927,32 @@ func (m *model) prettyPrint(node *Node, selected bool) []byte {
}
}
func (m *model) prettyHttpElement(text []byte, style theme.Color, selected bool, indexes []match) (out []byte) {
if len(text) == 0 {
return text
}
if selected {
style = theme.CurrentTheme.Cursor
}
if indexes == nil {
return style(text)
}
for i, p := range splitBytesByIndexes(text, indexes) {
if i%2 == 0 {
out = append(out, style(p.b)...)
} else if p.index == m.search.cursor {
out = append(out, theme.CurrentTheme.Cursor(p.b)...)
} else {
out = append(out, theme.CurrentTheme.Search(p.b)...)
}
}
return
}
func (m *model) viewHeight() int {
if m.searchInput.Focused() || m.searchInput.Value() != "" {
return m.termHeight - 2
@ -927,8 +1041,8 @@ func (m *model) selectNode(n *Node) {
func (m *model) cursorPath() string {
path := ""
at := m.cursorPointsTo()
for at != nil {
if at.Prev != nil {
for at != nil && at.Kind == Unspecified {
if at.Prev != nil && at.Prev.Kind == Unspecified {
if at.Chunk != nil && at.Value == nil {
at = at.Parent()
}

@ -22,7 +22,7 @@ func init() {
lipgloss.SetColorProfile(termenv.ANSI)
}
func prepare(t *testing.T) *teatest.TestModel {
func prepareFromJSON(t *testing.T) *teatest.TestModel {
file, err := os.Open("testdata/example.json")
require.NoError(t, err)
@ -48,6 +48,32 @@ func prepare(t *testing.T) *teatest.TestModel {
return tm
}
func prepareFromHTTP(t *testing.T) *teatest.TestModel {
file, err := os.Open("testdata/example.curl")
require.NoError(t, err)
data, err := io.ReadAll(file)
require.NoError(t, err)
head, err := formatHttpResponse(data)
require.NoError(t, err)
m := &model{
top: head,
head: head,
wrap: true,
showCursor: true,
digInput: textinput.New(),
searchInput: textinput.New(),
search: newSearch(),
}
tm := teatest.NewTestModel(
t, m,
teatest.WithInitialTermSize(80, 40),
)
return tm
}
func read(t *testing.T, tm *teatest.TestModel) []byte {
var out []byte
teatest.WaitFor(t,
@ -63,7 +89,7 @@ func read(t *testing.T, tm *teatest.TestModel) []byte {
}
func TestOutput(t *testing.T) {
tm := prepare(t)
tm := prepareFromJSON(t)
teatest.RequireEqualOutput(t, read(t, tm))
@ -72,7 +98,7 @@ func TestOutput(t *testing.T) {
}
func TestNavigation(t *testing.T) {
tm := prepare(t)
tm := prepareFromJSON(t)
tm.Send(tea.KeyMsg{Type: tea.KeyDown})
tm.Send(tea.KeyMsg{Type: tea.KeyDown})
@ -84,7 +110,7 @@ func TestNavigation(t *testing.T) {
}
func TestDig(t *testing.T) {
tm := prepare(t)
tm := prepareFromJSON(t)
tm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(".")})
tm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("year")})
@ -96,7 +122,7 @@ func TestDig(t *testing.T) {
}
func TestCollapseRecursive(t *testing.T) {
tm := prepare(t)
tm := prepareFromJSON(t)
tm.Send(tea.KeyMsg{Type: tea.KeyShiftLeft})
teatest.RequireEqualOutput(t, read(t, tm))
@ -109,7 +135,7 @@ func TestCollapseRecursiveWithSizes(t *testing.T) {
theme.ShowSizes = true
defer func() { theme.ShowSizes = true }()
tm := prepare(t)
tm := prepareFromJSON(t)
tm.Send(tea.KeyMsg{Type: tea.KeyShiftLeft})
teatest.RequireEqualOutput(t, read(t, tm))
@ -117,3 +143,12 @@ func TestCollapseRecursiveWithSizes(t *testing.T) {
tm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("q")})
tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))
}
func TestJSONWithHTTPHeaders(t *testing.T) {
tm := prepareFromHTTP(t)
teatest.RequireEqualOutput(t, read(t, tm))
tm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("q")})
tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))
}

@ -0,0 +1,40 @@
[?25lHTTP/1.1 200 OK
date: Sun, 17 Mar 2024 21:58:03 GMT
content-type: application/json; charset=utf-8
content-length: 35
age: 32
x-multiline: a
b
c
x-multivalue: value1
x-multivalue: value2
{ // 2
"key1": "value1",
"key2": "value2"
}
~
~
~
~
~
~
~
~
~
~
~
~
~
~
~
~
~
~
~
~
~
~
~
~


@ -0,0 +1,13 @@
HTTP/1.1 200 OK
date: Sun, 17 Mar 2024 21:58:03 GMT
content-type: application/json; charset=utf-8
content-length: 35
age: 32
x-multiline: a
b
c
x-multivalue: value1
x-multivalue: value2
{"key1": "value1","key2": "value2"}
Loading…
Cancel
Save