1use 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#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
18pub enum SelectError {
19 #[error(transparent)]
21 Expression(#[from] ExpressionError),
22 #[error("`{selector}` is not a valid {kind} expression: {message}")]
24 Malformed {
25 selector: String,
27 kind: &'static str,
29 message: String,
31 },
32 #[error("`{selector}` selects nothing from `{context}`")]
34 Empty {
35 selector: String,
37 context: String,
39 },
40 #[error("{0} expressions are not supported by this executor")]
42 Unsupported(&'static str),
43}
44
45pub(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
53pub(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
84pub(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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub(crate) enum Language {
97 Pointer,
98 Path,
99}
100
101pub(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
115pub(crate) fn apply(
117 language: Language,
118 selector: &str,
119 data: &Value,
120) -> Result<Option<Value>, SelectError> {
121 match language {
122 Language::Pointer => {
123 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 1 => nodes.first().cloned(),
140 _ => Some(Value::Array(
141 nodes.iter().map(|&node| node.clone()).collect(),
142 )),
143 })
144 }
145 }
146}
147
148pub(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 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 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}