@@ -7,6 +7,8 @@ package resolver
77import (
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].
197258func (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+ }
0 commit comments