use std::borrow::Cow;
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use base64::Engine as _;
use serde::Serialize;
use crate::completion::{CompleteFn, Completer};
use crate::context::Ctx;
use crate::spec::{self, BlobResourceContents, CacheScope, TextResourceContents};
use crate::tool::{IntoToolError, Json, StateRequirement, ToolError};
use crate::uri_template::UriVars;
pub type ResourceContents = spec::ReadResourceResultContentsItem;
pub type ResourceFuture =
Pin<Box<dyn Future<Output = Result<Vec<ResourceContents>, ToolError>> + Send + 'static>>;
pub type ReadFn = fn(uri: &str, vars: &UriVars, ctx: Ctx) -> Result<ResourceFuture, ToolError>;
pub trait Resource: 'static {
fn definition() -> ResourceDef;
}
#[derive(Debug, Clone)]
pub(crate) enum ResourceKind {
Static(spec::Resource),
Template(spec::ResourceTemplate),
}
#[derive(Debug, Clone)]
pub struct ResourceDef {
pub(crate) kind: ResourceKind,
pub(crate) read: ReadFn,
pub(crate) required_state: Vec<StateRequirement>,
pub(crate) ttl: Duration,
pub(crate) cache_scope: CacheScope,
pub(crate) completers: Vec<Completer>,
pub(crate) scopes: Vec<String>,
pub(crate) app: Option<crate::apps::AppUi>,
}
impl ResourceDef {
fn with_kind(kind: ResourceKind, read: ReadFn) -> Self {
Self {
kind,
read,
required_state: Vec::new(),
ttl: Duration::ZERO,
cache_scope: CacheScope::Private,
completers: Vec::new(),
scopes: Vec::new(),
app: None,
}
}
#[must_use]
pub fn app(mut self, ui: crate::apps::AppUi) -> Self {
if let ResourceKind::Static(resource) = &mut self.kind {
resource.mime_type = Some(crate::apps::MIME_TYPE.to_owned());
resource
.meta
.get_or_insert_with(Default::default)
.insert("ui".to_owned(), ui.to_meta());
}
self.app = Some(ui);
self
}
#[must_use]
pub fn is_app(&self) -> bool {
self.app.is_some()
}
#[must_use]
pub fn scopes<I, S>(mut self, scopes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.scopes = scopes.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn required_scopes(&self) -> &[String] {
&self.scopes
}
#[must_use]
pub fn new(resource: spec::Resource, read: ReadFn) -> Self {
Self::with_kind(ResourceKind::Static(resource), read)
}
#[must_use]
pub fn template(template: spec::ResourceTemplate, read: ReadFn) -> Self {
Self::with_kind(ResourceKind::Template(template), read)
}
#[must_use]
pub fn require_state<T: Send + Sync + 'static>(mut self) -> Self {
self.required_state.push(StateRequirement::of::<T>());
self
}
#[must_use]
pub fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = ttl;
self
}
#[must_use]
pub fn cache_scope(mut self, scope: CacheScope) -> Self {
self.cache_scope = scope;
self
}
#[must_use]
pub fn complete(mut self, variable: impl Into<String>, complete: CompleteFn) -> Self {
self.completers.push(Completer {
argument: variable.into(),
complete,
});
self
}
#[must_use]
pub fn uri(&self) -> &str {
match &self.kind {
ResourceKind::Static(r) => &r.uri,
ResourceKind::Template(t) => &t.uri_template,
}
}
#[must_use]
pub fn is_template(&self) -> bool {
matches!(self.kind, ResourceKind::Template(_))
}
}
pub trait IntoResourceContents {
fn into_contents(
self,
uri: &str,
mime_type: Option<&str>,
) -> Result<Vec<ResourceContents>, ToolError>;
}
fn text(uri: &str, mime_type: Option<&str>, text: String) -> Vec<ResourceContents> {
let mut contents = TextResourceContents::new(text, uri.to_owned());
contents.mime_type = mime_type.map(str::to_owned);
vec![ResourceContents::Text(contents)]
}
impl IntoResourceContents for String {
fn into_contents(
self,
uri: &str,
mime_type: Option<&str>,
) -> Result<Vec<ResourceContents>, ToolError> {
Ok(text(uri, mime_type, self))
}
}
impl IntoResourceContents for &'static str {
fn into_contents(
self,
uri: &str,
mime_type: Option<&str>,
) -> Result<Vec<ResourceContents>, ToolError> {
Ok(text(uri, mime_type, self.to_owned()))
}
}
impl IntoResourceContents for Cow<'static, str> {
fn into_contents(
self,
uri: &str,
mime_type: Option<&str>,
) -> Result<Vec<ResourceContents>, ToolError> {
Ok(text(uri, mime_type, self.into_owned()))
}
}
impl IntoResourceContents for Vec<u8> {
fn into_contents(
self,
uri: &str,
mime_type: Option<&str>,
) -> Result<Vec<ResourceContents>, ToolError> {
let blob = base64::engine::general_purpose::STANDARD.encode(self);
let mut contents = BlobResourceContents::new(blob, uri.to_owned());
contents.mime_type = mime_type.map(str::to_owned);
Ok(vec![ResourceContents::Blob(contents)])
}
}
impl<T: Serialize> IntoResourceContents for Json<T> {
fn into_contents(
self,
uri: &str,
mime_type: Option<&str>,
) -> Result<Vec<ResourceContents>, ToolError> {
let json = serde_json::to_string(&self.0).map_err(ToolError::internal)?;
Ok(text(
uri,
Some(mime_type.unwrap_or("application/json")),
json,
))
}
}
impl IntoResourceContents for TextResourceContents {
fn into_contents(self, _: &str, _: Option<&str>) -> Result<Vec<ResourceContents>, ToolError> {
Ok(vec![ResourceContents::Text(self)])
}
}
impl IntoResourceContents for BlobResourceContents {
fn into_contents(self, _: &str, _: Option<&str>) -> Result<Vec<ResourceContents>, ToolError> {
Ok(vec![ResourceContents::Blob(self)])
}
}
impl IntoResourceContents for ResourceContents {
fn into_contents(self, _: &str, _: Option<&str>) -> Result<Vec<ResourceContents>, ToolError> {
Ok(vec![self])
}
}
impl IntoResourceContents for Vec<ResourceContents> {
fn into_contents(self, _: &str, _: Option<&str>) -> Result<Vec<ResourceContents>, ToolError> {
Ok(self)
}
}
impl<T: IntoResourceContents, E: IntoToolError> IntoResourceContents for Result<T, E> {
fn into_contents(
self,
uri: &str,
mime_type: Option<&str>,
) -> Result<Vec<ResourceContents>, ToolError> {
match self {
Ok(value) => value.into_contents(uri, mime_type),
Err(error) => Err(error.into_tool_error()),
}
}
}
pub fn parse_uri_var<T: std::str::FromStr>(vars: &UriVars, name: &str) -> Result<T, ToolError> {
vars.get(name)
.and_then(|v| v.parse().ok())
.ok_or_else(ToolError::not_found)
}
pub fn parse_optional_uri_var<T: std::str::FromStr>(
vars: &UriVars,
name: &str,
) -> Result<Option<T>, ToolError> {
vars.get(name)
.map(|v| v.parse().map_err(|_| ToolError::not_found()))
.transpose()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn contents_conversions() {
let t = "hi".into_contents("a://b", Some("text/plain")).unwrap();
assert_eq!(
serde_json::to_value(&t).unwrap(),
serde_json::json!([{ "uri": "a://b", "mimeType": "text/plain", "text": "hi" }])
);
let b = vec![1u8, 2, 3].into_contents("a://b", None).unwrap();
assert_eq!(
serde_json::to_value(&b).unwrap(),
serde_json::json!([{ "uri": "a://b", "blob": "AQID" }])
);
let j = Json(serde_json::json!({ "x": 1 }))
.into_contents("a://b", None)
.unwrap();
assert_eq!(
serde_json::to_value(&j).unwrap(),
serde_json::json!([{ "uri": "a://b", "mimeType": "application/json", "text": "{\"x\":1}" }])
);
}
}