use crate::registry::{Kind, Registry};
pub fn route<'r>(registry: &'r Registry, path: &str) -> Option<(&'r Kind, Option<String>)> {
registry.kinds.iter().find_map(|k| {
let bindings = match_storage(&k.storage, path)?;
let id = if bindings.is_empty() {
None
} else {
Some(bindings.join("/"))
};
Some((k, id))
})
}
pub fn match_storage(pattern: &str, path: &str) -> Option<Vec<String>> {
let pat_segs: Vec<&str> = pattern.split('/').collect();
let path_segs: Vec<&str> = path.split('/').collect();
if pat_segs.len() != path_segs.len() {
return None;
}
let mut bindings = Vec::new();
for (pat, seg) in pat_segs.iter().zip(&path_segs) {
match placeholder(pat) {
Some((prefix, suffix)) => {
let bound = seg.strip_prefix(prefix)?.strip_suffix(suffix)?;
if bound.is_empty() {
return None;
}
bindings.push(bound.to_string());
}
None => {
if pat != seg {
return None;
}
}
}
}
Some(bindings)
}
pub fn storage_path(kind: &Kind, id: Option<&str>) -> Option<String> {
let n_placeholders = kind
.storage
.split('/')
.filter(|s| placeholder(s).is_some())
.count();
let segments: Vec<&str> = match id {
Some(id) => id.split('/').collect(),
None => Vec::new(),
};
if segments.len() != n_placeholders {
return None;
}
let mut next = segments.into_iter();
let out: Vec<String> = kind
.storage
.split('/')
.map(|seg| match placeholder(seg) {
Some((prefix, suffix)) => format!("{prefix}{}{suffix}", next.next().unwrap()),
None => seg.to_string(),
})
.collect();
Some(out.join("/"))
}
pub fn placeholder(segment: &str) -> Option<(&str, &str)> {
let open = segment.find('{')?;
let close = segment.find('}')?;
(open < close).then(|| (&segment[..open], &segment[close + 1..]))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::registry::Field;
fn kind(name: &str, storage: &str) -> Kind {
let _ = Field {
name: String::new(),
doc: String::new(),
ty: crate::registry::FieldType::Str,
role: None,
refers_to: None,
};
Kind {
name: name.into(),
doc: String::new(),
storage: storage.into(),
fields: vec![],
}
}
#[test]
fn routes_and_inverts_single_multi_and_singleton_patterns() {
let r = Registry {
schema_hash: String::new(),
kinds: vec![
kind("todo", "facts/todos/{id}.ron"),
kind("checkin", "facts/checkins/{person}/{date}.ron"),
kind("self", "facts/self.ron"),
],
queries: vec![],
roles: vec![],
};
for (path, kname, id) in [
("facts/todos/ship-it.ron", "todo", Some("ship-it")),
(
"facts/checkins/jane/2026-07-06.ron",
"checkin",
Some("jane/2026-07-06"),
),
("facts/self.ron", "self", None),
] {
let (k, got) = route(&r, path).unwrap();
assert_eq!((k.name.as_str(), got.as_deref()), (kname, id));
assert_eq!(storage_path(k, id).as_deref(), Some(path));
}
assert!(route(&r, "facts/unknown/x.ron").is_none());
assert!(route(&r, "README.md").is_none());
assert!(route(&r, "facts/todos/.ron").is_none(), "empty binding");
let todo = &r.kinds[0];
assert!(storage_path(todo, None).is_none(), "id-kind needs an id");
let checkin = &r.kinds[1];
assert!(storage_path(checkin, Some("only-one-segment")).is_none());
}
}