Skip to content

Commit 13fa4e7

Browse files
Parse a body file expression (#88)
1 parent a54ae2a commit 13fa4e7

File tree

10 files changed

+211
-17
lines changed

10 files changed

+211
-17
lines changed

internal/syntax/ast/ast_test.go

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,23 @@ func TestNode(t *testing.T) {
7070
end: token.Token{Kind: token.Text, Start: 12, End: 8},
7171
kind: ast.KindVarStatement,
7272
},
73+
{
74+
name: "var statement no value",
75+
// @variable
76+
node: ast.VarStatement{
77+
Value: nil,
78+
Ident: ast.Ident{
79+
Name: "variable",
80+
Token: token.Token{Kind: token.Ident, Start: 1, End: 9},
81+
Type: ast.KindIdent,
82+
},
83+
At: token.Token{Kind: token.At, Start: 0, End: 1},
84+
Type: ast.KindVarStatement,
85+
},
86+
start: token.Token{Kind: token.At, Start: 0, End: 1},
87+
end: token.Token{Kind: token.Ident, Start: 1, End: 9},
88+
kind: ast.KindVarStatement,
89+
},
7390
{
7491
name: "interp only",
7592
// {{ hello }}
@@ -209,6 +226,10 @@ func TestNode(t *testing.T) {
209226
{
210227
name: "request",
211228
node: ast.Request{
229+
Body: ast.Body{
230+
Token: token.Token{Kind: token.Body, Start: 30, End: 110},
231+
Type: ast.KindBody,
232+
},
212233
URL: ast.TextLiteral{
213234
Value: "https://example.com",
214235
Token: token.Token{Kind: token.URL, Start: 9, End: 28},
@@ -222,9 +243,25 @@ func TestNode(t *testing.T) {
222243
Type: ast.KindRequest,
223244
},
224245
start: token.Token{Kind: token.Separator, Start: 0, End: 3},
225-
end: token.Token{Kind: token.URL, Start: 9, End: 28},
246+
end: token.Token{Kind: token.Body, Start: 30, End: 110},
226247
kind: ast.KindRequest,
227248
},
249+
{
250+
name: "inner interp",
251+
node: ast.Interp{
252+
Expr: ast.Ident{
253+
Name: "id",
254+
Token: token.Token{Kind: token.Ident, Start: 3, End: 5},
255+
Type: ast.KindIdent,
256+
},
257+
Open: token.Token{Kind: token.OpenInterp, Start: 0, End: 2},
258+
Close: token.Token{Kind: token.CloseInterp, Start: 6, End: 8},
259+
Type: ast.KindInterp,
260+
},
261+
start: token.Token{Kind: token.OpenInterp, Start: 0, End: 2},
262+
end: token.Token{Kind: token.CloseInterp, Start: 6, End: 8},
263+
kind: ast.KindInterp,
264+
},
228265
{
229266
// https://example/com/{{ version }}/items/123
230267
// |------ left ------|-- interp --|- right -|
@@ -343,6 +380,32 @@ func TestNode(t *testing.T) {
343380
end: token.Token{Kind: token.Body, Start: 12, End: 136},
344381
kind: ast.KindBody,
345382
},
383+
{
384+
name: "body file",
385+
node: ast.BodyFile{
386+
Token: token.Token{Kind: token.LeftAngle, Start: 31, End: 32},
387+
Value: ast.TextLiteral{
388+
Value: "./body.json",
389+
Token: token.Token{Kind: token.Text, Start: 33, End: 44},
390+
Type: ast.KindTextLiteral,
391+
},
392+
Type: ast.KindBodyFile,
393+
},
394+
start: token.Token{Kind: token.LeftAngle, Start: 31, End: 32},
395+
end: token.Token{Kind: token.Text, Start: 33, End: 44},
396+
kind: ast.KindBodyFile,
397+
},
398+
{
399+
name: "body file no value",
400+
node: ast.BodyFile{
401+
Token: token.Token{Kind: token.LeftAngle, Start: 31, End: 32},
402+
Value: nil,
403+
Type: ast.KindBodyFile,
404+
},
405+
start: token.Token{Kind: token.LeftAngle, Start: 31, End: 32},
406+
end: token.Token{Kind: token.LeftAngle, Start: 31, End: 32},
407+
kind: ast.KindBodyFile,
408+
},
346409
{
347410
name: "empty file",
348411
node: ast.File{Type: ast.KindFile},

internal/syntax/ast/expression.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,39 @@ func (b Body) Kind() Kind {
223223

224224
// expressionNode marks a [Body] as an [Expression].
225225
func (b Body) expressionNode() {}
226+
227+
// BodyFile is a http body from a filepath.
228+
type BodyFile struct {
229+
// Value is the expression of the filepath.
230+
Value Expression
231+
232+
// Token is the [token.LeftAngle] token.
233+
Token token.Token
234+
235+
// Type is [KindBodyFile].
236+
Type Kind
237+
}
238+
239+
// Start returns the first token associated with the BodyFile, which
240+
// is the [token.LeftAngle].
241+
func (b BodyFile) Start() token.Token {
242+
return b.Token
243+
}
244+
245+
// End returns the last token associated with the BodyFile, which is
246+
// the final token in the filepath expression.
247+
func (b BodyFile) End() token.Token {
248+
if b.Value != nil {
249+
return b.Value.End()
250+
}
251+
252+
return b.Token
253+
}
254+
255+
// Kind returns [KindBodyFile].
256+
func (b BodyFile) Kind() Kind {
257+
return b.Type
258+
}
259+
260+
// expressionNode marks a [BodyFile] as an [Expression].
261+
func (b BodyFile) expressionNode() {}

internal/syntax/ast/kind.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const (
2121
KindHeader // Header
2222
KindInterpolatedExpression // InterpolatedExpression
2323
KindBody // Body
24+
KindBodyFile // BodyFile
2425
)
2526

2627
// MarshalText implements [encoding.TextMarshaler] for [Kind].

internal/syntax/ast/kind_string.go

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/syntax/ast/statement.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,9 @@ func (r Request) Start() token.Token {
189189

190190
// End returns the last token associated with the [Request].
191191
func (r Request) End() token.Token {
192-
// TODO(@FollowTheProcess): There's more that can come after this like body, response ref etc.
193-
if r.URL != nil {
194-
return r.URL.End()
192+
// TODO(@FollowTheProcess): The response file redirect or response ref should be last
193+
if r.Body != nil {
194+
return r.Body.End()
195195
}
196196

197197
return r.Method.End()

internal/syntax/parser/v2/parser.go

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,9 @@ func (p *Parser) parseRequest() (ast.Request, error) {
464464
result.Headers = append(result.Headers, header)
465465
}
466466

467-
if p.next.Is(token.Body) {
467+
// Body or < <body file>
468+
switch p.next.Kind {
469+
case token.Body:
468470
p.advance()
469471

470472
body, err := p.parseExpression(token.LowestPrecedence)
@@ -473,6 +475,17 @@ func (p *Parser) parseRequest() (ast.Request, error) {
473475
}
474476

475477
result.Body = body
478+
case token.LeftAngle:
479+
p.advance()
480+
481+
bodyFile, err := p.parseBodyFile()
482+
if err != nil {
483+
return result, err
484+
}
485+
486+
result.Body = bodyFile
487+
default:
488+
// Nothing, not all requests have a body
476489
}
477490

478491
return result, nil
@@ -542,21 +555,13 @@ func (p *Parser) parseExpression(precedence int) (ast.Expression, error) {
542555
//
543556
// In our case the Interp is the operator and carries the highest precedence.
544557

545-
for p.next.Is(token.Text, token.OpenInterp, token.Ident, token.URL, token.Body) && precedence < p.next.Precedence() {
558+
for p.next.Is(token.OpenInterp) && precedence < p.next.Precedence() {
546559
p.advance()
547560

548561
switch p.current.Kind {
549562
case token.OpenInterp:
550563
// It's an interpolated expression where we already know the left hand side
551564
expr, err = p.parseInterpolatedExpression(expr)
552-
case token.Text:
553-
expr, err = p.parseTextLiteral()
554-
case token.Ident:
555-
expr, err = p.parseIdent()
556-
case token.URL:
557-
expr, err = p.parseURL()
558-
case token.Body:
559-
expr, err = p.parseBody()
560565
default:
561566
p.errorf("parseExpression: unexpected token: %s", p.current.Kind)
562567
}
@@ -696,3 +701,24 @@ func (p *Parser) parseBody() (ast.Body, error) {
696701

697702
return body, nil
698703
}
704+
705+
// parseBodyFile parses a body file expression.
706+
func (p *Parser) parseBodyFile() (ast.BodyFile, error) {
707+
bodyFile := ast.BodyFile{
708+
Token: p.current,
709+
Type: ast.KindBodyFile,
710+
}
711+
712+
if err := p.expect(token.Text, token.OpenInterp); err != nil {
713+
return bodyFile, err
714+
}
715+
716+
value, err := p.parseExpression(token.LowestPrecedence)
717+
if err != nil {
718+
return bodyFile, err
719+
}
720+
721+
bodyFile.Value = value
722+
723+
return bodyFile, nil
724+
}

internal/syntax/parser/v2/testdata/invalid/bad-body-file.txtar

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ GET https://blowup.com
44

55
< £$%^&
66
-- want.txt --
7-
bad-body-file.txtar:4:1-2: parseStatement: unrecognised token: LeftAngle
7+
bad-body-file.txtar:4:1-2: Error token from scanner
88
bad-body-file.txtar:4:5: unrecognised character: '£'
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
source: parser_test.go
2+
expression: parsed
3+
---
4+
name: body-file.http
5+
statements:
6+
- url:
7+
value: https://api.somewhere.com/items/1
8+
token:
9+
kind: URL
10+
start: 41
11+
end: 74
12+
type: URL
13+
body:
14+
value:
15+
value: input.json
16+
token:
17+
kind: Text
18+
start: 109
19+
end: 119
20+
type: TextLiteral
21+
token:
22+
kind: LeftAngle
23+
start: 107
24+
end: 108
25+
type: BodyFile
26+
vars: []
27+
prompts: []
28+
headers:
29+
- value:
30+
value: application/json
31+
token:
32+
kind: Text
33+
start: 89
34+
end: 105
35+
type: TextLiteral
36+
key: Content-Type
37+
token:
38+
kind: Header
39+
start: 75
40+
end: 87
41+
type: Header
42+
comment:
43+
text: Read the body from ./input.json
44+
token:
45+
kind: Comment
46+
start: 4
47+
end: 35
48+
type: Comment
49+
method:
50+
token:
51+
kind: MethodPost
52+
start: 36
53+
end: 40
54+
type: Method
55+
sep:
56+
kind: Separator
57+
start: 0
58+
end: 3
59+
type: Request
60+
type: File
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### Read the body from ./input.json
2+
POST https://api.somewhere.com/items/1
3+
Content-Type: application/json
4+
5+
< input.json

internal/syntax/scanner/scanner.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ const (
3636
bufferSize = 32 // benchmarks suggest this is the optimum token channel buffer size
3737
)
3838

39+
// TODO(@FollowTheProcess): Allow interp in body file, so < {{ ident }}
40+
3941
// scanFn represents the state of the scanner as a function that does the work
4042
// associated with the current state, then returns the next state.
4143
type scanFn func(*Scanner) scanFn

0 commit comments

Comments
 (0)