#![deny(unsafe_code)]
#![warn(missing_docs, rust_2018_idioms)]
pub mod audit;
pub mod operations;
pub use exocortex_storage::{Direction, Invalidation, TraversalSpec, VisibilityContext};
use async_trait::async_trait;
use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Serialize};
pub struct OpContext {
pub visibility_ctx: exocortex_storage::VisibilityContext,
pub storage: std::sync::Arc<dyn exocortex_storage::Storage>,
pub cache: std::sync::Arc<exocortex_cache::LocalCache>,
pub deadline: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, thiserror::Error)]
pub enum OpError {
#[error("bad input: {0}")]
BadInput(String),
#[error("unauthorized: {0}")]
Unauthorized(String),
#[error("not found")]
NotFound,
#[error("deadline exceeded")]
DeadlineExceeded,
#[error("storage: {0}")]
Storage(String),
#[error("{0}")]
Other(String),
}
#[async_trait]
pub trait Operation: Send + Sync + 'static {
type Input: DeserializeOwned + JsonSchema + Send;
type Output: Serialize + JsonSchema + Send;
fn name(&self) -> &'static str;
fn mcp_tool_name(&self) -> &'static str;
fn http_method(&self) -> http::Method;
fn http_path(&self) -> &'static str;
async fn handle(&self, ctx: &OpContext, input: Self::Input) -> Result<Self::Output, OpError>;
}
pub struct OperationEntry {
pub name: &'static str,
pub mcp_tool_name: &'static str,
pub http_method: fn() -> http::Method,
pub http_path: &'static str,
pub input_schema: fn() -> schemars::schema::RootSchema,
pub output_schema: fn() -> schemars::schema::RootSchema,
#[allow(clippy::type_complexity)]
pub handler: for<'a> fn(
&'a OpContext,
serde_json::Value,
)
-> futures::future::BoxFuture<'a, Result<serde_json::Value, OpError>>,
}
inventory::collect!(OperationEntry);
#[macro_export]
macro_rules! register_operation {
($op:ident, $name:literal, $mcp:literal, $method:ident, $path:literal, $input:ty, $output:ty) => {
impl $crate::OperationNames for $op {
const NAME_OVERRIDE: &'static str = $name;
const MCP_NAME_OVERRIDE: &'static str = $mcp;
const HTTP_PATH_OVERRIDE: &'static str = $path;
fn http_method_override() -> http::Method {
http::Method::$method
}
}
inventory::submit! {
$crate::OperationEntry {
name: <$op as $crate::OperationNames>::NAME_OVERRIDE,
mcp_tool_name: <$op as $crate::OperationNames>::MCP_NAME_OVERRIDE,
http_method: <$op as $crate::OperationNames>::http_method_override,
http_path: <$op as $crate::OperationNames>::HTTP_PATH_OVERRIDE,
input_schema: || schemars::schema_for!($input),
output_schema: || schemars::schema_for!($output),
handler: |ctx, v| Box::pin(async move {
let input: $input =
serde_json::from_value(v).map_err(|e| $crate::OpError::BadInput(e.to_string()))?;
let out = $op::default().handle(ctx, input).await?;
serde_json::to_value(out).map_err(|e| $crate::OpError::Other(e.to_string()))
}),
}
}
};
}
pub trait OperationNames {
const NAME_OVERRIDE: &'static str;
const MCP_NAME_OVERRIDE: &'static str;
const HTTP_PATH_OVERRIDE: &'static str;
fn http_method_override() -> http::Method;
}
pub fn entries() -> Vec<&'static OperationEntry> {
let mut all: Vec<&'static OperationEntry> =
inventory::iter::<OperationEntry>.into_iter().collect();
all.sort_by_key(|e| e.name);
all
}