use serde_json::{Map, Value};
pub trait Extension: Send + Sync + 'static {
fn id(&self) -> &str;
fn settings(&self) -> Map<String, Value> {
Map::new()
}
}
pub(crate) fn is_valid_id(id: &str) -> bool {
let Some((prefix, name)) = id.split_once('/') else {
return false;
};
let label_ok = |label: &str| {
let mut chars = label.chars();
let first_ok = chars.next().is_some_and(|c| c.is_ascii_alphabetic());
let last_ok = label
.chars()
.last()
.is_some_and(|c| c.is_ascii_alphanumeric());
first_ok && last_ok && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
};
let name_ok = !name.is_empty()
&& name.starts_with(|c: char| c.is_ascii_alphanumeric())
&& name.ends_with(|c: char| c.is_ascii_alphanumeric())
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'));
prefix.split('.').all(label_ok) && name_ok
}
#[cfg(test)]
mod tests {
use super::is_valid_id;
#[test]
fn extension_ids() {
assert!(is_valid_id("io.modelcontextprotocol/tasks"));
assert!(is_valid_id("com.example/my-extension"));
assert!(!is_valid_id("tasks"));
assert!(!is_valid_id("com.example/"));
assert!(!is_valid_id("1com.example/x"));
}
}