use crate::handlers::{McpHandler, ResourcesReadHandler};
use crate::uri_template::{UriTemplate, VariableValidator};
use crate::{McpResult, McpServer};
use async_trait::async_trait;
use serde_json::{Value, json};
use std::collections::HashMap;
use turul_mcp_builders::prelude::*;
use turul_mcp_protocol::meta;
use turul_mcp_protocol::resources::ResourceContent;
#[derive(Clone)]
struct UserProfileResource {
base_pattern: String,
}
impl UserProfileResource {
fn new() -> Self {
Self {
base_pattern: "file:///user/{user_id}.json".to_string(),
}
}
}
impl HasResourceMetadata for UserProfileResource {
fn name(&self) -> &str {
"user_profile"
}
}
impl HasResourceUri for UserProfileResource {
fn uri(&self) -> &str {
&self.base_pattern
}
}
impl HasResourceDescription for UserProfileResource {
fn description(&self) -> Option<&str> {
Some("User profile data with dynamic user ID")
}
}
impl HasResourceMimeType for UserProfileResource {
fn mime_type(&self) -> Option<&str> {
Some("application/json")
}
}
impl HasResourceSize for UserProfileResource {
fn size(&self) -> Option<u64> {
None
}
}
impl HasResourceAnnotations for UserProfileResource {
fn annotations(&self) -> Option<&meta::Annotations> {
None
}
}
impl HasResourceMeta for UserProfileResource {
fn resource_meta(&self) -> Option<&HashMap<String, Value>> {
None
}
}
impl HasIcons for UserProfileResource {}
#[async_trait]
impl crate::McpResource for UserProfileResource {
async fn read(
&self,
params: Option<Value>,
_session: Option<&crate::SessionContext>,
) -> McpResult<Vec<ResourceContent>> {
let params = params.unwrap_or(json!({}));
if let Some(template_vars) = params.get("template_variables")
&& let Some(user_id) = template_vars.get("user_id").and_then(|v| v.as_str())
{
let user_data = json!({
"user_id": user_id,
"name": format!("User {}", user_id),
"email": format!("{}@example.com", user_id),
"profile": {
"created": "2024-01-01",
"active": true
}
});
let uri = format!("file:///user/{}.json", user_id);
return Ok(vec![ResourceContent::text(&uri, user_data.to_string())]);
}
Ok(vec![ResourceContent::text(
&self.base_pattern,
r#"{"error": "Template variables required"}"#,
)])
}
}
#[tokio::test]
async fn test_uri_template_resource_handler_integration() {
let template = UriTemplate::new("file:///user/{user_id}.json")
.unwrap()
.with_validator("user_id", VariableValidator::user_id());
let resource = UserProfileResource::new();
let read_handler = ResourcesReadHandler::new()
.without_security()
.add_template_resource(template, resource);
let read_params = json!({
"uri": "file:///user/alice123.json"
});
let result = read_handler.handle(Some(read_params)).await.unwrap();
assert!(result.is_object());
let contents = result.get("contents").unwrap().as_array().unwrap();
assert_eq!(contents.len(), 1);
let content = &contents[0];
assert_eq!(
content.get("mimeType").unwrap().as_str().unwrap(),
"text/plain"
);
assert_eq!(
content.get("uri").unwrap().as_str().unwrap(),
"file:///user/alice123.json"
);
let text = content.get("text").unwrap().as_str().unwrap();
let parsed: Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed.get("user_id").unwrap().as_str().unwrap(), "alice123");
assert_eq!(
parsed.get("name").unwrap().as_str().unwrap(),
"User alice123"
);
assert_eq!(
parsed.get("email").unwrap().as_str().unwrap(),
"alice123@example.com"
);
}
#[tokio::test]
async fn test_template_validation() {
let template = UriTemplate::new("file:///user/{user_id}.json")
.unwrap()
.with_validator("user_id", VariableValidator::user_id());
let resource = UserProfileResource::new();
let read_handler = ResourcesReadHandler::new()
.without_security()
.add_template_resource(template, resource);
let read_params = json!({
"uri": "file:///user/invalid@user.json"
});
let result = read_handler.handle(Some(read_params)).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_template_fallback_to_exact_uri() {
let template = UriTemplate::new("file:///user/{user_id}.json")
.unwrap()
.with_validator("user_id", VariableValidator::user_id());
let template_resource = UserProfileResource::new();
let static_resource = UserProfileResource {
base_pattern: "file:///static.json".to_string(),
};
let read_handler = ResourcesReadHandler::new()
.without_security()
.add_template_resource(template, template_resource)
.add_resource(static_resource);
let read_params = json!({
"uri": "file:///static.json"
});
let result = read_handler.handle(Some(read_params)).await.unwrap();
assert!(result.is_object());
assert!(result.get("contents").is_some());
}
#[test]
fn test_multiple_variable_template() {
let template = UriTemplate::new("file:///user/{user_id}/avatar.{format}")
.unwrap()
.with_validator("user_id", VariableValidator::user_id())
.with_validator("format", VariableValidator::image_format());
let vars = template
.extract("file:///user/alice123/avatar.png")
.unwrap();
assert_eq!(vars.get("user_id"), Some(&"alice123".to_string()));
assert_eq!(vars.get("format"), Some(&"png".to_string()));
let mut test_vars = HashMap::new();
test_vars.insert("user_id".to_string(), "bob456".to_string());
test_vars.insert("format".to_string(), "jpg".to_string());
let resolved = template.resolve(&test_vars).unwrap();
assert_eq!(resolved, "file:///user/bob456/avatar.jpg");
}
#[test]
fn test_mime_type_detection() {
let json_template = UriTemplate::new("file:///data/{id}.json").unwrap();
assert_eq!(json_template.mime_type(), Some("application/json"));
let image_template = UriTemplate::new("file:///images/{id}.{format}").unwrap();
assert_eq!(image_template.mime_type(), None);
let pdf_template = UriTemplate::new("file:///docs/{id}.pdf").unwrap();
assert_eq!(pdf_template.mime_type(), Some("application/pdf"));
}
#[test]
fn test_builder_template_validation_error_collection() {
let valid_template = UriTemplate::new("file:///valid/{id}.json").unwrap();
let invalid_template_pattern = "invalid template pattern {";
let builder = McpServer::builder()
.name("test")
.version("1.0.0")
.bind_address("127.0.0.1:0".parse().unwrap())
.template_resource(valid_template, UserProfileResource::new());
let invalid_template = match UriTemplate::new(invalid_template_pattern) {
Ok(t) => t,
Err(_) => {
return;
}
};
let builder_with_invalid =
builder.template_resource(invalid_template, UserProfileResource::new());
let result = builder_with_invalid.build();
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("validation errors") || error_msg.contains("Invalid resource template")
);
}