1use crate::{Body, Exchange, Value};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum PathSegment {
22 Key(String),
24 Index(usize),
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum ExchangeLookupPath {
33 Body(Vec<PathSegment>),
36 Header(String),
38 Property(String),
40 Unscoped(String),
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
48#[non_exhaustive]
49pub enum LookupPathError {
50 #[error("empty lookup path")]
52 Empty,
53 #[error("empty path segment in {input:?}")]
55 EmptySegment { input: String },
56 #[error("trailing dot in {input:?}")]
58 TrailingDot { input: String },
59 #[error("scope prefix {scope:?} requires a non-empty key")]
61 EmptyScopedKey { scope: String, input: String },
62}
63
64impl ExchangeLookupPath {
65 pub fn parse(s: &str) -> Result<Self, LookupPathError> {
66 if s.is_empty() {
67 return Err(LookupPathError::Empty);
68 }
69
70 let (head, rest_opt) = match s.split_once('.') {
74 Some((h, r)) => (h, Some(r)),
75 None => (s, None),
76 };
77
78 match head {
79 "body" => {
80 let Some(rest) = rest_opt else {
81 return Ok(ExchangeLookupPath::Body(Vec::new()));
83 };
84 if rest.is_empty() {
85 return Err(LookupPathError::TrailingDot { input: s.into() });
86 }
87 let segments = parse_body_segments(rest, s)?;
88 Ok(ExchangeLookupPath::Body(segments))
89 }
90 "header" => {
91 let Some(rest) = rest_opt else {
92 return Err(LookupPathError::EmptyScopedKey {
94 scope: "header".into(),
95 input: s.into(),
96 });
97 };
98 if rest.is_empty() {
99 return Err(LookupPathError::EmptyScopedKey {
100 scope: "header".into(),
101 input: s.into(),
102 });
103 }
104 Ok(ExchangeLookupPath::Header(rest.into()))
105 }
106 "property" | "exchangeProperty" => {
107 let scope = head;
108 let Some(rest) = rest_opt else {
109 return Err(LookupPathError::EmptyScopedKey {
110 scope: scope.into(),
111 input: s.into(),
112 });
113 };
114 if rest.is_empty() {
115 return Err(LookupPathError::EmptyScopedKey {
116 scope: scope.into(),
117 input: s.into(),
118 });
119 }
120 Ok(ExchangeLookupPath::Property(rest.into()))
121 }
122 _ => {
123 Ok(ExchangeLookupPath::Unscoped(s.into()))
126 }
127 }
128 }
129
130 pub fn lookup(&self, exchange: &Exchange) -> Option<Value> {
133 match self {
134 ExchangeLookupPath::Body(segments) => lookup_body(exchange, segments),
135 ExchangeLookupPath::Header(key) => exchange.input.header(key).cloned(),
136 ExchangeLookupPath::Property(key) => exchange.property(key).cloned(),
137 ExchangeLookupPath::Unscoped(token) => {
138 if let Some(value) = body_json_object(exchange).and_then(|obj| obj.get(token)) {
140 return Some(value.clone());
141 }
142 if let Some(value) = exchange.input.header(token) {
144 return Some(value.clone());
145 }
146 exchange.property(token).cloned()
148 }
149 }
150 }
151}
152
153fn body_json_object(exchange: &Exchange) -> Option<&serde_json::Map<String, Value>> {
154 match &exchange.input.body {
155 Body::Json(value) => value.as_object(),
156 _ => None,
157 }
158}
159
160fn lookup_body(exchange: &Exchange, segments: &[PathSegment]) -> Option<Value> {
161 let Body::Json(value) = &exchange.input.body else {
162 return None;
163 };
164 if segments.is_empty() {
165 return Some(value.clone());
167 }
168 let mut current = value;
169 for seg in segments {
170 current = match seg {
171 PathSegment::Key(k) => current.as_object().and_then(|obj| obj.get(k))?,
172 PathSegment::Index(i) => current.as_array().and_then(|arr| arr.get(*i))?,
173 };
174 }
175 Some(current.clone())
176}
177
178fn parse_body_segments(path: &str, full_input: &str) -> Result<Vec<PathSegment>, LookupPathError> {
183 let mut segments = Vec::new();
184 for seg in path.split('.') {
185 if seg.is_empty() {
186 return Err(LookupPathError::EmptySegment {
187 input: full_input.into(),
188 });
189 }
190 let parsed = seg
191 .parse::<usize>()
192 .ok()
193 .filter(|_| seg == "0" || !seg.starts_with('0'));
194 match parsed {
195 Some(i) => segments.push(PathSegment::Index(i)),
196 None => segments.push(PathSegment::Key(seg.into())),
197 }
198 }
199 Ok(segments)
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn parse_unscoped_token() {
208 assert_eq!(
209 ExchangeLookupPath::parse("my-param"),
210 Ok(ExchangeLookupPath::Unscoped("my-param".into()))
211 );
212 assert_eq!(
213 ExchangeLookupPath::parse("foo.bar"),
214 Ok(ExchangeLookupPath::Unscoped("foo.bar".into()))
215 );
216 }
217
218 #[test]
219 fn parse_body_scope_walks_segments() {
220 assert_eq!(
221 ExchangeLookupPath::parse("body.user.address.city"),
222 Ok(ExchangeLookupPath::Body(vec![
223 PathSegment::Key("user".into()),
224 PathSegment::Key("address".into()),
225 PathSegment::Key("city".into()),
226 ]))
227 );
228 }
229
230 #[test]
231 fn parse_body_scope_with_numeric_index() {
232 assert_eq!(
233 ExchangeLookupPath::parse("body.items.0"),
234 Ok(ExchangeLookupPath::Body(vec![
235 PathSegment::Key("items".into()),
236 PathSegment::Index(0),
237 ]))
238 );
239 }
240
241 #[test]
242 fn parse_body_scope_leading_zero_is_key_not_index() {
243 assert_eq!(
245 ExchangeLookupPath::parse("body.01"),
246 Ok(ExchangeLookupPath::Body(vec![PathSegment::Key(
247 "01".into()
248 )]))
249 );
250 }
251
252 #[test]
253 fn parse_body_scope_bare_is_empty_segments() {
254 assert_eq!(
256 ExchangeLookupPath::parse("body"),
257 Ok(ExchangeLookupPath::Body(vec![]))
258 );
259 }
260
261 #[test]
262 fn parse_header_scope_flat_key() {
263 assert_eq!(
264 ExchangeLookupPath::parse("header.some.name"),
265 Ok(ExchangeLookupPath::Header("some.name".into()))
266 );
267 }
268
269 #[test]
270 fn parse_property_scope_flat_key() {
271 assert_eq!(
272 ExchangeLookupPath::parse("property.some.name"),
273 Ok(ExchangeLookupPath::Property("some.name".into()))
274 );
275 }
276
277 #[test]
278 fn parse_exchange_property_alias() {
279 assert_eq!(
280 ExchangeLookupPath::parse("exchangeProperty.myKey"),
281 Ok(ExchangeLookupPath::Property("myKey".into()))
282 );
283 }
284
285 #[test]
286 fn parse_rejects_empty_input() {
287 assert_eq!(ExchangeLookupPath::parse(""), Err(LookupPathError::Empty));
288 }
289
290 #[test]
291 fn parse_rejects_trailing_dot() {
292 let err = ExchangeLookupPath::parse("body.").unwrap_err();
293 assert!(
294 matches!(err, LookupPathError::TrailingDot { .. }),
295 "{err:?}"
296 );
297 }
298
299 #[test]
300 fn parse_rejects_empty_segment_in_body_path() {
301 let err = ExchangeLookupPath::parse("body..name").unwrap_err();
302 assert!(
303 matches!(err, LookupPathError::EmptySegment { .. }),
304 "{err:?}"
305 );
306 }
307
308 #[test]
309 fn parse_rejects_empty_scoped_key_for_header() {
310 let err = ExchangeLookupPath::parse("header.").unwrap_err();
313 assert!(
314 matches!(err, LookupPathError::EmptyScopedKey { .. }),
315 "{err:?}"
316 );
317 }
318
319 #[test]
320 fn lookup_walks_nested_body_json() {
321 use crate::{Body, Exchange, Message};
322 let msg = Message::new(Body::Json(serde_json::json!({
323 "user": { "address": { "city": "Berlin" } }
324 })));
325 let ex = Exchange::new(msg);
326
327 let path = ExchangeLookupPath::parse("body.user.address.city").unwrap();
328 assert_eq!(path.lookup(&ex), Some(serde_json::json!("Berlin")));
329 }
330
331 #[test]
332 fn lookup_walks_body_array_index() {
333 use crate::{Body, Exchange, Message};
334 let msg = Message::new(Body::Json(serde_json::json!({
335 "items": [10, 20, 30]
336 })));
337 let ex = Exchange::new(msg);
338
339 let path = ExchangeLookupPath::parse("body.items.1").unwrap();
340 assert_eq!(path.lookup(&ex), Some(serde_json::json!(20)));
341 }
342
343 #[test]
344 fn lookup_body_whole_returns_full_body_value() {
345 use crate::{Body, Exchange, Message};
346 let msg = Message::new(Body::Json(serde_json::json!({"a": 1})));
347 let ex = Exchange::new(msg);
348
349 let path = ExchangeLookupPath::parse("body").unwrap();
350 assert_eq!(path.lookup(&ex), Some(serde_json::json!({"a": 1})));
351 }
352
353 #[test]
354 fn lookup_body_returns_none_when_not_json() {
355 use crate::{Body, Exchange, Message};
356 let msg = Message::new(Body::Text("hello".into()));
357 let ex = Exchange::new(msg);
358
359 let path = ExchangeLookupPath::parse("body.user").unwrap();
360 assert_eq!(path.lookup(&ex), None);
361 }
362
363 #[test]
364 fn lookup_body_returns_none_when_path_misses() {
365 use crate::{Body, Exchange, Message};
366 let msg = Message::new(Body::Json(serde_json::json!({"a": 1})));
367 let ex = Exchange::new(msg);
368
369 let path = ExchangeLookupPath::parse("body.b.c").unwrap();
370 assert_eq!(path.lookup(&ex), None);
371 }
372
373 #[test]
374 fn lookup_header_flat_dotted_key() {
375 use crate::{Exchange, Message};
376 let mut msg = Message::default();
377 msg.set_header("some.name", serde_json::json!(42));
378 let ex = Exchange::new(msg);
379
380 let path = ExchangeLookupPath::parse("header.some.name").unwrap();
381 assert_eq!(path.lookup(&ex), Some(serde_json::json!(42)));
382 }
383
384 #[test]
385 fn lookup_property_flat_dotted_key() {
386 use crate::{Exchange, Message};
387 let mut ex = Exchange::new(Message::default());
388 ex.set_property("config.key", serde_json::json!("v"));
389
390 let path = ExchangeLookupPath::parse("property.config.key").unwrap();
391 assert_eq!(path.lookup(&ex), Some(serde_json::json!("v")));
392 }
393
394 #[test]
395 fn lookup_unscoped_fallback_body_then_header_then_property() {
396 use crate::{Body, Exchange, Message};
397 let mut msg = Message::new(Body::Json(serde_json::json!({"id": 1})));
399 msg.set_header("id", serde_json::json!(2));
400 let ex = Exchange::new(msg);
401 let path = ExchangeLookupPath::parse("id").unwrap();
402 assert_eq!(path.lookup(&ex), Some(serde_json::json!(1)));
403 }
404
405 #[test]
406 fn lookup_unscoped_falls_through_to_header() {
407 use crate::{Exchange, Message};
408 let mut msg = Message::default();
409 msg.set_header("token", serde_json::json!("abc"));
410 let ex = Exchange::new(msg);
411 let path = ExchangeLookupPath::parse("token").unwrap();
412 assert_eq!(path.lookup(&ex), Some(serde_json::json!("abc")));
413 }
414
415 #[test]
416 fn lookup_unscoped_falls_through_to_property() {
417 use crate::{Exchange, Message};
418 let mut ex = Exchange::new(Message::default());
419 ex.set_property("tenant", serde_json::json!("acme"));
420 let path = ExchangeLookupPath::parse("tenant").unwrap();
421 assert_eq!(path.lookup(&ex), Some(serde_json::json!("acme")));
422 }
423
424 #[test]
425 fn lookup_unscoped_returns_none_when_missing_everywhere() {
426 use crate::{Exchange, Message};
427 let ex = Exchange::new(Message::default());
428 let path = ExchangeLookupPath::parse("nope").unwrap();
429 assert_eq!(path.lookup(&ex), None);
430 }
431}