use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathError {
pub placeholder: String,
}
impl std::fmt::Display for PathError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"path placeholder {{{}}} is missing, empty or not text/number in the payload",
self.placeholder
)
}
}
pub fn interpolate_path(template: &str, payload: &Value) -> Result<String, PathError> {
if !template.contains('{') {
return Ok(template.to_string());
}
let mut result = String::with_capacity(template.len() + 32);
let mut chars = template.chars().peekable();
while let Some(ch) = chars.next() {
if ch != '{' {
result.push(ch);
continue;
}
let mut key = String::new();
let mut closed = false;
for inner in chars.by_ref() {
if inner == '}' {
closed = true;
break;
}
key.push(inner);
}
if closed && !key.is_empty() {
let value = match payload.get(&key) {
Some(Value::String(s)) => s.clone(),
Some(Value::Number(n)) => n.to_string(),
Some(Value::Bool(b)) => b.to_string(),
_ => String::new(),
};
if value.is_empty() {
return Err(PathError { placeholder: key });
}
result.push_str(&value);
} else {
result.push('{');
result.push_str(&key);
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn ok(t: &str, p: &Value) -> String {
interpolate_path(t, p).expect("resolves")
}
#[test]
fn static_path_returned_unchanged() {
let p = json!({});
assert_eq!(ok("/places/hall", &p), "/places/hall");
assert_eq!(ok("/players", &p), "/players");
assert_eq!(ok("/", &p), "/");
assert_eq!(ok("", &p), "");
}
#[test]
fn placeholders_resolved() {
let p = json!({ "session_id": "abc123", "room_type": "arena", "user_id": "u9" });
assert_eq!(ok("/players/{session_id}", &p), "/players/abc123");
assert_eq!(ok("/rooms/{room_type}/players/{user_id}", &p), "/rooms/arena/players/u9");
}
#[test]
fn numbers_and_booleans_are_written_as_text() {
let p = json!({ "seat": 3, "ready": true });
assert_eq!(ok("/seats/{seat}/{ready}", &p), "/seats/3/true");
}
#[test]
fn a_missing_placeholder_is_refused_not_collapsed() {
let err = interpolate_path("/players/{session_id}", &json!({})).unwrap_err();
assert_eq!(err.placeholder, "session_id");
}
#[test]
fn empty_null_or_structured_values_are_refused() {
for v in [json!(""), Value::Null, json!({ "a": 1 }), json!([1])] {
let p = json!({ "session_id": v });
assert!(interpolate_path("/players/{session_id}", &p).is_err());
}
}
#[test]
fn malformed_unclosed_placeholder_emitted_verbatim() {
let p = json!({ "session_id": "abc" });
assert_eq!(ok("/players/{session_id", &p), "/players/{session_id");
}
}