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//! `eq_condition` stands in for `vantage-table`'s `Operation::eq`: the
5//! `Expressive<ciborium::Value>` impls for primitive types would have to live
6//! in a foreign crate, so the helper builds the same expression shape
7//! directly.
8
9use ciborium::Value as CborValue;
10use vantage_expressions::Expression;
11use vantage_expressions::traits::expressive::ExpressiveEnum;
12
13/// Build an `eq` condition `field = value` for `Table<RestApi, _>`.
14///
15/// `value` is anything that converts into `CborValue`, which includes the
16/// scalars: `eq_condition("userId", 1i64)`.
17pub fn eq_condition(field: &str, value: impl Into<CborValue>) -> Expression<CborValue> {
18    Expression::new(
19        "{} = {}",
20        vec![
21            ExpressiveEnum::Nested(Expression::new(field.to_string(), vec![])),
22            ExpressiveEnum::Nested(Expression::new(
23                "{}",
24                vec![ExpressiveEnum::Scalar(value.into())],
25            )),
26        ],
27    )
28}
29
30/// Try to peel an `eq`-shaped condition into `(field_name, value_string)`
31/// suitable for a URL query parameter. Returns `None` for anything
32/// that doesn't match — non-eq operators, compound values, nested
33/// column-on-column comparisons, deferred values, etc.
34pub(crate) fn condition_to_query_param(cond: &Expression<CborValue>) -> Option<(String, String)> {
35    if cond.template != "{} = {}" || cond.parameters.len() != 2 {
36        return None;
37    }
38
39    let field = match &cond.parameters[0] {
40        ExpressiveEnum::Nested(e) if e.parameters.is_empty() => e.template.clone(),
41        _ => return None,
42    };
43
44    let value = match &cond.parameters[1] {
45        ExpressiveEnum::Nested(e) if e.template == "{}" && e.parameters.len() == 1 => {
46            match &e.parameters[0] {
47                ExpressiveEnum::Scalar(v) => cbor_to_query_string(v)?,
48                _ => return None,
49            }
50        }
51        ExpressiveEnum::Scalar(v) => cbor_to_query_string(v)?,
52        _ => return None,
53    };
54
55    Some((field, value))
56}
57
58/// Render a scalar CBOR value as the string form expected in a URL
59/// query parameter. Compound values (arrays, maps) have no single-key
60/// representation, so they fall through as `None` and the condition
61/// is dropped from the query string.
62pub(crate) fn cbor_to_query_string(v: &CborValue) -> Option<String> {
63    match v {
64        CborValue::Bool(b) => Some(b.to_string()),
65        CborValue::Integer(i) => Some(i128::from(*i).to_string()),
66        CborValue::Float(f) => Some(f.to_string()),
67        CborValue::Text(s) => Some(s.clone()),
68        CborValue::Null | CborValue::Array(_) | CborValue::Map(_) | CborValue::Bytes(_) => None,
69        _ => None,
70    }
71}
72
73/// The `{placeholder}` names in an endpoint template, in order:
74/// `teams/{team_id}/users` yields `team_id`. Scanning stops at a `{`
75/// with no closing `}`.
76pub(crate) fn placeholders(template: &str) -> impl Iterator<Item = &str> {
77    let mut rest = template;
78    std::iter::from_fn(move || {
79        let open = rest.find('{')?;
80        let after = &rest[open + 1..];
81        let close = after.find('}')?;
82        rest = &after[close + 1..];
83        Some(&after[..close])
84    })
85}