Skip to main content

hitbox_http/predicates/body/
jq.rs

1//! JQ expression support for body predicates.
2//!
3//! Provides [`JqExpression`] for compiling and applying jq filters to JSON bodies,
4//! and [`JqOperation`] for matching against jq query results.
5
6use std::fmt::Debug;
7
8use hitbox::predicate::PredicateResult;
9use hyper::body::Body as HttpBody;
10use jaq_core::{
11    Ctx, Filter, Native, RcIter,
12    load::{Arena, File, Loader},
13};
14use jaq_json::{self, Val};
15use serde_json::Value;
16
17use crate::BufferedBody;
18
19/// A compiled jq expression for querying JSON bodies.
20///
21/// Wraps a jaq filter that can be compiled once and reused for multiple requests.
22/// This avoids the overhead of parsing and compiling the jq expression on each request.
23///
24/// # Examples
25///
26/// ```
27/// use hitbox_http::predicates::body::JqExpression;
28///
29/// // Compile a jq expression
30/// let expr = JqExpression::compile(".user.id").unwrap();
31///
32/// // Apply to JSON data
33/// let json = serde_json::json!({"user": {"id": 42}});
34/// let result = expr.apply(json);
35/// assert_eq!(result, Some(serde_json::json!(42)));
36/// ```
37///
38/// # Errors
39///
40/// [`compile`](Self::compile) returns `Err` if the jq expression is invalid.
41#[derive(Clone)]
42pub struct JqExpression(Filter<Native<Val>>);
43
44impl JqExpression {
45    /// Compiles a jq expression into a reusable filter.
46    ///
47    /// # Arguments
48    ///
49    /// * `expression` — A jq filter expression (e.g., `.user.id`, `.items[] | .name`)
50    ///
51    /// # Errors
52    ///
53    /// Returns `Err` if the expression cannot be parsed or compiled.
54    pub fn compile(expression: &str) -> Result<Self, String> {
55        let program = File {
56            code: expression,
57            path: (),
58        };
59        let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs()));
60        let arena = Arena::default();
61        let modules = loader
62            .load(&arena, program)
63            .map_err(|e| format!("Failed to load jq program: {:?}", e))?;
64        let filter = jaq_core::Compiler::default()
65            .with_funs(jaq_std::funs().chain(jaq_json::funs()))
66            .compile(modules)
67            .map_err(|e| format!("Failed to compile jq program: {:?}", e))?;
68        Ok(Self(filter))
69    }
70
71    /// Applies the filter to a JSON value and returns the result.
72    ///
73    /// Returns `None` if the filter produces `null` or no output.
74    /// If the filter produces multiple values, they are returned as a JSON array.
75    pub fn apply(&self, input: Value) -> Option<Value> {
76        let inputs = RcIter::new(core::iter::empty());
77        let out = self.0.run((Ctx::new([], &inputs), Val::from(input)));
78        let results: Result<Vec<_>, _> = out.collect();
79        match results {
80            Ok(values) if values.eq(&vec![Val::Null]) => None,
81            Ok(values) if !values.is_empty() => {
82                let mut values: Vec<Value> = values.into_iter().map(|v| v.into()).collect();
83                if values.len() == 1 {
84                    values.pop()
85                } else {
86                    Some(Value::Array(values))
87                }
88            }
89            _ => None,
90        }
91    }
92}
93
94impl Debug for JqExpression {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        f.debug_struct("JqExpression").finish_non_exhaustive()
97    }
98}
99
100/// Operations for matching jq query results.
101///
102/// Used with [`JqExpression`] to check if a JSON body matches certain criteria.
103///
104/// # Variants
105///
106/// - [`Eq`](Self::Eq) — The jq result must equal the specified value
107/// - [`Exist`](Self::Exist) — The jq result must be non-null
108/// - [`In`](Self::In) — The jq result must be one of the specified values
109#[derive(Debug, Clone)]
110pub enum JqOperation {
111    /// Match if the jq result equals this value.
112    Eq(Value),
113    /// Match if the jq result is non-null (path exists).
114    Exist,
115    /// Match if the jq result is one of these values.
116    In(Vec<Value>),
117}
118
119impl JqOperation {
120    /// Checks if the jq operation matches the body.
121    ///
122    /// Collects the entire body, parses it as JSON, applies the jq filter,
123    /// and checks if the result satisfies this operation.
124    ///
125    /// Returns [`Cacheable`](PredicateResult::Cacheable) if the operation is satisfied,
126    /// [`NonCacheable`](PredicateResult::NonCacheable) otherwise.
127    ///
128    /// # Caveats
129    ///
130    /// - The entire body is buffered into memory for JSON parsing
131    /// - Returns `NonCacheable` if the body is not valid JSON
132    pub async fn check<B>(
133        &self,
134        filter: &JqExpression,
135        body: BufferedBody<B>,
136    ) -> PredicateResult<BufferedBody<B>>
137    where
138        B: HttpBody + Unpin,
139        B::Data: Send,
140    {
141        // Collect the full body to parse as JSON
142        let body_bytes = match body.collect().await {
143            Ok(bytes) => bytes,
144            Err(error_body) => return PredicateResult::NonCacheable(error_body),
145        };
146
147        // Parse body as JSON
148        let json_value: Value = match serde_json::from_slice(&body_bytes) {
149            Ok(v) => v,
150            Err(_) => {
151                // Failed to parse JSON - non-cacheable
152                return PredicateResult::NonCacheable(BufferedBody::Complete(Some(body_bytes)));
153            }
154        };
155
156        // Apply the jq filter
157        let found_value = filter.apply(json_value);
158
159        // Check if the operation matches
160        let matches = match self {
161            JqOperation::Eq(expected) => {
162                found_value.as_ref().map(|v| v == expected).unwrap_or(false)
163            }
164            JqOperation::Exist => found_value.is_some(),
165            JqOperation::In(values) => found_value
166                .as_ref()
167                .map(|v| values.contains(v))
168                .unwrap_or(false),
169        };
170
171        let result_body = BufferedBody::Complete(Some(body_bytes));
172        if matches {
173            PredicateResult::Cacheable(result_body)
174        } else {
175            PredicateResult::NonCacheable(result_body)
176        }
177    }
178}