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