use regex::Regex;
use std::collections::HashSet;
pub struct Route {
pub regex: Regex,
pub fields: HashSet<String>,
pub resource: Box<dyn RouteResource>,
pub suffix: Option<String>,
}
impl Route {
pub fn new(
regex: Regex,
fields: HashSet<String>,
resource: Box<dyn RouteResource>,
suffix: Option<String>,
) -> Self {
Self {
regex,
fields,
resource,
suffix,
}
}
}
pub trait RouteResource: Send + Sync {
fn process_reply(
&self,
serder: &crate::keri::core::serdering::SerderKERI,
saider: &crate::cesr::saider::Saider,
route: &str,
cigars: Option<&[crate::cesr::indexing::siger::Siger]>,
tsgs: Option<
&[(
crate::cesr::prefixer::Prefixer,
crate::cesr::seqner::Seqner,
crate::cesr::saider::Saider,
Vec<crate::cesr::indexing::siger::Siger>,
)],
>,
params: std::collections::HashMap<String, String>,
) -> Result<(), crate::keri::KERIError>;
fn process_route_not_found(
&self,
serder: &crate::keri::core::serdering::SerderKERI,
saider: &crate::cesr::saider::Saider,
route: &str,
cigars: Option<&[crate::cesr::indexing::siger::Siger]>,
tsgs: Option<
&[(
crate::cesr::prefixer::Prefixer,
crate::cesr::seqner::Seqner,
crate::cesr::saider::Saider,
Vec<crate::cesr::indexing::siger::Siger>,
)],
>,
params: std::collections::HashMap<String, String>,
) -> Result<(), crate::keri::KERIError> {
Err(crate::keri::KERIError::ValueError(format!(
"Resource registered for route {} does not contain the correct processReply method",
route
)))
}
}
pub fn compile_uri_template(
template: &str,
) -> Result<(HashSet<String>, Regex), crate::keri::KERIError> {
if !template.starts_with('/') {
return Err(crate::keri::KERIError::ValueError(
"uri_template must start with '/'".to_string(),
));
}
if template.contains("//") {
return Err(crate::keri::KERIError::ValueError(
"uri_template may not contain '//'".to_string(),
));
}
let mut normalized_template = template.to_string();
if normalized_template != "/" && normalized_template.ends_with('/') {
normalized_template.pop();
}
let expression_pattern = regex::Regex::new(r"\{([a-zA-Z]\w*)\}")
.map_err(|e| crate::keri::KERIError::ValueError(format!("Invalid regex pattern: {}", e)))?;
let mut fields = HashSet::new();
for caps in expression_pattern.captures_iter(&normalized_template) {
if let Some(field_name) = caps.get(1) {
fields.insert(field_name.as_str().to_string());
}
}
let mut escaped = String::new();
for ch in normalized_template.chars() {
match ch {
'.' | '(' | ')' | '[' | ']' | '?' | '*' | '+' | '^' | '|' => {
escaped.push('\\');
escaped.push(ch);
}
_ => escaped.push(ch),
}
}
let pattern = expression_pattern.replace_all(&escaped, r"(?P<$1>[^/]+)");
let final_pattern = format!(r"^{}$", pattern);
let regex = Regex::new(&final_pattern).map_err(|e| {
crate::keri::KERIError::ValueError(format!("Invalid compiled regex: {}", e))
})?;
Ok((fields, regex))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compile_uri_template_simple() {
let (fields, regex) = compile_uri_template("/books").unwrap();
assert!(fields.is_empty());
assert!(regex.is_match("/books"));
assert!(!regex.is_match("/books/123"));
}
#[test]
fn test_compile_uri_template_with_param() {
let (fields, regex) = compile_uri_template("/books/{isbn}").unwrap();
assert_eq!(fields.len(), 1);
assert!(fields.contains("isbn"));
assert!(regex.is_match("/books/123"));
assert!(!regex.is_match("/books/123/characters"));
}
#[test]
fn test_compile_uri_template_multiple_params() {
let (fields, regex) = compile_uri_template("/books/{isbn}/characters/{name}").unwrap();
assert_eq!(fields.len(), 2);
assert!(fields.contains("isbn"));
assert!(fields.contains("name"));
assert!(regex.is_match("/books/123/characters/alice"));
}
#[test]
fn test_compile_uri_template_invalid() {
assert!(compile_uri_template("books").is_err()); assert!(compile_uri_template("/books//test").is_err()); }
#[test]
fn test_compile_uri_template_trailing_slash() {
let (fields, regex) = compile_uri_template("/books/").unwrap();
assert!(fields.is_empty());
assert!(regex.is_match("/books"));
assert!(!regex.is_match("/books/"));
}
}