Skip to main content

vantage_api_client/
operation.rs

1//! Condition helpers — build eq-conditions for `Table<RestApi, _>` and
2//! peel them apart at the `list_values` boundary into URL query params.
3//!
4//! Why not the standard `Operation::eq` from `vantage-table`? That trait
5//! is blanket-implemented for all `Expressive<T>`, but we can't provide
6//! the *value-side* `Expressive<serde_json::Value>` impls for primitive
7//! types — orphan rule (both `Expressive` and `serde_json::Value` are
8//! foreign to this crate). A future refactor wrapping the value type in
9//! a local newtype would unlock the full `Operation` surface; for now
10//! we provide an `eq_condition(field, value)` free function that builds
11//! the same expression shape directly.
12
13use serde_json::Value;
14use vantage_expressions::Expression;
15use vantage_expressions::traits::expressive::ExpressiveEnum;
16
17/// Build an `eq` condition `field = value` for use with `Table<RestApi,
18/// _>::add_condition`. Produces the same shape as
19/// `vantage_table::operation::Operation::eq` would for backends that
20/// support it.
21///
22/// ```ignore
23/// use vantage_api_client::eq_condition;
24/// use serde_json::json;
25///
26/// let mut comments = Table::<RestApi, EmptyEntity>::new("comments", api);
27/// comments.add_condition(eq_condition("postId", json!(1)));
28/// ```
29pub fn eq_condition(field: &str, value: Value) -> Expression<Value> {
30    Expression::new(
31        "{} = {}",
32        vec![
33            // Field side: bare identifier expression.
34            ExpressiveEnum::Nested(Expression::new(field.to_string(), vec![])),
35            // Value side: scalar wrapped in a `{}` expression so it
36            // matches the layout `Operation::eq` produces.
37            ExpressiveEnum::Nested(Expression::new("{}", vec![ExpressiveEnum::Scalar(value)])),
38        ],
39    )
40}
41
42/// Try to peel an `eq`-shaped condition into `(field_name, value_string)`
43/// suitable for a URL query parameter.
44///
45/// Recognised shape — same one `eq_condition` (and
46/// `vantage_table::Operation::eq` for compatible value types) produces:
47///
48/// ```text
49/// Expression {
50///     template: "{} = {}",
51///     parameters: [
52///         Nested(Expression { template: <field_name>, parameters: [] }),
53///         Nested(Expression { template: "{}", parameters: [Scalar(value)] }),
54///     ],
55/// }
56/// ```
57///
58/// Returns `None` for anything that doesn't match — non-eq operators,
59/// nested column-on-column comparisons, deferred values, etc. The caller
60/// decides what to do (skip silently is the v1 stance).
61pub(crate) fn condition_to_query_param(cond: &Expression<Value>) -> Option<(String, String)> {
62    if cond.template != "{} = {}" || cond.parameters.len() != 2 {
63        return None;
64    }
65
66    let field = match &cond.parameters[0] {
67        ExpressiveEnum::Nested(e) if e.parameters.is_empty() => e.template.clone(),
68        _ => return None,
69    };
70
71    let value = match &cond.parameters[1] {
72        ExpressiveEnum::Nested(e) if e.template == "{}" && e.parameters.len() == 1 => {
73            match &e.parameters[0] {
74                ExpressiveEnum::Scalar(v) => json_to_query_string(v)?,
75                _ => return None,
76            }
77        }
78        ExpressiveEnum::Scalar(v) => json_to_query_string(v)?,
79        _ => return None,
80    };
81
82    Some((field, value))
83}
84
85fn json_to_query_string(v: &Value) -> Option<String> {
86    match v {
87        Value::Bool(b) => Some(b.to_string()),
88        Value::Number(n) => Some(n.to_string()),
89        Value::String(s) => Some(s.clone()),
90        Value::Null | Value::Array(_) | Value::Object(_) => None,
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use serde_json::json;
98
99    #[test]
100    fn eq_condition_round_trips_int() {
101        let cond = eq_condition("userId", json!(1));
102        let pair = condition_to_query_param(&cond).expect("eq parses");
103        assert_eq!(pair, ("userId".into(), "1".into()));
104    }
105
106    #[test]
107    fn eq_condition_round_trips_string() {
108        let cond = eq_condition("name", json!("Alice"));
109        let pair = condition_to_query_param(&cond).expect("eq parses");
110        assert_eq!(pair, ("name".into(), "Alice".into()));
111    }
112
113    #[test]
114    fn eq_condition_round_trips_bool() {
115        let cond = eq_condition("completed", json!(true));
116        let pair = condition_to_query_param(&cond).expect("eq parses");
117        assert_eq!(pair, ("completed".into(), "true".into()));
118    }
119
120    #[test]
121    fn raw_expression_with_unknown_template_returns_none() {
122        let cond = Expression::<Value>::new("CUSTOM SQL", vec![]);
123        assert!(condition_to_query_param(&cond).is_none());
124    }
125
126    #[test]
127    fn array_value_returns_none() {
128        let cond = eq_condition("tags", json!(["a", "b"]));
129        // Arrays don't map to a single query-param string.
130        assert!(condition_to_query_param(&cond).is_none());
131    }
132}