Skip to content

Commit 5795910

Browse files
Resolve some simple requests (#101)
* Resolve a minimal request * Resolve request vars and keywords
1 parent 714083d commit 5795910

File tree

3 files changed

+199
-10
lines changed

3 files changed

+199
-10
lines changed

internal/syntax/resolver/resolver.go

Lines changed: 156 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ package resolver
77
import (
88
"errors"
99
"fmt"
10+
"net/http"
11+
"net/url"
1012
"time"
1113

1214
"go.followtheprocess.codes/zap/internal/spec"
@@ -48,22 +50,24 @@ func (r *Resolver) Resolve(in ast.File) (spec.File, error) {
4850
Requests: []spec.Request{},
4951
}
5052

53+
var errs []error
54+
5155
for _, statement := range in.Statements {
5256
newFile, err := r.resolveFileStatement(file, statement)
5357
if err != nil {
5458
// If we can't resolve this one, try carrying on. This ensures we provide
5559
// multiple diagnostics for the user rather than one at a time
60+
errs = append(errs, err)
5661
continue
5762
}
5863

5964
// Update the file
6065
file = newFile
6166
}
6267

63-
// We've had diagnostics reported somewhere during resolving so correctly
64-
// return an error now.
68+
// We've had diagnostics reported during resolving so just bubble up a top level error
6569
if r.hadErrors {
66-
return spec.File{}, ErrResolve
70+
return spec.File{}, fmt.Errorf("%w: %w", ErrResolve, errors.Join(errs...))
6771
}
6872

6973
return file, nil
@@ -101,16 +105,29 @@ func (r *Resolver) resolveFileStatement(file spec.File, statement ast.Statement)
101105
case ast.VarStatement:
102106
file, err = r.resolveGlobalVarStatement(file, stmt)
103107
if err != nil {
104-
return spec.File{}, ErrResolve
108+
return spec.File{}, err
105109
}
106110
case ast.PromptStatement:
107111
file, err = r.resolveGlobalPromptStatement(file, stmt)
108112
if err != nil {
109-
return spec.File{}, ErrResolve
113+
return spec.File{}, err
114+
}
115+
case ast.Request:
116+
request, err := r.resolveRequestStatement(stmt)
117+
if err != nil {
118+
return spec.File{}, err
119+
}
120+
121+
// If it doesn't have a name set, give it a numerical name based
122+
// on it's position in the file e.g. "#1", "#2" etc.
123+
if request.Name == "" {
124+
request.Name = fmt.Sprintf("#%d", len(file.Requests)+1)
110125
}
111126

127+
file.Requests = append(file.Requests, request)
128+
112129
default:
113-
return file, fmt.Errorf("unhandled ast statement: %T", stmt)
130+
return file, fmt.Errorf("unexpected global statement: %T", stmt)
114131
}
115132

116133
return file, nil
@@ -131,12 +148,17 @@ func (r *Resolver) resolveGlobalVarStatement(file spec.File, statement ast.VarSt
131148
value, err := r.resolveExpression(statement.Value)
132149
if err != nil {
133150
r.errorf(statement, "failed to resolve value expression for key %s: %v", key, err)
134-
return spec.File{}, ErrResolve
151+
return spec.File{}, err
135152
}
136153

137154
if !isKeyword {
138155
// Normal var
156+
if file.Vars == nil {
157+
file.Vars = make(map[string]string)
158+
}
159+
139160
file.Vars[key] = value
161+
140162
return file, nil
141163
}
142164

@@ -148,15 +170,15 @@ func (r *Resolver) resolveGlobalVarStatement(file spec.File, statement ast.VarSt
148170
duration, err := time.ParseDuration(value)
149171
if err != nil {
150172
r.errorf(statement.Value, "invalid timeout value: %v", err)
151-
return spec.File{}, ErrResolve
173+
return spec.File{}, err
152174
}
153175

154176
file.Timeout = duration
155177
case token.ConnectionTimeout:
156178
duration, err := time.ParseDuration(value)
157179
if err != nil {
158180
r.errorf(statement.Value, "invalid connection-timeout value: %v", err)
159-
return spec.File{}, ErrResolve
181+
return spec.File{}, err
160182
}
161183

162184
file.ConnectionTimeout = duration
@@ -179,7 +201,7 @@ func (r *Resolver) resolveGlobalPromptStatement(file spec.File, statement ast.Pr
179201

180202
if _, exists := file.Prompts[name]; exists {
181203
r.errorf(statement, "prompt %s already declared", name)
182-
return spec.File{}, ErrResolve
204+
return spec.File{}, fmt.Errorf("prompt %s already declared", name)
183205
}
184206

185207
// Shouldn't need this because file is declared top level with all this
@@ -193,12 +215,136 @@ func (r *Resolver) resolveGlobalPromptStatement(file spec.File, statement ast.Pr
193215
return file, nil
194216
}
195217

218+
// resolveRequestStatement resolves an [ast.Request] into a [spec.Request].
219+
func (r *Resolver) resolveRequestStatement(in ast.Request) (spec.Request, error) {
220+
rawURL, err := r.resolveExpression(in.URL)
221+
if err != nil {
222+
r.errorf(in.URL, "failed to resolve URL expression: %v", err)
223+
return spec.Request{}, err
224+
}
225+
226+
// TODO(@FollowTheProcess): Should the spec.Request store the URL as *url.URL?
227+
//
228+
// This is probably one to change once parser v2 has been swapped in
229+
230+
// Validate the URL here
231+
_, err = url.ParseRequestURI(rawURL)
232+
if err != nil {
233+
r.errorf(in.URL, "invalid URL %s: %v", rawURL, err)
234+
return spec.Request{}, err
235+
}
236+
237+
method, err := r.resolveHTTPMethod(in.Method)
238+
if err != nil {
239+
return spec.Request{}, err
240+
}
241+
242+
request := spec.Request{
243+
Method: method,
244+
URL: rawURL,
245+
}
246+
247+
for _, varStatement := range in.Vars {
248+
request, err = r.resolveRequestVarStatement(request, varStatement)
249+
if err != nil {
250+
return spec.Request{}, err
251+
}
252+
}
253+
254+
return request, nil
255+
}
256+
196257
// resolveExpression resolves an [ast.Expression].
197258
func (r *Resolver) resolveExpression(expression ast.Expression) (string, error) {
198259
switch expr := expression.(type) {
199260
case ast.TextLiteral:
200261
return expr.Value, nil
262+
case ast.URL:
263+
return expr.Value, nil
201264
default:
202265
return "", fmt.Errorf("unhandled ast expression: %T", expr)
203266
}
204267
}
268+
269+
// resolveHTTPMethod resolves an [ast.Method].
270+
func (r *Resolver) resolveHTTPMethod(method ast.Method) (string, error) {
271+
switch method.Token.Kind {
272+
case token.MethodGet:
273+
return http.MethodGet, nil
274+
case token.MethodHead:
275+
return http.MethodHead, nil
276+
case token.MethodPost:
277+
return http.MethodPost, nil
278+
case token.MethodPut:
279+
return http.MethodPut, nil
280+
case token.MethodDelete:
281+
return http.MethodDelete, nil
282+
case token.MethodConnect:
283+
return http.MethodConnect, nil
284+
case token.MethodPatch:
285+
return http.MethodPatch, nil
286+
case token.MethodOptions:
287+
return http.MethodOptions, nil
288+
case token.MethodTrace:
289+
return http.MethodTrace, nil
290+
default:
291+
r.error(method, "invalid HTTP method")
292+
return "", errors.New("invalid HTTP method")
293+
}
294+
}
295+
296+
// resolveRequestVarStatement resolves a variable declaration in the request scope,
297+
// storing it in the request and returning the modified request.
298+
func (r *Resolver) resolveRequestVarStatement(request spec.Request, statement ast.VarStatement) (spec.Request, error) {
299+
key := statement.Ident.Name
300+
301+
kind, isKeyword := token.Keyword(key)
302+
if isKeyword && kind == token.NoRedirect {
303+
// @no-redirect has no value expression, simply setting it is enough
304+
request.NoRedirect = true
305+
return request, nil
306+
}
307+
308+
value, err := r.resolveExpression(statement.Value)
309+
if err != nil {
310+
r.errorf(statement, "failed to resolve value expression for key %s: %v", key, err)
311+
return spec.Request{}, err
312+
}
313+
314+
if !isKeyword {
315+
// Normal var
316+
if request.Vars == nil {
317+
request.Vars = make(map[string]string)
318+
}
319+
320+
request.Vars[key] = value
321+
322+
return request, nil
323+
}
324+
325+
// Otherwise, handle the specific keyword by setting the right field
326+
switch kind {
327+
case token.Name:
328+
request.Name = value
329+
case token.Timeout:
330+
duration, err := time.ParseDuration(value)
331+
if err != nil {
332+
r.errorf(statement.Value, "invalid timeout value: %v", err)
333+
return spec.Request{}, err
334+
}
335+
336+
request.Timeout = duration
337+
case token.ConnectionTimeout:
338+
duration, err := time.ParseDuration(value)
339+
if err != nil {
340+
r.errorf(statement.Value, "invalid connection-timeout value: %v", err)
341+
return spec.Request{}, err
342+
}
343+
344+
request.ConnectionTimeout = duration
345+
default:
346+
return spec.Request{}, fmt.Errorf("unhandled keyword: %s", kind)
347+
}
348+
349+
return request, nil
350+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# A single, minimal HTTP request
2+
3+
-- src.http --
4+
###
5+
GET https://example.com
6+
-- want.json --
7+
{
8+
"name": "minimal-request.txtar",
9+
"requests": [
10+
{
11+
"name": "#1",
12+
"method": "GET",
13+
"url": "https://example.com"
14+
}
15+
]
16+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# A single HTTP request with some variables and keywords
2+
3+
-- src.http --
4+
###
5+
# @name MyRequest
6+
# @timeout 30s
7+
# @connection-timeout 10s
8+
# @no-redirect
9+
# @normal-var = test
10+
GET https://example.com
11+
-- want.json --
12+
{
13+
"name": "request-vars.txtar",
14+
"requests": [
15+
{
16+
"vars": {
17+
"normal-var": "test"
18+
},
19+
"name": "MyRequest",
20+
"method": "GET",
21+
"url": "https://example.com",
22+
"timeout": 30000000000,
23+
"connectionTimeout": 10000000000,
24+
"noRedirect": true
25+
}
26+
]
27+
}

0 commit comments

Comments
 (0)