@@ -11,6 +11,7 @@ import (
1111 "os/exec"
1212 "regexp"
1313 "runtime"
14+ "slices"
1415 "strings"
1516 "time"
1617
@@ -47,6 +48,107 @@ func runCLICommand(command api.CLIStepCLICommand, variables map[string]string) (
4748 return result
4849}
4950
51+ func runJqQuery (step api.CLIStepJqQuery , variables map [string ]string ) api.JqQueryResult {
52+ filePath := InterpolateVariables (step .FilePath , variables )
53+ queryText := InterpolateVariables (step .Query , variables )
54+ result := api.JqQueryResult {
55+ FilePath : filePath ,
56+ Query : queryText ,
57+ }
58+
59+ input , err := readJqInput (filePath , step .InputMode )
60+ if err != nil {
61+ result .ExitCode = 1
62+ result .Stdout = err .Error ()
63+ return result
64+ }
65+
66+ query , err := gojq .Parse (queryText )
67+ if err != nil {
68+ result .ExitCode = 1
69+ result .Stdout = err .Error ()
70+ return result
71+ }
72+
73+ iter := query .Run (input )
74+ results := make ([]string , 0 )
75+ for {
76+ value , ok := iter .Next ()
77+ if ! ok {
78+ break
79+ }
80+ if err , ok := value .(error ); ok {
81+ result .ExitCode = 1
82+ result .Stdout = err .Error ()
83+ result .Results = results
84+ return result
85+ }
86+ stringified , err := stringifyJqResult (value )
87+ if err != nil {
88+ result .ExitCode = 1
89+ result .Stdout = err .Error ()
90+ result .Results = results
91+ return result
92+ }
93+ results = append (results , stringified )
94+ }
95+
96+ result .ExitCode = 0
97+ result .Results = results
98+ result .Stdout = strings .Join (results , "\n " )
99+ return result
100+ }
101+
102+ func readJqInput (filePath string , inputMode string ) (any , error ) {
103+ contents , err := os .ReadFile (filePath )
104+ if err != nil {
105+ return nil , err
106+ }
107+
108+ mode := strings .ToLower (strings .TrimSpace (inputMode ))
109+ if mode == "" {
110+ mode = "json"
111+ }
112+
113+ decoder := json .NewDecoder (bytes .NewReader (contents ))
114+ if mode == "jsonl" {
115+ values := make ([]any , 0 )
116+ for {
117+ var value any
118+ err := decoder .Decode (& value )
119+ if errors .Is (err , io .EOF ) {
120+ break
121+ }
122+ if err != nil {
123+ return nil , err
124+ }
125+ values = append (values , value )
126+ }
127+ return values , nil
128+ }
129+
130+ var value any
131+ if err := decoder .Decode (& value ); err != nil {
132+ return nil , err
133+ }
134+ if err := decoder .Decode (& struct {}{}); err != io .EOF {
135+ if err == nil {
136+ return nil , errors .New ("expected a single JSON value" )
137+ }
138+ return nil , err
139+ }
140+
141+ return value , nil
142+ }
143+
144+ func stringifyJqResult (value any ) (string , error ) {
145+ data , err := json .Marshal (value )
146+ if err != nil {
147+ return "" , err
148+ }
149+ return string (data ), nil
150+ }
151+
50152func runHTTPRequest (
51153 client * http.Client ,
52154 baseURL string ,
@@ -157,6 +259,10 @@ func CLIChecks(cliData api.CLIData, overrideBaseURL string, ch chan tea.Msg) (re
157259 Method : step .HTTPRequest .Request .Method ,
158260 ResponseVariables : step .HTTPRequest .ResponseVariables ,
159261 }
262+ } else if step .JqQuery != nil {
263+ filePath := InterpolateVariables (step .JqQuery .FilePath , variables )
264+ queryText := InterpolateVariables (step .JqQuery .Query , variables )
265+ ch <- messages.StartStepMsg {CMD : fmt .Sprintf ("jq '%s' %s" , queryText , filePath )}
160266 }
161267
162268 switch {
@@ -173,6 +279,12 @@ func CLIChecks(cliData api.CLIData, overrideBaseURL string, ch chan tea.Msg) (re
173279 sendHTTPRequestResults (ch , * step .HTTPRequest , result , i )
174280 handleSleep (step .HTTPRequest , ch )
175281
282+ case step .JqQuery != nil :
283+ result := runJqQuery (* step .JqQuery , variables )
284+ results [i ].JqQueryResult = & result
285+ sendJqQueryResults (ch , * step .JqQuery , result , i )
286+ handleSleep (step .JqQuery , ch )
287+
176288 default :
177289 cobra .CheckErr ("unable to run lesson: missing step" )
178290 }
@@ -220,6 +332,31 @@ func sendHTTPRequestResults(ch chan tea.Msg, req api.CLIStepHTTPRequest, result
220332 }
221333}
222334
335+ func sendJqQueryResults (ch chan tea.Msg , step api.CLIStepJqQuery , result api.JqQueryResult , index int ) {
336+ for _ , test := range step .Tests {
337+ ch <- messages.StartTestMsg {Text : prettyPrintJqTest (test )}
338+ }
339+
340+ allPassed := true
341+ for j , test := range step .Tests {
342+ passed := evaluateJqTest (test , result )
343+ allPassed = allPassed && passed
344+ ch <- messages.ResolveTestMsg {
345+ StepIndex : index ,
346+ TestIndex : j ,
347+ Passed : & passed ,
348+ }
349+ }
350+
351+ ch <- messages.ResolveStepMsg {
352+ Index : index ,
353+ Passed : & allPassed ,
354+ Result : & api.CLIStepResult {
355+ JqQueryResult : & result ,
356+ },
357+ }
358+ }
359+
223360func ApplySubmissionResults (cliData api.CLIData , failure * api.VerificationResultStructuredErrCLI , ch chan tea.Msg ) {
224361 for i , step := range cliData .Steps {
225362 pass := true
@@ -250,6 +387,15 @@ func ApplySubmissionResults(cliData api.CLIData, failure *api.VerificationResult
250387 }
251388 }
252389 }
390+ if step .JqQuery != nil {
391+ for j := range step .JqQuery .Tests {
392+ ch <- messages.ResolveTestMsg {
393+ StepIndex : i ,
394+ TestIndex : j ,
395+ Passed : & pass ,
396+ }
397+ }
398+ }
253399
254400 if ! pass {
255401 break
@@ -258,6 +404,69 @@ func ApplySubmissionResults(cliData api.CLIData, failure *api.VerificationResult
258404 }
259405}
260406
407+ func evaluateJqTest (test api.JqQueryTest , result api.JqQueryResult ) bool {
408+ switch {
409+ case test .ExitCode != nil :
410+ return result .ExitCode == * test .ExitCode
411+ case test .ExpectedBool != nil :
412+ if len (result .Results ) == 0 {
413+ return false
414+ }
415+ expected := fmt .Sprintf ("%t" , * test .ExpectedBool )
416+ return result .Results [0 ] == expected
417+ case test .ResultsContainAll != nil :
418+ return resultsContainAll (result .Results , test .ResultsContainAll )
419+ case test .ResultsContainNone != nil :
420+ return resultsContainNone (result .Results , test .ResultsContainNone )
421+ default :
422+ return false
423+ }
424+ }
425+
426+ func resultsContainAll (results []string , expected []string ) bool {
427+ for _ , item := range expected {
428+ if ! slices .Contains (results , item ) {
429+ return false
430+ }
431+ }
432+ return true
433+ }
434+
435+ func resultsContainNone (results []string , forbidden []string ) bool {
436+ for _ , item := range forbidden {
437+ if slices .Contains (results , item ) {
438+ return false
439+ }
440+ }
441+ return true
442+ }
443+
444+ func prettyPrintJqTest (test api.JqQueryTest ) string {
445+ if test .ExitCode != nil {
446+ return fmt .Sprintf ("Expect exit code %d" , * test .ExitCode )
447+ }
448+ if test .ExpectedBool != nil {
449+ return fmt .Sprintf ("Expect first result to be %t" , * test .ExpectedBool )
450+ }
451+ if test .ResultsContainAll != nil {
452+ var str strings.Builder
453+ str .WriteString ("Expect results to contain all of:" )
454+ for _ , contains := range test .ResultsContainAll {
455+ fmt .Fprintf (& str , "\n - '%s'" , contains )
456+ }
457+ return str .String ()
458+ }
459+ if test .ResultsContainNone != nil {
460+ var str strings.Builder
461+ str .WriteString ("Expect results to contain none of:" )
462+ for _ , containsNone := range test .ResultsContainNone {
463+ fmt .Fprintf (& str , "\n - '%s'" , containsNone )
464+ }
465+ return str .String ()
466+ }
467+ return ""
468+ }
469+
261470func prettyPrintCLICommand (test api.CLICommandTest , variables map [string ]string ) string {
262471 if test .ExitCode != nil {
263472 return fmt .Sprintf ("Expect exit code %d" , * test .ExitCode )
@@ -351,7 +560,7 @@ func truncateAndStringifyBody(body []byte) string {
351560
352561func parseVariables (body []byte , vardefs []api.HTTPRequestResponseVariable , variables map [string ]string ) error {
353562 for _ , vardef := range vardefs {
354- val , err := valFromJQPath (vardef .Path , string (body ))
563+ val , err := valFromJqPath (vardef .Path , string (body ))
355564 if err != nil {
356565 return err
357566 }
@@ -360,8 +569,8 @@ func parseVariables(body []byte, vardefs []api.HTTPRequestResponseVariable, vari
360569 return nil
361570}
362571
363- func valFromJQPath (path string , jsn string ) (any , error ) {
364- vals , err := valsFromJQPath (path , jsn )
572+ func valFromJqPath (path string , jsn string ) (any , error ) {
573+ vals , err := valsFromJqPath (path , jsn )
365574 if err != nil {
366575 return nil , err
367576 }
@@ -375,7 +584,7 @@ func valFromJQPath(path string, jsn string) (any, error) {
375584 return val , nil
376585}
377586
378- func valsFromJQPath (path string , jsn string ) ([]any , error ) {
587+ func valsFromJqPath (path string , jsn string ) ([]any , error ) {
379588 var parseable any
380589 err := json .Unmarshal ([]byte (jsn ), & parseable )
381590 if err != nil {
0 commit comments