Skip to main content

vantage_api_client/rest/
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 the value-side
6//! `Expressive<ciborium::Value>` impls for primitive types live in
7//! foreign crates — we can't add them ourselves. So we ship a focused
8//! `eq_condition` helper that produces the same expression shape
9//! directly.
10
11use ciborium::Value as CborValue;
12use vantage_expressions::Expression;
13use vantage_expressions::traits::expressive::ExpressiveEnum;
14
15/// Build an `eq` condition `field = value` for `Table<RestApi, _>`.
16///
17/// `value` is accepted as anything that converts into `CborValue` —
18/// most scalars (`i64`, `f64`, `bool`, `&str`, `String`) implement
19/// this directly through ciborium's `From` impls, so the call site
20/// stays readable: `eq_condition("userId", 1i64)`.
21pub fn eq_condition(field: &str, value: impl Into<CborValue>) -> Expression<CborValue> {
22    Expression::new(
23        "{} = {}",
24        vec![
25            ExpressiveEnum::Nested(Expression::new(field.to_string(), vec![])),
26            ExpressiveEnum::Nested(Expression::new(
27                "{}",
28                vec![ExpressiveEnum::Scalar(value.into())],
29            )),
30        ],
31    )
32}
33
34/// Try to peel an `eq`-shaped condition into `(field_name, value_string)`
35/// suitable for a URL query parameter. Returns `None` for anything
36/// that doesn't match — non-eq operators, compound values, nested
37/// column-on-column comparisons, deferred values, etc.
38pub(crate) fn condition_to_query_param(cond: &Expression<CborValue>) -> Option<(String, String)> {
39    if cond.template != "{} = {}" || cond.parameters.len() != 2 {
40        return None;
41    }
42
43    let field = match &cond.parameters[0] {
44        ExpressiveEnum::Nested(e) if e.parameters.is_empty() => e.template.clone(),
45        _ => return None,
46    };
47
48    let value = match &cond.parameters[1] {
49        ExpressiveEnum::Nested(e) if e.template == "{}" && e.parameters.len() == 1 => {
50            match &e.parameters[0] {
51                ExpressiveEnum::Scalar(v) => cbor_to_query_string(v)?,
52                _ => return None,
53            }
54        }
55        ExpressiveEnum::Scalar(v) => cbor_to_query_string(v)?,
56        _ => return None,
57    };
58
59    Some((field, value))
60}
61
62/// Render a scalar CBOR value as the string form expected in a URL
63/// query parameter. Compound values (arrays, maps) have no single-key
64/// representation, so they fall through as `None` and the condition
65/// is dropped from the query string.
66pub(crate) fn cbor_to_query_string(v: &CborValue) -> Option<String> {
67    match v {
68        CborValue::Bool(b) => Some(b.to_string()),
69        CborValue::Integer(i) => Some(i128::from(*i).to_string()),
70        CborValue::Float(f) => Some(f.to_string()),
71        CborValue::Text(s) => Some(s.clone()),
72        CborValue::Null | CborValue::Array(_) | CborValue::Map(_) | CborValue::Bytes(_) => None,
73        _ => None,
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn eq_condition_round_trips_int() {
83        let cond = eq_condition("userId", 1i64);
84        let pair = condition_to_query_param(&cond).expect("eq parses");
85        assert_eq!(pair, ("userId".into(), "1".into()));
86    }
87
88    #[test]
89    fn eq_condition_round_trips_string() {
90        let cond = eq_condition("name", "Alice");
91        let pair = condition_to_query_param(&cond).expect("eq parses");
92        assert_eq!(pair, ("name".into(), "Alice".into()));
93    }
94
95    #[test]
96    fn eq_condition_round_trips_bool() {
97        let cond = eq_condition("completed", true);
98        let pair = condition_to_query_param(&cond).expect("eq parses");
99        assert_eq!(pair, ("completed".into(), "true".into()));
100    }
101
102    #[test]
103    fn raw_expression_with_unknown_template_returns_none() {
104        let cond = Expression::<CborValue>::new("CUSTOM SQL", vec![]);
105        assert!(condition_to_query_param(&cond).is_none());
106    }
107
108    #[test]
109    fn array_value_returns_none() {
110        let cond = eq_condition("tags", vec![CborValue::Text("a".into())]);
111        assert!(condition_to_query_param(&cond).is_none());
112    }
113}