use std::collections::{BTreeSet, HashMap};
use std::sync::{Arc, RwLock};
use crate::context::{NotificationSender, ServerNotification};
use crate::tool::Tool;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct RegistryChange {
pub added: Vec<String>,
pub removed: Vec<String>,
pub replaced: Vec<String>,
}
impl RegistryChange {
fn between<'a>(
previous: impl IntoIterator<Item = &'a String>,
current: impl IntoIterator<Item = &'a String>,
) -> Self {
let previous: BTreeSet<&String> = previous.into_iter().collect();
let current: BTreeSet<&String> = current.into_iter().collect();
Self {
added: current
.difference(&previous)
.map(|key| (*key).clone())
.collect(),
removed: previous
.difference(¤t)
.map(|key| (*key).clone())
.collect(),
replaced: current
.intersection(&previous)
.map(|key| (*key).clone())
.collect(),
}
}
}
pub(crate) struct DynamicToolsInner {
tools: RwLock<HashMap<String, Arc<Tool>>>,
notification_senders: RwLock<Vec<NotificationSender>>,
}
impl DynamicToolsInner {
pub(crate) fn new() -> Self {
Self {
tools: RwLock::new(HashMap::new()),
notification_senders: RwLock::new(Vec::new()),
}
}
pub(crate) fn add_notification_sender(&self, sender: NotificationSender) {
let mut senders = self.notification_senders.write().unwrap();
senders.push(sender);
}
fn broadcast_tools_changed(&self) {
let mut senders = self.notification_senders.write().unwrap();
senders.retain(|tx| !tx.is_closed());
for tx in senders.iter() {
let _ = tx.try_send(ServerNotification::ToolsListChanged);
}
}
pub(crate) fn list(&self) -> Vec<Arc<Tool>> {
let tools = self.tools.read().unwrap();
tools.values().cloned().collect()
}
pub(crate) fn get(&self, name: &str) -> Option<Arc<Tool>> {
let tools = self.tools.read().unwrap();
tools.get(name).cloned()
}
pub(crate) fn contains(&self, name: &str) -> bool {
let tools = self.tools.read().unwrap();
tools.contains_key(name)
}
}
#[derive(Clone)]
pub struct DynamicToolRegistry {
inner: Arc<DynamicToolsInner>,
}
impl DynamicToolRegistry {
pub(crate) fn new(inner: Arc<DynamicToolsInner>) -> Self {
Self { inner }
}
pub fn register(&self, tool: Tool) {
{
let mut tools = self.inner.tools.write().unwrap();
tools.insert(tool.name.clone(), Arc::new(tool));
}
self.inner.broadcast_tools_changed();
}
pub fn unregister(&self, name: &str) -> bool {
let removed = {
let mut tools = self.inner.tools.write().unwrap();
tools.remove(name).is_some()
};
if removed {
self.inner.broadcast_tools_changed();
}
removed
}
pub fn replace_all(&self, tools: impl IntoIterator<Item = Tool>) -> RegistryChange {
let tools: HashMap<String, Arc<Tool>> = tools
.into_iter()
.map(|tool| (tool.name.clone(), Arc::new(tool)))
.collect();
let names: Vec<String> = tools.keys().cloned().collect();
let previous = {
let mut current = self.inner.tools.write().unwrap();
std::mem::replace(&mut *current, tools)
};
self.inner.broadcast_tools_changed();
RegistryChange::between(previous.keys(), &names)
}
pub fn list(&self) -> Vec<Arc<Tool>> {
self.inner.list()
}
pub fn contains(&self, name: &str) -> bool {
self.inner.contains(name)
}
}
pub(crate) struct DynamicPromptsInner {
prompts: RwLock<HashMap<String, Arc<crate::prompt::Prompt>>>,
notification_senders: RwLock<Vec<NotificationSender>>,
}
impl DynamicPromptsInner {
pub(crate) fn new() -> Self {
Self {
prompts: RwLock::new(HashMap::new()),
notification_senders: RwLock::new(Vec::new()),
}
}
pub(crate) fn add_notification_sender(&self, sender: NotificationSender) {
let mut senders = self.notification_senders.write().unwrap();
senders.push(sender);
}
fn broadcast_prompts_changed(&self) {
let mut senders = self.notification_senders.write().unwrap();
senders.retain(|tx| !tx.is_closed());
for tx in senders.iter() {
let _ = tx.try_send(ServerNotification::PromptsListChanged);
}
}
pub(crate) fn list(&self) -> Vec<Arc<crate::prompt::Prompt>> {
let prompts = self.prompts.read().unwrap();
prompts.values().cloned().collect()
}
pub(crate) fn get(&self, name: &str) -> Option<Arc<crate::prompt::Prompt>> {
let prompts = self.prompts.read().unwrap();
prompts.get(name).cloned()
}
pub(crate) fn contains(&self, name: &str) -> bool {
let prompts = self.prompts.read().unwrap();
prompts.contains_key(name)
}
}
#[derive(Clone)]
pub struct DynamicPromptRegistry {
inner: Arc<DynamicPromptsInner>,
}
impl DynamicPromptRegistry {
pub(crate) fn new(inner: Arc<DynamicPromptsInner>) -> Self {
Self { inner }
}
pub fn register(&self, prompt: crate::prompt::Prompt) {
{
let mut prompts = self.inner.prompts.write().unwrap();
prompts.insert(prompt.name.clone(), Arc::new(prompt));
}
self.inner.broadcast_prompts_changed();
}
pub fn unregister(&self, name: &str) -> bool {
let removed = {
let mut prompts = self.inner.prompts.write().unwrap();
prompts.remove(name).is_some()
};
if removed {
self.inner.broadcast_prompts_changed();
}
removed
}
pub fn replace_all(
&self,
prompts: impl IntoIterator<Item = crate::prompt::Prompt>,
) -> RegistryChange {
let prompts: HashMap<String, Arc<crate::prompt::Prompt>> = prompts
.into_iter()
.map(|prompt| (prompt.name.clone(), Arc::new(prompt)))
.collect();
let names: Vec<String> = prompts.keys().cloned().collect();
let previous = {
let mut current = self.inner.prompts.write().unwrap();
std::mem::replace(&mut *current, prompts)
};
self.inner.broadcast_prompts_changed();
RegistryChange::between(previous.keys(), &names)
}
pub fn list(&self) -> Vec<Arc<crate::prompt::Prompt>> {
self.inner.list()
}
pub fn contains(&self, name: &str) -> bool {
self.inner.contains(name)
}
}
pub(crate) struct DynamicResourcesInner {
resources: RwLock<HashMap<String, Arc<crate::resource::Resource>>>,
notification_senders: RwLock<Vec<NotificationSender>>,
}
impl DynamicResourcesInner {
pub(crate) fn new() -> Self {
Self {
resources: RwLock::new(HashMap::new()),
notification_senders: RwLock::new(Vec::new()),
}
}
pub(crate) fn add_notification_sender(&self, sender: NotificationSender) {
let mut senders = self.notification_senders.write().unwrap();
senders.push(sender);
}
fn broadcast_resources_changed(&self) {
let mut senders = self.notification_senders.write().unwrap();
senders.retain(|tx| !tx.is_closed());
for tx in senders.iter() {
let _ = tx.try_send(ServerNotification::ResourcesListChanged);
}
}
pub(crate) fn list(&self) -> Vec<Arc<crate::resource::Resource>> {
let resources = self.resources.read().unwrap();
resources.values().cloned().collect()
}
pub(crate) fn get(&self, uri: &str) -> Option<Arc<crate::resource::Resource>> {
let resources = self.resources.read().unwrap();
resources.get(uri).cloned()
}
}
#[derive(Clone)]
pub struct DynamicResourceRegistry {
inner: Arc<DynamicResourcesInner>,
}
impl DynamicResourceRegistry {
pub(crate) fn new(inner: Arc<DynamicResourcesInner>) -> Self {
Self { inner }
}
pub fn register(&self, resource: crate::resource::Resource) {
{
let mut resources = self.inner.resources.write().unwrap();
resources.insert(resource.uri.clone(), Arc::new(resource));
}
self.inner.broadcast_resources_changed();
}
pub fn unregister(&self, uri: &str) -> bool {
let removed = {
let mut resources = self.inner.resources.write().unwrap();
resources.remove(uri).is_some()
};
if removed {
self.inner.broadcast_resources_changed();
}
removed
}
pub fn replace_all(
&self,
resources: impl IntoIterator<Item = crate::resource::Resource>,
) -> RegistryChange {
let resources: HashMap<String, Arc<crate::resource::Resource>> = resources
.into_iter()
.map(|resource| (resource.uri.clone(), Arc::new(resource)))
.collect();
let uris: Vec<String> = resources.keys().cloned().collect();
let previous = {
let mut current = self.inner.resources.write().unwrap();
std::mem::replace(&mut *current, resources)
};
self.inner.broadcast_resources_changed();
RegistryChange::between(previous.keys(), &uris)
}
pub fn list(&self) -> Vec<Arc<crate::resource::Resource>> {
self.inner.list()
}
pub fn contains(&self, uri: &str) -> bool {
let resources = self.inner.resources.read().unwrap();
resources.contains_key(uri)
}
}
pub(crate) struct DynamicResourceTemplatesInner {
templates: RwLock<Vec<Arc<crate::resource::ResourceTemplate>>>,
notification_senders: RwLock<Vec<NotificationSender>>,
}
impl DynamicResourceTemplatesInner {
pub(crate) fn new() -> Self {
Self {
templates: RwLock::new(Vec::new()),
notification_senders: RwLock::new(Vec::new()),
}
}
pub(crate) fn add_notification_sender(&self, sender: NotificationSender) {
let mut senders = self.notification_senders.write().unwrap();
senders.push(sender);
}
fn broadcast_resources_changed(&self) {
let mut senders = self.notification_senders.write().unwrap();
senders.retain(|tx| !tx.is_closed());
for tx in senders.iter() {
let _ = tx.try_send(ServerNotification::ResourcesListChanged);
}
}
pub(crate) fn list(&self) -> Vec<Arc<crate::resource::ResourceTemplate>> {
let templates = self.templates.read().unwrap();
templates.clone()
}
pub(crate) fn match_uri(
&self,
uri: &str,
) -> Option<(
Arc<crate::resource::ResourceTemplate>,
std::collections::HashMap<String, String>,
)> {
let templates = self.templates.read().unwrap();
for template in templates.iter() {
if let Some(variables) = template.match_uri(uri) {
return Some((Arc::clone(template), variables));
}
}
None
}
}
#[derive(Clone)]
pub struct DynamicResourceTemplateRegistry {
inner: Arc<DynamicResourceTemplatesInner>,
}
impl DynamicResourceTemplateRegistry {
pub(crate) fn new(inner: Arc<DynamicResourceTemplatesInner>) -> Self {
Self { inner }
}
pub fn register(&self, template: crate::resource::ResourceTemplate) {
{
let mut templates = self.inner.templates.write().unwrap();
templates.retain(|t| t.uri_template != template.uri_template);
templates.push(Arc::new(template));
}
self.inner.broadcast_resources_changed();
}
pub fn unregister(&self, uri_template: &str) -> bool {
let removed = {
let mut templates = self.inner.templates.write().unwrap();
let before = templates.len();
templates.retain(|t| t.uri_template != uri_template);
templates.len() < before
};
if removed {
self.inner.broadcast_resources_changed();
}
removed
}
pub fn replace_all(
&self,
templates: impl IntoIterator<Item = crate::resource::ResourceTemplate>,
) -> RegistryChange {
let mut ordered: Vec<Arc<crate::resource::ResourceTemplate>> = Vec::new();
for template in templates {
ordered.retain(|t| t.uri_template != template.uri_template);
ordered.push(Arc::new(template));
}
let patterns: Vec<String> = ordered.iter().map(|t| t.uri_template.clone()).collect();
let previous = {
let mut current = self.inner.templates.write().unwrap();
std::mem::replace(&mut *current, ordered)
};
self.inner.broadcast_resources_changed();
RegistryChange::between(previous.iter().map(|t| &t.uri_template), &patterns)
}
pub fn list(&self) -> Vec<Arc<crate::resource::ResourceTemplate>> {
self.inner.list()
}
pub fn contains(&self, uri_template: &str) -> bool {
let templates = self.inner.templates.read().unwrap();
templates.iter().any(|t| t.uri_template == uri_template)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CallToolResult;
use crate::tool::ToolBuilder;
use tokio::sync::mpsc;
fn make_tool(name: &str) -> Tool {
ToolBuilder::new(name)
.description(format!("Test tool: {name}"))
.no_params_handler(|| async { Ok(CallToolResult::text("ok")) })
.build()
}
fn make_registry() -> (DynamicToolRegistry, Arc<DynamicToolsInner>) {
let inner = Arc::new(DynamicToolsInner::new());
let registry = DynamicToolRegistry::new(inner.clone());
(registry, inner)
}
#[test]
fn test_register_and_list() {
let (registry, _) = make_registry();
assert!(registry.list().is_empty());
registry.register(make_tool("tool_a"));
assert_eq!(registry.list().len(), 1);
assert!(registry.contains("tool_a"));
registry.register(make_tool("tool_b"));
assert_eq!(registry.list().len(), 2);
assert!(registry.contains("tool_b"));
}
#[test]
fn test_unregister() {
let (registry, _) = make_registry();
registry.register(make_tool("tool_a"));
registry.register(make_tool("tool_b"));
assert_eq!(registry.list().len(), 2);
assert!(registry.unregister("tool_a"));
assert_eq!(registry.list().len(), 1);
assert!(!registry.contains("tool_a"));
assert!(registry.contains("tool_b"));
}
#[test]
fn test_unregister_nonexistent() {
let (registry, _) = make_registry();
assert!(!registry.unregister("no_such_tool"));
}
#[test]
fn test_register_replaces_existing() {
let (registry, _) = make_registry();
registry.register(make_tool("tool_a"));
registry.register(make_tool("tool_a"));
assert_eq!(registry.list().len(), 1);
}
#[test]
fn test_contains() {
let (registry, _) = make_registry();
assert!(!registry.contains("tool_a"));
registry.register(make_tool("tool_a"));
assert!(registry.contains("tool_a"));
registry.unregister("tool_a");
assert!(!registry.contains("tool_a"));
}
#[test]
fn test_inner_get() {
let (registry, inner) = make_registry();
assert!(inner.get("tool_a").is_none());
registry.register(make_tool("tool_a"));
let tool = inner.get("tool_a").unwrap();
assert_eq!(tool.name, "tool_a");
}
#[tokio::test]
async fn test_broadcast_on_register() {
let (registry, inner) = make_registry();
let (tx, mut rx) = mpsc::channel(16);
inner.add_notification_sender(tx);
registry.register(make_tool("tool_a"));
let notification = rx.try_recv().unwrap();
assert!(matches!(notification, ServerNotification::ToolsListChanged));
}
#[tokio::test]
async fn test_broadcast_on_unregister() {
let (registry, inner) = make_registry();
registry.register(make_tool("tool_a"));
let (tx, mut rx) = mpsc::channel(16);
inner.add_notification_sender(tx);
registry.unregister("tool_a");
let notification = rx.try_recv().unwrap();
assert!(matches!(notification, ServerNotification::ToolsListChanged));
}
#[tokio::test]
async fn test_no_broadcast_on_unregister_nonexistent() {
let (registry, inner) = make_registry();
let (tx, mut rx) = mpsc::channel(16);
inner.add_notification_sender(tx);
registry.unregister("no_such_tool");
assert!(rx.try_recv().is_err());
}
#[tokio::test]
async fn test_closed_senders_are_cleaned_up() {
let (registry, inner) = make_registry();
let (tx, rx) = mpsc::channel(16);
inner.add_notification_sender(tx);
drop(rx);
registry.register(make_tool("tool_a"));
let (tx2, mut rx2) = mpsc::channel(16);
inner.add_notification_sender(tx2);
registry.register(make_tool("tool_b"));
let notification = rx2.try_recv().unwrap();
assert!(matches!(notification, ServerNotification::ToolsListChanged));
}
#[tokio::test]
async fn test_replace_all_reports_change_and_broadcasts_once() {
let (registry, inner) = make_registry();
registry.register(make_tool("tool_a"));
registry.register(make_tool("tool_b"));
registry.register(make_tool("tool_c"));
let (tx, mut rx) = mpsc::channel(16);
inner.add_notification_sender(tx);
let change = registry.replace_all([make_tool("tool_d"), make_tool("tool_b")]);
assert_eq!(change.added, ["tool_d"]);
assert_eq!(change.removed, ["tool_a", "tool_c"]);
assert_eq!(change.replaced, ["tool_b"]);
let mut names: Vec<String> = registry.list().iter().map(|t| t.name.clone()).collect();
names.sort();
assert_eq!(names, ["tool_b", "tool_d"]);
assert!(matches!(
rx.try_recv().unwrap(),
ServerNotification::ToolsListChanged
));
assert!(
rx.try_recv().is_err(),
"replace_all broadcasts exactly once"
);
}
#[tokio::test]
async fn test_replace_all_broadcasts_when_unchanged() {
let (registry, inner) = make_registry();
let (tx, mut rx) = mpsc::channel(16);
inner.add_notification_sender(tx);
let change = registry.replace_all(Vec::new());
assert_eq!(change, RegistryChange::default());
assert!(matches!(
rx.try_recv().unwrap(),
ServerNotification::ToolsListChanged
));
assert!(rx.try_recv().is_err());
}
#[test]
fn test_replace_all_keeps_last_duplicate() {
let (registry, inner) = make_registry();
let first = ToolBuilder::new("dup")
.description("first")
.no_params_handler(|| async { Ok(CallToolResult::text("first")) })
.build();
let second = ToolBuilder::new("dup")
.description("second")
.no_params_handler(|| async { Ok(CallToolResult::text("second")) })
.build();
let change = registry.replace_all([first, second]);
assert_eq!(change.added, ["dup"]);
assert!(change.removed.is_empty());
assert!(change.replaced.is_empty());
assert_eq!(registry.list().len(), 1);
assert_eq!(
inner.get("dup").unwrap().description.as_deref(),
Some("second")
);
}
#[test]
fn test_replace_all_keeps_in_flight_tool() {
let (registry, inner) = make_registry();
registry.register(make_tool("tool_a"));
let held = inner.get("tool_a").unwrap();
registry.replace_all(Vec::new());
assert!(!registry.contains("tool_a"));
assert_eq!(held.name, "tool_a");
}
fn make_prompt(name: &str) -> crate::prompt::Prompt {
crate::prompt::PromptBuilder::new(name)
.description(format!("Test prompt: {name}"))
.user_message("ok")
}
fn make_prompt_registry() -> (DynamicPromptRegistry, Arc<DynamicPromptsInner>) {
let inner = Arc::new(DynamicPromptsInner::new());
let registry = DynamicPromptRegistry::new(inner.clone());
(registry, inner)
}
#[test]
fn test_prompt_register_and_list() {
let (registry, _) = make_prompt_registry();
assert!(registry.list().is_empty());
registry.register(make_prompt("prompt_a"));
assert_eq!(registry.list().len(), 1);
assert!(registry.contains("prompt_a"));
registry.register(make_prompt("prompt_b"));
assert_eq!(registry.list().len(), 2);
assert!(registry.contains("prompt_b"));
}
#[test]
fn test_prompt_unregister() {
let (registry, _) = make_prompt_registry();
registry.register(make_prompt("prompt_a"));
registry.register(make_prompt("prompt_b"));
assert!(registry.unregister("prompt_a"));
assert_eq!(registry.list().len(), 1);
assert!(!registry.contains("prompt_a"));
assert!(registry.contains("prompt_b"));
}
#[test]
fn test_prompt_unregister_nonexistent() {
let (registry, _) = make_prompt_registry();
assert!(!registry.unregister("no_such_prompt"));
}
#[tokio::test]
async fn test_prompt_broadcast_on_register() {
let (registry, inner) = make_prompt_registry();
let (tx, mut rx) = mpsc::channel(16);
inner.add_notification_sender(tx);
registry.register(make_prompt("prompt_a"));
let notification = rx.try_recv().unwrap();
assert!(matches!(
notification,
ServerNotification::PromptsListChanged
));
}
#[tokio::test]
async fn test_prompt_broadcast_on_unregister() {
let (registry, inner) = make_prompt_registry();
registry.register(make_prompt("prompt_a"));
let (tx, mut rx) = mpsc::channel(16);
inner.add_notification_sender(tx);
registry.unregister("prompt_a");
let notification = rx.try_recv().unwrap();
assert!(matches!(
notification,
ServerNotification::PromptsListChanged
));
}
#[tokio::test]
async fn test_prompt_replace_all() {
let (registry, inner) = make_prompt_registry();
registry.register(make_prompt("prompt_a"));
registry.register(make_prompt("prompt_b"));
let (tx, mut rx) = mpsc::channel(16);
inner.add_notification_sender(tx);
let change = registry.replace_all([make_prompt("prompt_b"), make_prompt("prompt_c")]);
assert_eq!(change.added, ["prompt_c"]);
assert_eq!(change.removed, ["prompt_a"]);
assert_eq!(change.replaced, ["prompt_b"]);
assert!(!registry.contains("prompt_a"));
assert!(registry.contains("prompt_c"));
assert!(matches!(
rx.try_recv().unwrap(),
ServerNotification::PromptsListChanged
));
assert!(rx.try_recv().is_err());
}
fn make_resource(uri: &str) -> crate::resource::Resource {
crate::resource::ResourceBuilder::new(uri)
.name(uri)
.text("content")
}
fn make_resource_registry() -> (DynamicResourceRegistry, Arc<DynamicResourcesInner>) {
let inner = Arc::new(DynamicResourcesInner::new());
let registry = DynamicResourceRegistry::new(inner.clone());
(registry, inner)
}
#[test]
fn test_resource_register_and_list() {
let (registry, _) = make_resource_registry();
assert!(registry.list().is_empty());
registry.register(make_resource("file:///a.txt"));
assert_eq!(registry.list().len(), 1);
assert!(registry.contains("file:///a.txt"));
registry.register(make_resource("file:///b.txt"));
assert_eq!(registry.list().len(), 2);
}
#[test]
fn test_resource_unregister() {
let (registry, _) = make_resource_registry();
registry.register(make_resource("file:///a.txt"));
registry.register(make_resource("file:///b.txt"));
assert!(registry.unregister("file:///a.txt"));
assert_eq!(registry.list().len(), 1);
assert!(!registry.contains("file:///a.txt"));
assert!(registry.contains("file:///b.txt"));
}
#[test]
fn test_resource_unregister_nonexistent() {
let (registry, _) = make_resource_registry();
assert!(!registry.unregister("file:///nope"));
}
#[tokio::test]
async fn test_resource_broadcast_on_register() {
let (registry, inner) = make_resource_registry();
let (tx, mut rx) = mpsc::channel(16);
inner.add_notification_sender(tx);
registry.register(make_resource("file:///a.txt"));
let notification = rx.try_recv().unwrap();
assert!(matches!(
notification,
ServerNotification::ResourcesListChanged
));
}
#[tokio::test]
async fn test_resource_replace_all() {
let (registry, inner) = make_resource_registry();
registry.register(make_resource("file:///a.txt"));
registry.register(make_resource("file:///b.txt"));
let (tx, mut rx) = mpsc::channel(16);
inner.add_notification_sender(tx);
let change = registry.replace_all([
make_resource("file:///c.txt"),
make_resource("file:///a.txt"),
]);
assert_eq!(change.added, ["file:///c.txt"]);
assert_eq!(change.removed, ["file:///b.txt"]);
assert_eq!(change.replaced, ["file:///a.txt"]);
assert!(!registry.contains("file:///b.txt"));
assert!(inner.get("file:///c.txt").is_some());
assert!(matches!(
rx.try_recv().unwrap(),
ServerNotification::ResourcesListChanged
));
assert!(rx.try_recv().is_err());
}
fn make_template_registry() -> (
DynamicResourceTemplateRegistry,
Arc<DynamicResourceTemplatesInner>,
) {
let inner = Arc::new(DynamicResourceTemplatesInner::new());
let registry = DynamicResourceTemplateRegistry::new(inner.clone());
(registry, inner)
}
#[test]
fn test_template_register_and_list() {
use crate::resource::ResourceTemplateBuilder;
let (registry, _) = make_template_registry();
assert!(registry.list().is_empty());
let template = ResourceTemplateBuilder::new("db://tables/{table}")
.name("Tables")
.handler(
|uri: String, _vars: std::collections::HashMap<String, String>| async move {
Ok(crate::protocol::ReadResourceResult {
contents: vec![crate::protocol::ResourceContent {
uri,
mime_type: None,
text: Some("data".to_string()),
blob: None,
meta: None,
}],
meta: None,
..Default::default()
})
},
);
registry.register(template);
assert_eq!(registry.list().len(), 1);
assert!(registry.contains("db://tables/{table}"));
}
#[test]
fn test_template_unregister() {
use crate::resource::ResourceTemplateBuilder;
let (registry, _) = make_template_registry();
let template = ResourceTemplateBuilder::new("db://tables/{table}")
.name("Tables")
.handler(
|uri: String, _vars: std::collections::HashMap<String, String>| async move {
Ok(crate::protocol::ReadResourceResult {
contents: vec![crate::protocol::ResourceContent {
uri,
mime_type: None,
text: Some("data".to_string()),
blob: None,
meta: None,
}],
meta: None,
..Default::default()
})
},
);
registry.register(template);
assert!(registry.unregister("db://tables/{table}"));
assert!(registry.list().is_empty());
assert!(!registry.unregister("db://tables/{table}"));
}
#[tokio::test]
async fn test_template_broadcast_on_register() {
use crate::resource::ResourceTemplateBuilder;
let (registry, inner) = make_template_registry();
let (tx, mut rx) = mpsc::channel(16);
inner.add_notification_sender(tx);
let template = ResourceTemplateBuilder::new("db://tables/{table}")
.name("Tables")
.handler(
|uri: String, _vars: std::collections::HashMap<String, String>| async move {
Ok(crate::protocol::ReadResourceResult {
contents: vec![crate::protocol::ResourceContent {
uri,
mime_type: None,
text: Some("data".to_string()),
blob: None,
meta: None,
}],
meta: None,
..Default::default()
})
},
);
registry.register(template);
let notification = rx.try_recv().unwrap();
assert!(matches!(
notification,
ServerNotification::ResourcesListChanged
));
}
#[tokio::test]
async fn test_template_match_uri() {
use crate::resource::ResourceTemplateBuilder;
let (_, inner) = make_template_registry();
let template = ResourceTemplateBuilder::new("db://tables/{table}")
.name("Tables")
.handler(
|uri: String, _vars: std::collections::HashMap<String, String>| async move {
Ok(crate::protocol::ReadResourceResult {
contents: vec![crate::protocol::ResourceContent {
uri,
mime_type: None,
text: Some("data".to_string()),
blob: None,
meta: None,
}],
meta: None,
..Default::default()
})
},
);
{
let mut templates = inner.templates.write().unwrap();
templates.push(Arc::new(template));
}
let result = inner.match_uri("db://tables/users");
assert!(result.is_some());
let (_, vars) = result.unwrap();
assert_eq!(vars.get("table").unwrap(), "users");
assert!(inner.match_uri("db://other/path").is_none());
}
fn make_template(pattern: &str, body: &'static str) -> crate::resource::ResourceTemplate {
crate::resource::ResourceTemplateBuilder::new(pattern)
.name(pattern)
.handler(
move |uri: String, _vars: std::collections::HashMap<String, String>| async move {
Ok(crate::protocol::ReadResourceResult::text(uri, body))
},
)
}
#[tokio::test]
async fn test_template_replace_all() {
let (registry, inner) = make_template_registry();
registry.register(make_template("db://tables/{table}", "table"));
registry.register(make_template("db://views/{view}", "view"));
let (tx, mut rx) = mpsc::channel(16);
inner.add_notification_sender(tx);
let change = registry.replace_all([
make_template("db://tables/{table}", "table v2"),
make_template("db://indexes/{index}", "index"),
]);
assert_eq!(change.added, ["db://indexes/{index}"]);
assert_eq!(change.removed, ["db://views/{view}"]);
assert_eq!(change.replaced, ["db://tables/{table}"]);
assert!(matches!(
rx.try_recv().unwrap(),
ServerNotification::ResourcesListChanged
));
assert!(rx.try_recv().is_err());
let (template, vars) = inner.match_uri("db://tables/users").unwrap();
let result = template.read("db://tables/users", vars).await.unwrap();
assert_eq!(result.contents[0].text.as_deref(), Some("table v2"));
assert!(inner.match_uri("db://views/v").is_none());
}
#[tokio::test]
async fn test_template_replace_all_keeps_given_order() {
let (registry, inner) = make_template_registry();
registry.replace_all([
make_template("db://users/{id}", "narrow"),
make_template("db://{+rest}", "wide"),
]);
let (template, _) = inner.match_uri("db://users/1").unwrap();
assert_eq!(template.uri_template, "db://users/{id}");
let change = registry.replace_all([
make_template("db://users/{id}", "first"),
make_template("db://{+rest}", "wide"),
make_template("db://users/{id}", "last"),
]);
assert_eq!(change.replaced, ["db://users/{id}", "db://{+rest}"]);
let patterns: Vec<String> = registry
.list()
.iter()
.map(|t| t.uri_template.clone())
.collect();
assert_eq!(patterns, ["db://{+rest}", "db://users/{id}"]);
let (template, _) = inner.match_uri("db://users/1").unwrap();
assert_eq!(template.uri_template, "db://{+rest}");
}
}