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),
}
}
const TEMPLATE_VAR_START: char = '{';
const TEMPLATE_VAR_END: char = '}';
const EMPTY_STR: &str = "";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplateSegment<'a> {
Literal(&'a str),
Variable(&'a str),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompiledUriTemplate<'a> {
raw: &'a str,
segments: Vec<TemplateSegment<'a>>,
var_count: usize,
}
impl<'a> CompiledUriTemplate<'a> {
#[must_use]
pub fn new(template: &'a str) -> Self {
if !template.contains(TEMPLATE_VAR_START) {
return Self {
raw: template,
segments: alloc::vec![TemplateSegment::Literal(template)],
var_count: 0,
};
}
let mut segments = Vec::new();
let mut rem = template;
let mut var_count = 0;
while let Some(start_idx) = rem.find(TEMPLATE_VAR_START) {
if start_idx > 0 {
segments.push(TemplateSegment::Literal(&rem[..start_idx]));
}
let after_start = &rem[start_idx + 1..];
if let Some(end_idx) = after_start.find(TEMPLATE_VAR_END) {
let var_name = &after_start[..end_idx];
segments.push(TemplateSegment::Variable(var_name));
var_count += 1;
rem = &after_start[end_idx + 1..];
} else {
segments.push(TemplateSegment::Literal(&rem[start_idx..]));
rem = EMPTY_STR;
break;
}
}
if !rem.is_empty() {
segments.push(TemplateSegment::Literal(rem));
}
Self {
raw: template,
segments,
var_count,
}
}
#[must_use]
pub const fn raw(&self) -> &'a str {
self.raw
}
#[must_use]
pub fn segments(&self) -> &[TemplateSegment<'a>] {
&self.segments
}
#[must_use]
pub const fn var_count(&self) -> usize {
self.var_count
}
#[must_use]
pub fn matches(&self, uri: &str) -> bool {
if self.var_count == 0 {
return self.raw == uri;
}
self.match_with_callback(uri, |_, _| {}).is_some()
}
#[must_use]
pub fn extract_borrowed<'u>(&self, uri: &'u str) -> Option<Vec<(&'a str, &'u str)>> {
if self.var_count == 0 {
return if self.raw == uri {
Some(Vec::new())
} else {
None
};
}
let mut vars = Vec::with_capacity(self.var_count);
self.match_with_callback(uri, |k, v| vars.push((k, v)))?;
Some(vars)
}
fn match_with_callback<'u>(
&self,
uri: &'u str,
mut on_var: impl FnMut(&'a str, &'u str),
) -> Option<()> {
let mut u_rem = uri;
for (i, segment) in self.segments.iter().enumerate() {
match *segment {
TemplateSegment::Literal(lit) => {
if !u_rem.starts_with(lit) {
return None;
}
u_rem = &u_rem[lit.len()..];
}
TemplateSegment::Variable(var_name) => {
let val_str = match self.segments.get(i + 1) {
None => {
let val = u_rem;
u_rem = EMPTY_STR;
val
}
Some(TemplateSegment::Literal(next_lit)) => {
let val_end = u_rem.find(next_lit)?;
let val = &u_rem[..val_end];
u_rem = &u_rem[val_end..];
val
}
Some(TemplateSegment::Variable(_)) => EMPTY_STR,
};
on_var(var_name, val_str);
}
}
}
if u_rem.is_empty() { Some(()) } else { None }
}
}
#[must_use]
pub fn match_uri_template(
template: &str,
uri: &str,
) -> Option<alloc::collections::BTreeMap<String, String>> {
let compiled = CompiledUriTemplate::new(template);
let mut map = alloc::collections::BTreeMap::new();
compiled.match_with_callback(uri, |k, v| {
map.insert(k.to_string(), v.to_string());
})?;
Some(map)
}
pub(crate) type BoxResourceFuture<'a> =
Pin<Box<dyn Future<Output = Result<ResourceOutput, ToolError>> + Send + 'a>>;
pub(crate) trait ErasedResource: Send + Sync {
fn read_with_vars<'a>(
&'a self,
uri: &'a str,
vars: Vec<(&'static str, &'a str)>,
) -> BoxResourceFuture<'a>;
}
impl<T: RustResource> ErasedResource for T {
fn read_with_vars<'a>(
&'a self,
uri: &'a str,
vars: Vec<(&'static str, &'a str)>,
) -> BoxResourceFuture<'a> {
Box::pin(async move {
let deserializer = serde::de::value::MapDeserializer::new(
vars.into_iter()
.map(|(k, v)| (k, serde::de::value::BorrowedStrDeserializer::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,
template: CompiledUriTemplate<'static>,
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 const fn new() -> Self {
Self {
resources: Vec::new(),
}
}
pub fn register<R: RustResource + 'static>(&mut self, resource: R) -> &mut Self {
if let Some(pos) = self.resources.iter().position(|e| e.name == R::NAME) {
self.resources.remove(pos);
}
self.resources.push(RegisteredResource {
name: R::NAME,
definition: definition_of_resource(&resource),
template: CompiledUriTemplate::new(R::URI_TEMPLATE),
erased: Box::new(resource),
});
self
}
#[must_use]
pub fn with_resource<R: RustResource + 'static>(mut self, resource: R) -> Self {
self.register(resource);
self
}
pub fn remove(&mut self, name: &str) -> bool {
if let Some(pos) = self.resources.iter().position(|e| e.name == name) {
self.resources.remove(pos);
true
} else {
false
}
}
pub fn clear(&mut self) {
self.resources.clear();
}
#[must_use]
pub fn definitions(&self) -> Vec<ResourceDefinition> {
self.resources
.iter()
.map(|entry| entry.definition.clone())
.collect()
}
#[must_use]
pub const fn len(&self) -> usize {
self.resources.len()
}
#[must_use]
pub const 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.template.matches(uri))
}
#[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(vars) = resource.template.extract_borrowed(uri) {
return resource.erased.read_with_vars(uri, vars).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());
}
#[test]
fn adjacent_template_variables_match() {
let m = match_uri_template("x://{a}{b}", "x://hello").expect("should match");
assert_eq!(m.get("a").map(String::as_str), Some(""));
assert_eq!(m.get("b").map(String::as_str), Some("hello"));
}
#[tokio::test]
async fn erased_resource_deserialization_failure_on_non_string_param() {
use super::*;
struct IntResource;
#[derive(serde::Deserialize)]
struct IntParams {
count: u64,
}
impl RustResource for IntResource {
const NAME: &'static str = "int_res";
const URI_TEMPLATE: &'static str = "res://{count}";
const DESCRIPTION: &'static str = "Resource with integer param";
const MIME_TYPE: Option<&'static str> = None;
type Params = IntParams;
async fn read(
&self,
uri: &str,
params: Self::Params,
) -> Result<ResourceOutput, ToolError> {
Ok(ResourceOutput::text(uri, None, format!("{}", params.count)))
}
}
let reg = ResourceRegistry::new().with_resource(IntResource);
let err = reg.read("res://42").await.unwrap_err();
assert!(
err.to_string()
.contains("Failed to deserialize resource parameters from URI variables")
);
}
#[test]
fn compiled_uri_template_segments_and_accessors() {
use super::{CompiledUriTemplate, TemplateSegment};
let static_tmpl = CompiledUriTemplate::new("config://app");
assert_eq!(static_tmpl.raw(), "config://app");
assert_eq!(static_tmpl.var_count(), 0);
assert_eq!(
static_tmpl.segments(),
&[TemplateSegment::Literal("config://app")]
);
assert!(static_tmpl.matches("config://app"));
assert!(!static_tmpl.matches("config://other"));
assert_eq!(
static_tmpl.extract_borrowed("config://app"),
Some(alloc::vec::Vec::new())
);
let dyn_tmpl = CompiledUriTemplate::new("file:///logs/{date}/{app}.log");
assert_eq!(dyn_tmpl.raw(), "file:///logs/{date}/{app}.log");
assert_eq!(dyn_tmpl.var_count(), 2);
assert_eq!(
dyn_tmpl.segments(),
&[
TemplateSegment::Literal("file:///logs/"),
TemplateSegment::Variable("date"),
TemplateSegment::Literal("/"),
TemplateSegment::Variable("app"),
TemplateSegment::Literal(".log"),
]
);
}
#[test]
fn compiled_uri_template_extract_borrowed_zero_copy() {
use super::CompiledUriTemplate;
let tmpl = CompiledUriTemplate::new("res://{host}:{port}/{path}");
let uri = "res://localhost:8080/api/v1";
let extracted = tmpl.extract_borrowed(uri).expect("should match");
assert_eq!(
extracted,
alloc::vec![("host", "localhost"), ("port", "8080"), ("path", "api/v1"),]
);
let uri_start = uri.as_ptr() as usize;
let uri_end = uri_start + uri.len();
for (_, val) in extracted {
let val_ptr = val.as_ptr() as usize;
assert!(val_ptr >= uri_start && val_ptr + val.len() <= uri_end);
}
}
}