Skip to main content

llm_tool/
rust_resource.rs

1//! Strongly-typed Rust resource trait and URI template matching.
2
3use alloc::{
4    borrow::Cow,
5    boxed::Box,
6    format,
7    string::{String, ToString},
8};
9use core::{future::Future, pin::Pin};
10
11use super::types::{ResourceDefinition, ResourceOutput, ToolError};
12
13/// A custom resource or resource template implemented in Rust.
14pub trait RustResource: Send + Sync {
15    /// Strongly-typed parameters extracted from URI variables.
16    type Params: serde::de::DeserializeOwned + Send;
17
18    /// URI template pattern (e.g. `"file:///logs/{date}/{app}.log"` or `"config://app"`).
19    const URI_TEMPLATE: &'static str;
20
21    /// Unique resource name.
22    const NAME: &'static str;
23
24    /// Human-readable description.
25    const DESCRIPTION: &'static str;
26
27    /// Optional MIME type.
28    const MIME_TYPE: Option<&'static str>;
29
30    /// Return the resource description.
31    fn description(&self) -> Cow<'static, str> {
32        Cow::Borrowed(Self::DESCRIPTION)
33    }
34
35    /// Read the resource with URI and extracted parameters.
36    fn read(
37        &self,
38        uri: &str,
39        params: Self::Params,
40    ) -> impl Future<Output = Result<ResourceOutput, ToolError>> + Send;
41}
42
43/// Build a [`ResourceDefinition`] from any [`RustResource`] implementor.
44pub fn definition_of_resource<T: RustResource>(resource: &T) -> ResourceDefinition {
45    ResourceDefinition {
46        uri_template: T::URI_TEMPLATE.to_string(),
47        name: T::NAME.to_string(),
48        description: resource.description().into_owned(),
49        mime_type: T::MIME_TYPE.map(ToString::to_string),
50    }
51}
52
53/// Helper to match an incoming URI against a URI template pattern with `{variable}` placeholders.
54///
55/// Returns `Some(map)` if the URI matches the pattern, mapping each `{variable}` name
56/// to its extracted value string. Returns `None` if the URI does not match.
57#[must_use]
58pub fn match_uri_template(
59    template: &str,
60    uri: &str,
61) -> Option<alloc::collections::BTreeMap<String, String>> {
62    let mut map = alloc::collections::BTreeMap::new();
63    let mut t_rem = template;
64    let mut u_rem = uri;
65
66    while let Some(start_idx) = t_rem.find('{') {
67        let prefix = &t_rem[..start_idx];
68        if !u_rem.starts_with(prefix) {
69            return None;
70        }
71        u_rem = &u_rem[prefix.len()..];
72        t_rem = &t_rem[start_idx + 1..];
73
74        let end_idx = t_rem.find('}')?;
75        let var_name = &t_rem[..end_idx];
76        t_rem = &t_rem[end_idx + 1..];
77
78        let val_str = if t_rem.is_empty() {
79            let val = u_rem;
80            u_rem = "";
81            val
82        } else {
83            let next_char = t_rem.chars().next()?;
84            let val_end = u_rem.find(next_char)?;
85            let val = &u_rem[..val_end];
86            u_rem = &u_rem[val_end..];
87            val
88        };
89
90        map.insert(var_name.to_string(), val_str.to_string());
91    }
92
93    if t_rem == u_rem { Some(map) } else { None }
94}
95
96/// Type-erased future returned by [`ErasedResource::read_erased`].
97pub type BoxResourceFuture<'a> =
98    Pin<Box<dyn Future<Output = Result<ResourceOutput, ToolError>> + Send + 'a>>;
99
100/// Type-erased wrapper enabling heterogeneous resource storage.
101pub trait ErasedResource: Send + Sync {
102    /// Return the resource definition.
103    fn definition(&self) -> ResourceDefinition;
104
105    /// Check if the incoming URI matches this resource's pattern, extract variables, and execute `read`.
106    fn read_erased<'a>(&'a self, uri: &'a str) -> Option<BoxResourceFuture<'a>>;
107}
108
109impl<T: RustResource> ErasedResource for T {
110    fn definition(&self) -> ResourceDefinition {
111        definition_of_resource(self)
112    }
113
114    fn read_erased<'a>(&'a self, uri: &'a str) -> Option<BoxResourceFuture<'a>> {
115        let params_map = match_uri_template(T::URI_TEMPLATE, uri)?;
116        Some(Box::pin(async move {
117            let deserializer = serde::de::value::MapDeserializer::new(
118                params_map
119                    .into_iter()
120                    .map(|(k, v)| (k, serde::de::value::StringDeserializer::new(v))),
121            );
122            let params: T::Params = serde::de::Deserialize::deserialize(deserializer).map_err(
123                |e: serde::de::value::Error| {
124                    ToolError::new(format!(
125                        "Failed to deserialize resource parameters from URI variables: {e}"
126                    ))
127                },
128            )?;
129            self.read(uri, params).await
130        }))
131    }
132}