Skip to main content

roas_arazzo_executor/
select.rs

1//! Turning the places a value can come from into a value.
2//!
3//! A v1.1 `value` is either a literal (which may hold runtime
4//! expressions) or a [`Selector`] naming structured data and an
5//! expression that picks from it. Both end up here, as does the
6//! `target` of a payload replacement.
7//!
8//! JSON Pointer and JSONPath are supported; XPath is not, and says so
9//! rather than quietly selecting nothing.
10
11use crate::expression::{self, ExpressionError, Scope};
12use roas_arazzo::v1_1::{ExpressionKind, Selector, SelectorKind, SelectorType, ValueOrSelector};
13use serde_json::Value;
14use serde_json_path::JsonPath;
15
16/// Why a value could not be produced.
17#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
18pub enum SelectError {
19    /// A runtime expression in the value could not be evaluated.
20    #[error(transparent)]
21    Expression(#[from] ExpressionError),
22    /// The selector expression itself is malformed.
23    #[error("`{selector}` is not a valid {kind} expression: {message}")]
24    Malformed {
25        /// The expression as written.
26        selector: String,
27        /// The language it was read as.
28        kind: &'static str,
29        /// What the parser said.
30        message: String,
31    },
32    /// The selector is valid but picked nothing.
33    #[error("`{selector}` selects nothing from `{context}`")]
34    Empty {
35        /// The expression as written.
36        selector: String,
37        /// The runtime expression naming what it applied to.
38        context: String,
39    },
40    /// XPath: no engine here, and pretending otherwise would be worse.
41    #[error("{0} expressions are not supported by this executor")]
42    Unsupported(&'static str),
43}
44
45/// The value a `value | selector` position holds.
46pub(crate) fn value_of(value: &ValueOrSelector, scope: &Scope<'_>) -> Result<Value, SelectError> {
47    match value {
48        ValueOrSelector::Literal(literal) => resolve(literal, scope),
49        ValueOrSelector::Selector(selector) => select(selector, scope),
50    }
51}
52
53/// A literal, with every runtime expression inside it replaced.
54///
55/// A string that *is* an expression becomes whatever the expression
56/// produced, keeping its type — `$statusCode` is a number, not `"200"`.
57/// A string that merely *contains* one is interpolated. Objects and
58/// arrays are walked, so a request payload written with expressions
59/// inside it comes out filled in.
60pub(crate) fn resolve(value: &Value, scope: &Scope<'_>) -> Result<Value, SelectError> {
61    Ok(match value {
62        Value::String(text) if expression::is_expression(text) => {
63            expression::evaluate(text, scope)?
64        }
65        Value::String(text) if text.contains("{$") => {
66            Value::String(expression::interpolate(text, scope)?)
67        }
68        Value::Array(items) => Value::Array(
69            items
70                .iter()
71                .map(|item| resolve(item, scope))
72                .collect::<Result<_, _>>()?,
73        ),
74        Value::Object(members) => Value::Object(
75            members
76                .iter()
77                .map(|(name, member)| Ok((name.clone(), resolve(member, scope)?)))
78                .collect::<Result<_, SelectError>>()?,
79        ),
80        other => other.clone(),
81    })
82}
83
84/// What a selector picks out of the data its context names.
85pub(crate) fn select(selector: &Selector, scope: &Scope<'_>) -> Result<Value, SelectError> {
86    let context = expression::evaluate(&selector.context, scope)?;
87    let picked = apply(kind_of(&selector.type_)?, &selector.selector, &context)?;
88    picked.ok_or_else(|| SelectError::Empty {
89        selector: selector.selector.clone(),
90        context: selector.context.clone(),
91    })
92}
93
94/// The language a selector or replacement target is written in.
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub(crate) enum Language {
97    Pointer,
98    Path,
99}
100
101/// Read a `SelectorType` as a language this crate can apply.
102pub(crate) fn kind_of(type_: &SelectorType) -> Result<Language, SelectError> {
103    match type_ {
104        SelectorType::Simple(SelectorKind::Jsonpointer) => Ok(Language::Pointer),
105        SelectorType::Simple(SelectorKind::Jsonpath) => Ok(Language::Path),
106        SelectorType::Simple(SelectorKind::Xpath) => Err(SelectError::Unsupported("XPath")),
107        SelectorType::Expression(expression) => match expression.type_ {
108            ExpressionKind::Jsonpointer => Ok(Language::Pointer),
109            ExpressionKind::Jsonpath => Ok(Language::Path),
110            ExpressionKind::Xpath => Err(SelectError::Unsupported("XPath")),
111        },
112    }
113}
114
115/// Apply `selector` to `data`. `None` when it picked nothing.
116pub(crate) fn apply(
117    language: Language,
118    selector: &str,
119    data: &Value,
120) -> Result<Option<Value>, SelectError> {
121    match language {
122        Language::Pointer => {
123            // A pointer may be written with the `#` that would precede
124            // it in a URI fragment.
125            let pointer = selector.strip_prefix('#').unwrap_or(selector);
126            Ok(data.pointer(pointer).cloned())
127        }
128        Language::Path => {
129            let path = JsonPath::parse(selector).map_err(|error| SelectError::Malformed {
130                selector: selector.to_owned(),
131                kind: "JSONPath",
132                message: error.to_string(),
133            })?;
134            let nodes = path.query(data);
135            Ok(match nodes.len() {
136                0 => None,
137                // One node is the value; several are the list of them,
138                // which is what a JSONPath that matches many means.
139                1 => nodes.first().cloned(),
140                _ => Some(Value::Array(
141                    nodes.iter().map(|&node| node.clone()).collect(),
142                )),
143            })
144        }
145    }
146}
147
148/// Put `value` where `target` points inside `data`.
149///
150/// A payload replacement writes into the body the step is about to
151/// send, so the target has to be somewhere that body has — or somewhere
152/// its parent object can take a new member.
153pub(crate) fn place(
154    language: Language,
155    target: &str,
156    data: &mut Value,
157    value: Value,
158) -> Result<(), String> {
159    let pointer = match language {
160        Language::Pointer => target.strip_prefix('#').unwrap_or(target).to_owned(),
161        Language::Path => {
162            let path = JsonPath::parse(target)
163                .map_err(|error| format!("`{target}` is not a valid JSONPath: {error}"))?;
164            path.query_located(data)
165                .locations()
166                .next()
167                .map(|location| location.to_json_pointer())
168                .ok_or_else(|| format!("`{target}` matches nothing in the payload"))?
169        }
170    };
171    if let Some(slot) = data.pointer_mut(&pointer) {
172        *slot = value;
173        return Ok(());
174    }
175    // A pointer may name a member that is not there yet, which is how a
176    // replacement adds one.
177    let (parent, member) = pointer
178        .rsplit_once('/')
179        .ok_or_else(|| format!("`{target}` does not point anywhere in the payload"))?;
180    match data.pointer_mut(parent) {
181        Some(Value::Object(members)) => {
182            members.insert(member.replace("~1", "/").replace("~0", "~"), value);
183            Ok(())
184        }
185        _ => Err(format!("`{target}` points into nothing the payload has")),
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use crate::expression::tests::{Fixture, exchange};
193    use serde_json::json;
194
195    fn selector(context: &str, selector: &str, kind: SelectorKind) -> Selector {
196        Selector {
197            context: context.to_owned(),
198            selector: selector.to_owned(),
199            type_: SelectorType::Simple(kind),
200            extensions: None,
201        }
202    }
203
204    #[test]
205    fn a_literal_keeps_the_type_the_expression_produced() {
206        let fixture = Fixture {
207            here: Some(exchange()),
208            ..Fixture::default()
209        };
210        let scope = fixture.scope();
211        assert_eq!(
212            value_of(&ValueOrSelector::literal("$statusCode"), &scope),
213            Ok(json!(200)),
214            "a whole expression keeps its own type"
215        );
216        assert_eq!(
217            value_of(&ValueOrSelector::literal("id-{$inputs.petId}"), &scope),
218            Ok(json!("id-7")),
219            "an expression inside text makes text"
220        );
221        assert_eq!(
222            value_of(&ValueOrSelector::literal("plain"), &scope),
223            Ok(json!("plain"))
224        );
225        assert_eq!(
226            value_of(&ValueOrSelector::literal(42), &scope),
227            Ok(json!(42))
228        );
229    }
230
231    #[test]
232    fn a_payload_is_filled_in_wherever_an_expression_sits() {
233        let fixture = Fixture {
234            here: Some(exchange()),
235            ..Fixture::default()
236        };
237        let payload = json!({
238            "pet": { "id": "$response.body#/id" },
239            "tags": ["$response.body#/tags/0", "plain"],
240            "count": 1,
241        });
242        assert_eq!(
243            resolve(&payload, &fixture.scope()),
244            Ok(json!({
245                "pet": { "id": 7 },
246                "tags": ["cat", "plain"],
247                "count": 1,
248            }))
249        );
250    }
251
252    #[test]
253    fn a_pointer_selector_picks_from_its_context() {
254        let fixture = Fixture {
255            here: Some(exchange()),
256            ..Fixture::default()
257        };
258        let scope = fixture.scope();
259        assert_eq!(
260            select(
261                &selector("$response.body", "/tags/0", SelectorKind::Jsonpointer),
262                &scope
263            ),
264            Ok(json!("cat"))
265        );
266        // Written the way a URI fragment spells it.
267        assert_eq!(
268            select(
269                &selector("$response.body", "#/id", SelectorKind::Jsonpointer),
270                &scope
271            ),
272            Ok(json!(7))
273        );
274        assert!(matches!(
275            select(
276                &selector("$response.body", "/nope", SelectorKind::Jsonpointer),
277                &scope
278            ),
279            Err(SelectError::Empty { .. })
280        ));
281    }
282
283    #[test]
284    fn a_path_selector_picks_one_node_or_the_list_of_them() {
285        let fixture = Fixture {
286            here: Some(exchange()),
287            ..Fixture::default()
288        };
289        let scope = fixture.scope();
290        assert_eq!(
291            select(
292                &selector("$response.body", "$.id", SelectorKind::Jsonpath),
293                &scope
294            ),
295            Ok(json!(7)),
296            "one node is the value itself"
297        );
298        assert_eq!(
299            select(
300                &selector("$response.body", "$.tags[*]", SelectorKind::Jsonpath),
301                &scope
302            ),
303            Ok(json!("cat")),
304            "a single match stays a single value"
305        );
306        assert!(matches!(
307            select(
308                &selector("$response.body", "$.nope", SelectorKind::Jsonpath),
309                &scope
310            ),
311            Err(SelectError::Empty { .. })
312        ));
313        assert!(matches!(
314            select(
315                &selector("$response.body", "$[", SelectorKind::Jsonpath),
316                &scope
317            ),
318            Err(SelectError::Malformed { .. })
319        ));
320    }
321
322    #[test]
323    fn several_nodes_come_back_as_the_list_of_them() {
324        let data = json!({ "tags": ["cat", "small"] });
325        assert_eq!(
326            apply(Language::Path, "$.tags[*]", &data),
327            Ok(Some(json!(["cat", "small"])))
328        );
329    }
330
331    #[test]
332    fn xpath_says_it_is_not_supported() {
333        assert_eq!(
334            kind_of(&SelectorType::Simple(SelectorKind::Xpath)),
335            Err(SelectError::Unsupported("XPath"))
336        );
337        let fixture = Fixture::default();
338        assert_eq!(
339            select(
340                &selector("$inputs", "/x", SelectorKind::Xpath),
341                &fixture.scope()
342            ),
343            Err(SelectError::Unsupported("XPath"))
344        );
345    }
346
347    #[test]
348    fn an_expression_type_names_the_same_languages() {
349        use roas_arazzo::v1_1::ExpressionType;
350        let typed = |kind| {
351            SelectorType::Expression(ExpressionType {
352                type_: kind,
353                version: String::new(),
354                extensions: None,
355            })
356        };
357        assert_eq!(
358            kind_of(&typed(ExpressionKind::Jsonpointer)),
359            Ok(Language::Pointer)
360        );
361        assert_eq!(
362            kind_of(&typed(ExpressionKind::Jsonpath)),
363            Ok(Language::Path)
364        );
365        assert_eq!(
366            kind_of(&typed(ExpressionKind::Xpath)),
367            Err(SelectError::Unsupported("XPath"))
368        );
369    }
370
371    #[test]
372    fn a_selector_whose_context_is_not_there_says_which_part_failed() {
373        let fixture = Fixture::default();
374        assert!(matches!(
375            select(
376                &selector("$response.body", "/id", SelectorKind::Jsonpointer),
377                &fixture.scope()
378            ),
379            Err(SelectError::Expression(_))
380        ));
381    }
382}