1use regex::Regex;
8use serde_json::{json, Value};
9use std::collections::HashMap;
10
11pub type TemplateResourceHandler =
13 fn(uri: &str, vars: &HashMap<String, String>) -> Result<ResourceContents, String>;
14
15pub type GenericResourceReadHandler = fn(&str) -> Result<ResourceContents, String>;
17
18#[derive(Debug, Clone)]
24pub struct ResourceContent {
25 pub uri: String,
27 pub mime_type: Option<String>,
29 pub text: Option<String>,
31 pub blob: Option<String>,
33}
34
35impl ResourceContent {
36 pub fn text(uri: impl Into<String>, content: impl Into<String>, mime_type: Option<String>) -> Self {
38 Self {
39 uri: uri.into(),
40 mime_type,
41 text: Some(content.into()),
42 blob: None,
43 }
44 }
45
46 pub fn blob(uri: impl Into<String>, base64_data: impl Into<String>, mime_type: Option<String>) -> Self {
48 Self {
49 uri: uri.into(),
50 mime_type,
51 text: None,
52 blob: Some(base64_data.into()),
53 }
54 }
55
56 pub fn to_json(&self) -> Value {
58 let mut obj = serde_json::Map::new();
59 obj.insert("uri".to_string(), json!(self.uri));
60 if let Some(ref mt) = self.mime_type {
61 obj.insert("mimeType".to_string(), json!(mt));
62 }
63 if let Some(ref t) = self.text {
64 obj.insert("text".to_string(), json!(t));
65 }
66 if let Some(ref b) = self.blob {
67 obj.insert("blob".to_string(), json!(b));
68 }
69 Value::Object(obj)
70 }
71}
72
73pub type ResourceContents = Vec<ResourceContent>;
75
76pub type ResourceHandler = fn(&str) -> Result<ResourceContents, String>;
80
81#[derive(Debug, Clone)]
86pub struct Resource {
87 pub uri: String,
89 pub name: Option<String>,
91 pub title: Option<String>,
93 pub description: Option<String>,
95 pub mime_type: Option<String>,
97 pub handler: ResourceHandler,
99}
100
101impl Resource {
102 pub fn builder(uri: impl Into<String>, handler: ResourceHandler) -> ResourceBuilder {
113 ResourceBuilder {
114 uri: uri.into(),
115 name: None,
116 title: None,
117 description: None,
118 mime_type: None,
119 handler,
120 }
121 }
122
123 pub fn to_list_item(&self) -> Value {
136 let mut obj = serde_json::Map::new();
137 obj.insert("uri".to_string(), json!(self.uri));
138 if let Some(ref n) = self.name {
139 obj.insert("name".to_string(), json!(n));
140 }
141 if let Some(ref t) = self.title {
142 obj.insert("title".to_string(), json!(t));
143 }
144 if let Some(ref d) = self.description {
145 obj.insert("description".to_string(), json!(d));
146 }
147 if let Some(ref mt) = self.mime_type {
148 obj.insert("mimeType".to_string(), json!(mt));
149 }
150 Value::Object(obj)
151 }
152}
153
154pub struct ResourceBuilder {
156 uri: String,
157 name: Option<String>,
158 title: Option<String>,
159 description: Option<String>,
160 mime_type: Option<String>,
161 handler: ResourceHandler,
162}
163
164impl ResourceBuilder {
165 pub fn name(mut self, name: impl Into<String>) -> Self {
167 self.name = Some(name.into());
168 self
169 }
170
171 pub fn title(mut self, title: impl Into<String>) -> Self {
173 self.title = Some(title.into());
174 self
175 }
176
177 pub fn description(mut self, description: impl Into<String>) -> Self {
179 self.description = Some(description.into());
180 self
181 }
182
183 pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
185 self.mime_type = Some(mime_type.into());
186 self
187 }
188
189 pub fn build(self) -> Resource {
191 Resource {
192 uri: self.uri,
193 name: self.name,
194 title: self.title,
195 description: self.description,
196 mime_type: self.mime_type,
197 handler: self.handler,
198 }
199 }
200}
201
202fn compile_uri_template(template: &str) -> Result<(Regex, Vec<String>), ()> {
210 if !template.contains('{') {
211 let re = Regex::new(&format!("^{}$", regex::escape(template))).map_err(|_| ())?;
212 return Ok((re, Vec::new()));
213 }
214
215 let mut var_names = Vec::new();
216 let mut pattern = String::from("^");
217 let mut rest = template;
218
219 while let Some(open) = rest.find('{') {
220 let literal = &rest[..open];
221 pattern.push_str(®ex::escape(literal));
222
223 let after = &rest[open + 1..];
224 let close = after.find('}').ok_or(())?;
225 let name = after[..close].trim();
226 if name.is_empty() {
227 return Err(());
228 }
229 var_names.push(name.to_string());
230 pattern.push_str("([^/]+)");
231 rest = &after[close + 1..];
232 }
233
234 pattern.push_str(®ex::escape(rest));
235 pattern.push('$');
236
237 let re = Regex::new(&pattern).map_err(|_| ())?;
238 Ok((re, var_names))
239}
240
241pub fn match_uri_against_template(uri: &str, template: &str) -> Option<HashMap<String, String>> {
253 let (re, var_names) = compile_uri_template(template).ok()?;
254 let caps = re.captures(uri)?;
255
256 if var_names.is_empty() {
257 return Some(HashMap::new());
258 }
259
260 let mut vars = HashMap::new();
261 for (i, name) in var_names.iter().enumerate() {
262 let group = caps.get(i + 1)?.as_str();
263 vars.insert(name.clone(), group.to_string());
264 }
265 Some(vars)
266}
267
268#[derive(Debug)]
274pub struct CompiledTemplateMatcher {
275 matcher: Regex,
276 var_names: Vec<String>,
277 pub template: ResourceTemplate,
279}
280
281impl CompiledTemplateMatcher {
282 pub fn new(template: ResourceTemplate) -> Result<Self, String> {
286 let (matcher, var_names) = compile_uri_template(&template.uri_template)
287 .map_err(|_| format!("invalid URI template: {}", template.uri_template))?;
288 Ok(Self {
289 matcher,
290 var_names,
291 template,
292 })
293 }
294
295 pub fn match_uri(&self, uri: &str) -> Option<HashMap<String, String>> {
297 let caps = self.matcher.captures(uri)?;
298 let mut vars = HashMap::with_capacity(self.var_names.len());
299 for (i, name) in self.var_names.iter().enumerate() {
300 let group = caps.get(i + 1)?.as_str();
301 vars.insert(name.clone(), group.to_string());
302 }
303 Some(vars)
304 }
305}
306
307#[derive(Debug, Clone)]
311pub struct ResourceTemplate {
312 pub uri_template: String,
314 pub name: Option<String>,
315 pub title: Option<String>,
316 pub description: Option<String>,
317 pub mime_type: Option<String>,
318 pub handler: TemplateResourceHandler,
319}
320
321impl ResourceTemplate {
322 pub fn builder(
323 uri_template: impl Into<String>,
324 handler: TemplateResourceHandler,
325 ) -> ResourceTemplateBuilder {
326 ResourceTemplateBuilder {
327 uri_template: uri_template.into(),
328 name: None,
329 title: None,
330 description: None,
331 mime_type: None,
332 handler,
333 }
334 }
335
336 pub fn to_template_list_item(&self) -> Value {
338 let mut obj = serde_json::Map::new();
339 obj.insert("uriTemplate".to_string(), json!(self.uri_template));
340 if let Some(ref n) = self.name {
341 obj.insert("name".to_string(), json!(n));
342 }
343 if let Some(ref t) = self.title {
344 obj.insert("title".to_string(), json!(t));
345 }
346 if let Some(ref d) = self.description {
347 obj.insert("description".to_string(), json!(d));
348 }
349 if let Some(ref mt) = self.mime_type {
350 obj.insert("mimeType".to_string(), json!(mt));
351 }
352 Value::Object(obj)
353 }
354}
355
356pub struct ResourceTemplateBuilder {
357 uri_template: String,
358 name: Option<String>,
359 title: Option<String>,
360 description: Option<String>,
361 mime_type: Option<String>,
362 handler: TemplateResourceHandler,
363}
364
365impl ResourceTemplateBuilder {
366 pub fn name(mut self, name: impl Into<String>) -> Self {
367 self.name = Some(name.into());
368 self
369 }
370
371 pub fn title(mut self, title: impl Into<String>) -> Self {
372 self.title = Some(title.into());
373 self
374 }
375
376 pub fn description(mut self, description: impl Into<String>) -> Self {
377 self.description = Some(description.into());
378 self
379 }
380
381 pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
382 self.mime_type = Some(mime_type.into());
383 self
384 }
385
386 pub fn build(self) -> ResourceTemplate {
387 ResourceTemplate {
388 uri_template: self.uri_template,
389 name: self.name,
390 title: self.title,
391 description: self.description,
392 mime_type: self.mime_type,
393 handler: self.handler,
394 }
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::{match_uri_against_template, CompiledTemplateMatcher, ResourceTemplate};
401 use std::collections::HashMap;
402
403 fn dummy_handler(_uri: &str, _vars: &HashMap<String, String>) -> Result<super::ResourceContents, String> {
404 Ok(vec![])
405 }
406
407 #[test]
408 fn match_uri_template_trailing_placeholder() {
409 let m = match_uri_against_template("file:///docs/readme.md", "file:///docs/{path}").unwrap();
410 assert_eq!(m.get("path").map(|s| s.as_str()), Some("readme.md"));
411 }
412
413 #[test]
414 fn match_uri_template_two_segments() {
415 let m = match_uri_against_template(
416 "https://example.com/foo",
417 "https://{host}/{path}",
418 )
419 .unwrap();
420 assert_eq!(m.get("host").map(|s| s.as_str()), Some("example.com"));
421 assert_eq!(m.get("path").map(|s| s.as_str()), Some("foo"));
422 }
423
424 #[test]
425 fn match_uri_template_multi_segment_path_needs_more_placeholders() {
426 assert!(
427 match_uri_against_template("https://example.com/foo/bar", "https://{host}/{path}")
428 .is_none(),
429 "two placeholders => two segments; leftover /bar does not match"
430 );
431 let m = match_uri_against_template(
432 "https://example.com/foo/bar",
433 "https://{host}/{a}/{b}",
434 )
435 .unwrap();
436 assert_eq!(m.get("a").map(|s| s.as_str()), Some("foo"));
437 assert_eq!(m.get("b").map(|s| s.as_str()), Some("bar"));
438 }
439
440 #[test]
441 fn compiled_matcher_reuses_regex() {
442 let tmpl = ResourceTemplate::builder("gmc:///reading/{token}", dummy_handler)
443 .name("readings")
444 .build();
445 let matcher = CompiledTemplateMatcher::new(tmpl).unwrap();
446
447 let m1 = matcher.match_uri("gmc:///reading/abc123").unwrap();
448 assert_eq!(m1.get("token").map(|s| s.as_str()), Some("abc123"));
449
450 let m2 = matcher.match_uri("gmc:///reading/xyz789").unwrap();
451 assert_eq!(m2.get("token").map(|s| s.as_str()), Some("xyz789"));
452
453 assert!(matcher.match_uri("gmc:///other/abc").is_none());
454 }
455
456 #[test]
457 fn compiled_matcher_dots_in_literal_are_exact() {
458 let tmpl = ResourceTemplate::builder("https://api.example.com/{id}", dummy_handler)
459 .build();
460 let matcher = CompiledTemplateMatcher::new(tmpl).unwrap();
461
462 assert!(matcher.match_uri("https://api.example.com/42").is_some());
463 assert!(
464 matcher.match_uri("https://apiXexampleYcom/42").is_none(),
465 "dots in the literal must match literally, not as regex wildcards"
466 );
467 }
468
469 #[test]
470 fn compiled_matcher_invalid_template() {
471 let tmpl = ResourceTemplate::builder("bad://{unclosed", dummy_handler).build();
472 assert!(CompiledTemplateMatcher::new(tmpl).is_err());
473 }
474}