use alloc::{
borrow::Cow,
boxed::Box,
format,
string::{String, ToString},
vec::Vec,
};
use core::{future::Future, pin::Pin};
use super::types::{RegistryItem, ResourceDefinition, ResourceOutput, ToolError};
pub trait RustResource: Send + Sync {
type Params: serde::de::DeserializeOwned + Send;
const URI_TEMPLATE: &'static str;
const NAME: &'static str;
const DESCRIPTION: &'static str;
const MIME_TYPE: Option<&'static str>;
fn description(&self) -> Cow<'static, str> {
Cow::Borrowed(Self::DESCRIPTION)
}
fn read(
&self,
uri: &str,
params: Self::Params,
) -> impl Future<Output = Result<ResourceOutput, ToolError>> + Send;
}
#[must_use]
pub fn definition_of_resource<T: RustResource>(resource: &T) -> ResourceDefinition {
ResourceDefinition {
uri_template: T::URI_TEMPLATE.to_string(),
name: T::NAME.to_string(),
description: resource.description().into_owned(),
mime_type: T::MIME_TYPE.map(ToString::to_string),
}
}
#[must_use]
pub fn match_uri_template(
template: &str,
uri: &str,
) -> Option<alloc::collections::BTreeMap<String, String>> {
let mut map = alloc::collections::BTreeMap::new();
let mut t_rem = template;
let mut u_rem = uri;
while let Some(start_idx) = t_rem.find('{') {
let prefix = &t_rem[..start_idx];
if !u_rem.starts_with(prefix) {
return None;
}
u_rem = &u_rem[prefix.len()..];
t_rem = &t_rem[start_idx + 1..];
let end_idx = t_rem.find('}')?;
let var_name = &t_rem[..end_idx];
t_rem = &t_rem[end_idx + 1..];
let val_str = if t_rem.is_empty() {
let val = u_rem;
u_rem = "";
val
} else {
let next_char = t_rem.chars().next()?;
let val_end = u_rem.find(next_char)?;
let val = &u_rem[..val_end];
u_rem = &u_rem[val_end..];
val
};
map.insert(var_name.to_string(), val_str.to_string());
}
if t_rem == u_rem { Some(map) } else { None }
}
pub(crate) type BoxResourceFuture<'a> =
Pin<Box<dyn Future<Output = Result<ResourceOutput, ToolError>> + Send + 'a>>;
pub(crate) trait ErasedResource: Send + Sync {
fn read_erased<'a>(&'a self, uri: &'a str) -> Option<BoxResourceFuture<'a>>;
}
impl<T: RustResource> ErasedResource for T {
fn read_erased<'a>(&'a self, uri: &'a str) -> Option<BoxResourceFuture<'a>> {
let params_map = match_uri_template(T::URI_TEMPLATE, uri)?;
Some(Box::pin(async move {
let deserializer = serde::de::value::MapDeserializer::new(
params_map
.into_iter()
.map(|(k, v)| (k, serde::de::value::StringDeserializer::new(v))),
);
let params: T::Params = serde::de::Deserialize::deserialize(deserializer).map_err(
|e: serde::de::value::Error| {
ToolError::new(format!(
"Failed to deserialize resource parameters from URI variables: {e}"
))
},
)?;
self.read(uri, params).await
}))
}
}
struct RegisteredResource {
name: &'static str,
definition: ResourceDefinition,
erased: Box<dyn ErasedResource>,
}
#[derive(Default)]
pub struct ResourceRegistry {
resources: Vec<RegisteredResource>,
}
impl core::fmt::Debug for ResourceRegistry {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let names: Vec<&str> = self.resources.iter().map(|r| r.name).collect();
f.debug_struct("ResourceRegistry")
.field("resource_count", &self.resources.len())
.field("resource_names", &names)
.finish()
}
}
impl ResourceRegistry {
#[must_use]
pub fn new() -> Self {
Self {
resources: Vec::new(),
}
}
pub fn register<R: RustResource + 'static>(&mut self, resource: R) -> &mut Self {
self.resources.push(RegisteredResource {
name: R::NAME,
definition: definition_of_resource(&resource),
erased: Box::new(resource),
});
self
}
#[must_use]
pub fn with_resource<R: RustResource + 'static>(mut self, resource: R) -> Self {
self.register(resource);
self
}
#[must_use]
pub fn definitions(&self) -> Vec<ResourceDefinition> {
self.resources
.iter()
.map(|entry| entry.definition.clone())
.collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.resources.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.resources.is_empty()
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.resources.iter().any(|entry| entry.name == name)
}
#[must_use]
pub fn matches(&self, uri: &str) -> bool {
self.resources
.iter()
.any(|entry| entry.erased.read_erased(uri).is_some())
}
#[must_use]
pub fn definition(&self, name: &str) -> Option<&ResourceDefinition> {
self.resources
.iter()
.find(|entry| entry.name == name)
.map(|entry| &entry.definition)
}
#[must_use]
pub fn iter(&self) -> ResourceDefinitions<'_> {
ResourceDefinitions {
inner: self.resources.iter(),
}
}
pub async fn read(&self, uri: &str) -> Result<ResourceOutput, ToolError> {
for resource in &self.resources {
if let Some(fut) = resource.erased.read_erased(uri) {
return fut.await;
}
}
Err(ToolError::not_found(RegistryItem::Resource, uri))
}
}
pub struct ResourceDefinitions<'a> {
inner: core::slice::Iter<'a, RegisteredResource>,
}
impl Iterator for ResourceDefinitions<'_> {
type Item = (&'static str, ResourceDefinition);
fn next(&mut self) -> Option<Self::Item> {
self.inner
.next()
.map(|entry| (entry.name, entry.definition.clone()))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl ExactSizeIterator for ResourceDefinitions<'_> {
fn len(&self) -> usize {
self.inner.len()
}
}
impl<'a> IntoIterator for &'a ResourceRegistry {
type Item = (&'static str, ResourceDefinition);
type IntoIter = ResourceDefinitions<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::match_uri_template;
#[test]
fn exact_match_no_variables() {
let m = match_uri_template("config://app", "config://app").expect("should match");
assert!(m.is_empty());
}
#[test]
fn no_match_different_literal() {
assert!(match_uri_template("config://app", "config://other").is_none());
}
#[test]
fn single_trailing_variable_captures_rest() {
let m = match_uri_template("file:///{path}", "file:///etc/hosts").expect("should match");
assert_eq!(m.get("path").map(String::as_str), Some("etc/hosts"));
}
#[test]
fn multiple_variables() {
let m = match_uri_template(
"file:///logs/{date}/{app}.log",
"file:///logs/2024-01-01/server.log",
)
.expect("should match");
assert_eq!(m.get("date").map(String::as_str), Some("2024-01-01"));
assert_eq!(m.get("app").map(String::as_str), Some("server"));
}
#[test]
fn variable_stops_at_delimiter() {
let m = match_uri_template("x://{a}/{b}", "x://one/two").expect("should match");
assert_eq!(m.get("a").map(String::as_str), Some("one"));
assert_eq!(m.get("b").map(String::as_str), Some("two"));
}
#[test]
fn prefix_mismatch_returns_none() {
assert!(match_uri_template("x://{a}", "y://foo").is_none());
}
#[test]
fn unterminated_template_variable_returns_none() {
assert!(match_uri_template("x://{a", "x://foo").is_none());
}
#[test]
fn missing_delimiter_in_uri_returns_none() {
assert!(match_uri_template("x://{a}/end", "x://noslash").is_none());
}
#[test]
fn trailing_literal_must_match() {
assert!(match_uri_template("f://{a}.log", "f://name.txt").is_none());
}
#[test]
fn empty_variable_value_is_allowed() {
let m = match_uri_template("a://{x}/b", "a:///b").expect("should match");
assert_eq!(m.get("x").map(String::as_str), Some(""));
}
#[test]
fn longer_uri_than_literal_template_returns_none() {
assert!(match_uri_template("a://b", "a://bc").is_none());
}
}