use crate::{Body, Exchange, Value};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathSegment {
Key(String),
Index(usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExchangeLookupPath {
Body(Vec<PathSegment>),
Header(String),
Property(String),
Unscoped(String),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum LookupPathError {
#[error("empty lookup path")]
Empty,
#[error("empty path segment in {input:?}")]
EmptySegment { input: String },
#[error("trailing dot in {input:?}")]
TrailingDot { input: String },
#[error("scope prefix {scope:?} requires a non-empty key")]
EmptyScopedKey { scope: String, input: String },
}
impl ExchangeLookupPath {
pub fn parse(s: &str) -> Result<Self, LookupPathError> {
if s.is_empty() {
return Err(LookupPathError::Empty);
}
let (head, rest_opt) = match s.split_once('.') {
Some((h, r)) => (h, Some(r)),
None => (s, None),
};
match head {
"body" => {
let Some(rest) = rest_opt else {
return Ok(ExchangeLookupPath::Body(Vec::new()));
};
if rest.is_empty() {
return Err(LookupPathError::TrailingDot { input: s.into() });
}
let segments = parse_body_segments(rest, s)?;
Ok(ExchangeLookupPath::Body(segments))
}
"header" => {
let Some(rest) = rest_opt else {
return Err(LookupPathError::EmptyScopedKey {
scope: "header".into(),
input: s.into(),
});
};
if rest.is_empty() {
return Err(LookupPathError::EmptyScopedKey {
scope: "header".into(),
input: s.into(),
});
}
Ok(ExchangeLookupPath::Header(rest.into()))
}
"property" | "exchangeProperty" => {
let scope = head;
let Some(rest) = rest_opt else {
return Err(LookupPathError::EmptyScopedKey {
scope: scope.into(),
input: s.into(),
});
};
if rest.is_empty() {
return Err(LookupPathError::EmptyScopedKey {
scope: scope.into(),
input: s.into(),
});
}
Ok(ExchangeLookupPath::Property(rest.into()))
}
_ => {
Ok(ExchangeLookupPath::Unscoped(s.into()))
}
}
}
pub fn lookup(&self, exchange: &Exchange) -> Option<Value> {
match self {
ExchangeLookupPath::Body(segments) => lookup_body(exchange, segments),
ExchangeLookupPath::Header(key) => exchange.input.header(key).cloned(),
ExchangeLookupPath::Property(key) => exchange.property(key).cloned(),
ExchangeLookupPath::Unscoped(token) => {
if let Some(value) = body_json_object(exchange).and_then(|obj| obj.get(token)) {
return Some(value.clone());
}
if let Some(value) = exchange.input.header(token) {
return Some(value.clone());
}
exchange.property(token).cloned()
}
}
}
}
fn body_json_object(exchange: &Exchange) -> Option<&serde_json::Map<String, Value>> {
match &exchange.input.body {
Body::Json(value) => value.as_object(),
_ => None,
}
}
fn lookup_body(exchange: &Exchange, segments: &[PathSegment]) -> Option<Value> {
let Body::Json(value) = &exchange.input.body else {
return None;
};
if segments.is_empty() {
return Some(value.clone());
}
let mut current = value;
for seg in segments {
current = match seg {
PathSegment::Key(k) => current.as_object().and_then(|obj| obj.get(k))?,
PathSegment::Index(i) => current.as_array().and_then(|arr| arr.get(*i))?,
};
}
Some(current.clone())
}
fn parse_body_segments(path: &str, full_input: &str) -> Result<Vec<PathSegment>, LookupPathError> {
let mut segments = Vec::new();
for seg in path.split('.') {
if seg.is_empty() {
return Err(LookupPathError::EmptySegment {
input: full_input.into(),
});
}
let parsed = seg
.parse::<usize>()
.ok()
.filter(|_| seg == "0" || !seg.starts_with('0'));
match parsed {
Some(i) => segments.push(PathSegment::Index(i)),
None => segments.push(PathSegment::Key(seg.into())),
}
}
Ok(segments)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_unscoped_token() {
assert_eq!(
ExchangeLookupPath::parse("my-param"),
Ok(ExchangeLookupPath::Unscoped("my-param".into()))
);
assert_eq!(
ExchangeLookupPath::parse("foo.bar"),
Ok(ExchangeLookupPath::Unscoped("foo.bar".into()))
);
}
#[test]
fn parse_body_scope_walks_segments() {
assert_eq!(
ExchangeLookupPath::parse("body.user.address.city"),
Ok(ExchangeLookupPath::Body(vec![
PathSegment::Key("user".into()),
PathSegment::Key("address".into()),
PathSegment::Key("city".into()),
]))
);
}
#[test]
fn parse_body_scope_with_numeric_index() {
assert_eq!(
ExchangeLookupPath::parse("body.items.0"),
Ok(ExchangeLookupPath::Body(vec![
PathSegment::Key("items".into()),
PathSegment::Index(0),
]))
);
}
#[test]
fn parse_body_scope_leading_zero_is_key_not_index() {
assert_eq!(
ExchangeLookupPath::parse("body.01"),
Ok(ExchangeLookupPath::Body(vec![PathSegment::Key(
"01".into()
)]))
);
}
#[test]
fn parse_body_scope_bare_is_empty_segments() {
assert_eq!(
ExchangeLookupPath::parse("body"),
Ok(ExchangeLookupPath::Body(vec![]))
);
}
#[test]
fn parse_header_scope_flat_key() {
assert_eq!(
ExchangeLookupPath::parse("header.some.name"),
Ok(ExchangeLookupPath::Header("some.name".into()))
);
}
#[test]
fn parse_property_scope_flat_key() {
assert_eq!(
ExchangeLookupPath::parse("property.some.name"),
Ok(ExchangeLookupPath::Property("some.name".into()))
);
}
#[test]
fn parse_exchange_property_alias() {
assert_eq!(
ExchangeLookupPath::parse("exchangeProperty.myKey"),
Ok(ExchangeLookupPath::Property("myKey".into()))
);
}
#[test]
fn parse_rejects_empty_input() {
assert_eq!(ExchangeLookupPath::parse(""), Err(LookupPathError::Empty));
}
#[test]
fn parse_rejects_trailing_dot() {
let err = ExchangeLookupPath::parse("body.").unwrap_err();
assert!(
matches!(err, LookupPathError::TrailingDot { .. }),
"{err:?}"
);
}
#[test]
fn parse_rejects_empty_segment_in_body_path() {
let err = ExchangeLookupPath::parse("body..name").unwrap_err();
assert!(
matches!(err, LookupPathError::EmptySegment { .. }),
"{err:?}"
);
}
#[test]
fn parse_rejects_empty_scoped_key_for_header() {
let err = ExchangeLookupPath::parse("header.").unwrap_err();
assert!(
matches!(err, LookupPathError::EmptyScopedKey { .. }),
"{err:?}"
);
}
#[test]
fn lookup_walks_nested_body_json() {
use crate::{Body, Exchange, Message};
let msg = Message::new(Body::Json(serde_json::json!({
"user": { "address": { "city": "Berlin" } }
})));
let ex = Exchange::new(msg);
let path = ExchangeLookupPath::parse("body.user.address.city").unwrap();
assert_eq!(path.lookup(&ex), Some(serde_json::json!("Berlin")));
}
#[test]
fn lookup_walks_body_array_index() {
use crate::{Body, Exchange, Message};
let msg = Message::new(Body::Json(serde_json::json!({
"items": [10, 20, 30]
})));
let ex = Exchange::new(msg);
let path = ExchangeLookupPath::parse("body.items.1").unwrap();
assert_eq!(path.lookup(&ex), Some(serde_json::json!(20)));
}
#[test]
fn lookup_body_whole_returns_full_body_value() {
use crate::{Body, Exchange, Message};
let msg = Message::new(Body::Json(serde_json::json!({"a": 1})));
let ex = Exchange::new(msg);
let path = ExchangeLookupPath::parse("body").unwrap();
assert_eq!(path.lookup(&ex), Some(serde_json::json!({"a": 1})));
}
#[test]
fn lookup_body_returns_none_when_not_json() {
use crate::{Body, Exchange, Message};
let msg = Message::new(Body::Text("hello".into()));
let ex = Exchange::new(msg);
let path = ExchangeLookupPath::parse("body.user").unwrap();
assert_eq!(path.lookup(&ex), None);
}
#[test]
fn lookup_body_returns_none_when_path_misses() {
use crate::{Body, Exchange, Message};
let msg = Message::new(Body::Json(serde_json::json!({"a": 1})));
let ex = Exchange::new(msg);
let path = ExchangeLookupPath::parse("body.b.c").unwrap();
assert_eq!(path.lookup(&ex), None);
}
#[test]
fn lookup_header_flat_dotted_key() {
use crate::{Exchange, Message};
let mut msg = Message::default();
msg.set_header("some.name", serde_json::json!(42));
let ex = Exchange::new(msg);
let path = ExchangeLookupPath::parse("header.some.name").unwrap();
assert_eq!(path.lookup(&ex), Some(serde_json::json!(42)));
}
#[test]
fn lookup_property_flat_dotted_key() {
use crate::{Exchange, Message};
let mut ex = Exchange::new(Message::default());
ex.set_property("config.key", serde_json::json!("v"));
let path = ExchangeLookupPath::parse("property.config.key").unwrap();
assert_eq!(path.lookup(&ex), Some(serde_json::json!("v")));
}
#[test]
fn lookup_unscoped_fallback_body_then_header_then_property() {
use crate::{Body, Exchange, Message};
let mut msg = Message::new(Body::Json(serde_json::json!({"id": 1})));
msg.set_header("id", serde_json::json!(2));
let ex = Exchange::new(msg);
let path = ExchangeLookupPath::parse("id").unwrap();
assert_eq!(path.lookup(&ex), Some(serde_json::json!(1)));
}
#[test]
fn lookup_unscoped_falls_through_to_header() {
use crate::{Exchange, Message};
let mut msg = Message::default();
msg.set_header("token", serde_json::json!("abc"));
let ex = Exchange::new(msg);
let path = ExchangeLookupPath::parse("token").unwrap();
assert_eq!(path.lookup(&ex), Some(serde_json::json!("abc")));
}
#[test]
fn lookup_unscoped_falls_through_to_property() {
use crate::{Exchange, Message};
let mut ex = Exchange::new(Message::default());
ex.set_property("tenant", serde_json::json!("acme"));
let path = ExchangeLookupPath::parse("tenant").unwrap();
assert_eq!(path.lookup(&ex), Some(serde_json::json!("acme")));
}
#[test]
fn lookup_unscoped_returns_none_when_missing_everywhere() {
use crate::{Exchange, Message};
let ex = Exchange::new(Message::default());
let path = ExchangeLookupPath::parse("nope").unwrap();
assert_eq!(path.lookup(&ex), None);
}
}