pub mod error;
pub mod extract;
pub use error::{ErrorCode, ErrorResponse, IntoErrorCode, SchemaValidationError};
pub use extract::Context;
#[cfg(feature = "ws")]
pub use extract::WsSender;
#[cfg(feature = "cli")]
pub trait CliSubcommand {
fn cli_command() -> ::clap::Command;
fn cli_dispatch(&self, matches: &::clap::ArgMatches) -> Result<(), Box<dyn std::error::Error>>;
}
#[cfg(feature = "mcp")]
pub trait McpNamespace {
fn mcp_namespace_tools() -> Vec<serde_json::Value>;
fn mcp_namespace_tool_names() -> Vec<String>;
fn mcp_namespace_call(
&self,
name: &str,
args: serde_json::Value,
) -> Result<serde_json::Value, String>;
fn mcp_namespace_call_async(
&self,
name: &str,
args: serde_json::Value,
) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
}
#[cfg(feature = "jsonrpc")]
pub trait JsonRpcMount {
fn jsonrpc_mount_methods() -> Vec<String>;
fn jsonrpc_mount_dispatch(
&self,
method: &str,
params: serde_json::Value,
) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
}
#[cfg(feature = "ws")]
pub trait WsMount {
fn ws_mount_methods() -> Vec<String>;
fn ws_mount_dispatch(
&self,
method: &str,
params: serde_json::Value,
) -> Result<serde_json::Value, String>;
fn ws_mount_dispatch_async(
&self,
method: &str,
params: serde_json::Value,
) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
}
#[cfg(feature = "http")]
pub trait HttpMount: Send + Sync + 'static {
fn http_mount_router(self: ::std::sync::Arc<Self>) -> ::axum::Router;
fn http_mount_openapi_paths() -> Vec<crate::HttpMountPathInfo>
where
Self: Sized;
}
#[cfg(feature = "http")]
#[derive(Debug, Clone)]
pub struct HttpMountPathInfo {
pub path: String,
pub method: String,
pub summary: Option<String>,
}
#[cfg(feature = "cli")]
pub fn cli_format_output(
value: serde_json::Value,
jsonl: bool,
json: bool,
jq: Option<&str>,
) -> Result<String, Box<dyn std::error::Error>> {
if let Some(filter) = jq {
use jaq_core::load::{Arena, File as JaqFile, Loader};
use jaq_core::{Compiler, Ctx, Vars, data, unwrap_valr};
use jaq_json::Val;
let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs()));
let arena = Arena::default();
let program = JaqFile {
code: filter,
path: (),
};
let modules = loader
.load(&arena, program)
.map_err(|errs| format!("jq parse error: {:?}", errs))?;
let filter_compiled = Compiler::default()
.with_funs(jaq_std::funs().chain(jaq_json::funs()))
.compile(modules)
.map_err(|errs| format!("jq compile error: {:?}", errs))?;
let val: Val = serde_json::from_value(value)?;
let ctx = Ctx::<data::JustLut<Val>>::new(&filter_compiled.lut, Vars::new([]));
let out = filter_compiled.id.run((ctx, val)).map(unwrap_valr);
let mut results = Vec::new();
for result in out {
match result {
Ok(v) => results.push(v.to_string()),
Err(e) => return Err(format!("jq runtime error: {:?}", e).into()),
}
}
Ok(results.join("\n"))
} else if jsonl {
match value {
serde_json::Value::Array(items) => {
let lines: Vec<String> = items
.iter()
.map(serde_json::to_string)
.collect::<Result<_, _>>()?;
Ok(lines.join("\n"))
}
other => Ok(serde_json::to_string(&other)?),
}
} else if json {
Ok(serde_json::to_string(&value)?)
} else {
Ok(serde_json::to_string_pretty(&value)?)
}
}
#[cfg(feature = "jsonschema")]
pub fn cli_schema_for<T: schemars::JsonSchema>() -> serde_json::Value {
serde_json::to_value(schemars::schema_for!(T))
.unwrap_or_else(|_| serde_json::json!({"type": "object"}))
}
#[cfg(all(feature = "cli", feature = "jsonschema"))]
#[derive(Clone)]
pub struct SchemaValueParser<T: Clone + Send + Sync + 'static> {
variants: Option<std::sync::Arc<[&'static str]>>,
_marker: std::marker::PhantomData<T>,
}
#[cfg(all(feature = "cli", feature = "jsonschema"))]
impl<T> Default for SchemaValueParser<T>
where
T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
{
fn default() -> Self {
Self::new()
}
}
#[cfg(all(feature = "cli", feature = "jsonschema"))]
impl<T> SchemaValueParser<T>
where
T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
{
pub fn new() -> Self {
let variants = extract_enum_variants::<T>().map(|strings| {
let leaked: Vec<&'static str> = strings
.into_iter()
.map(|s| Box::leak(s.into_boxed_str()) as &'static str)
.collect();
leaked.into()
});
Self {
variants,
_marker: std::marker::PhantomData,
}
}
}
#[cfg(all(feature = "cli", feature = "jsonschema"))]
fn extract_enum_variants<T: schemars::JsonSchema>() -> Option<Vec<String>> {
let schema_value = serde_json::to_value(schemars::schema_for!(T)).ok()?;
let enum_values = schema_value.get("enum")?.as_array()?;
let variants: Vec<String> = enum_values
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
if variants.is_empty() {
None
} else {
Some(variants)
}
}
#[cfg(all(feature = "cli", feature = "jsonschema"))]
impl<T> ::clap::builder::TypedValueParser for SchemaValueParser<T>
where
T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
{
type Value = T;
fn parse_ref(
&self,
_cmd: &::clap::Command,
_arg: Option<&::clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<T, ::clap::Error> {
let s = value
.to_str()
.ok_or_else(|| ::clap::Error::new(::clap::error::ErrorKind::InvalidUtf8))?;
s.parse::<T>()
.map_err(|_| ::clap::Error::new(::clap::error::ErrorKind::InvalidValue))
}
fn possible_values(
&self,
) -> Option<Box<dyn Iterator<Item = ::clap::builder::PossibleValue> + '_>> {
let variants = self.variants.as_ref()?;
Some(Box::new(
variants
.iter()
.copied()
.map(::clap::builder::PossibleValue::new),
))
}
}
#[derive(Debug, Clone)]
pub struct MethodInfo {
pub name: String,
pub docs: Option<String>,
pub params: Vec<ParamInfo>,
pub return_type: String,
pub is_async: bool,
pub is_streaming: bool,
pub is_optional: bool,
pub is_result: bool,
}
#[derive(Debug, Clone)]
pub struct ParamInfo {
pub name: String,
pub ty: String,
pub is_optional: bool,
pub is_id: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpMethod {
Get,
Post,
Put,
Patch,
Delete,
}
impl HttpMethod {
pub fn infer_from_name(name: &str) -> Self {
if name.starts_with("get_")
|| name.starts_with("fetch_")
|| name.starts_with("read_")
|| name.starts_with("list_")
|| name.starts_with("find_")
|| name.starts_with("search_")
{
HttpMethod::Get
} else if name.starts_with("create_")
|| name.starts_with("add_")
|| name.starts_with("new_")
{
HttpMethod::Post
} else if name.starts_with("update_") || name.starts_with("set_") {
HttpMethod::Put
} else if name.starts_with("patch_") || name.starts_with("modify_") {
HttpMethod::Patch
} else if name.starts_with("delete_") || name.starts_with("remove_") {
HttpMethod::Delete
} else {
HttpMethod::Post
}
}
pub fn as_str(&self) -> &'static str {
match self {
HttpMethod::Get => "GET",
HttpMethod::Post => "POST",
HttpMethod::Put => "PUT",
HttpMethod::Patch => "PATCH",
HttpMethod::Delete => "DELETE",
}
}
}
pub fn infer_path(method_name: &str, http_method: HttpMethod) -> String {
let resource = method_name
.strip_prefix("get_")
.or_else(|| method_name.strip_prefix("fetch_"))
.or_else(|| method_name.strip_prefix("read_"))
.or_else(|| method_name.strip_prefix("list_"))
.or_else(|| method_name.strip_prefix("find_"))
.or_else(|| method_name.strip_prefix("search_"))
.or_else(|| method_name.strip_prefix("create_"))
.or_else(|| method_name.strip_prefix("add_"))
.or_else(|| method_name.strip_prefix("new_"))
.or_else(|| method_name.strip_prefix("update_"))
.or_else(|| method_name.strip_prefix("set_"))
.or_else(|| method_name.strip_prefix("patch_"))
.or_else(|| method_name.strip_prefix("modify_"))
.or_else(|| method_name.strip_prefix("delete_"))
.or_else(|| method_name.strip_prefix("remove_"))
.unwrap_or(method_name);
let path_resource = if resource.ends_with('s') {
resource.to_string()
} else {
format!("{resource}s")
};
match http_method {
HttpMethod::Post => format!("/{path_resource}"),
HttpMethod::Get
if method_name.starts_with("list_")
|| method_name.starts_with("search_")
|| method_name.starts_with("find_") =>
{
format!("/{path_resource}")
}
HttpMethod::Get | HttpMethod::Put | HttpMethod::Patch | HttpMethod::Delete => {
format!("/{path_resource}/{{id}}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_http_method_inference() {
assert_eq!(HttpMethod::infer_from_name("get_user"), HttpMethod::Get);
assert_eq!(HttpMethod::infer_from_name("list_users"), HttpMethod::Get);
assert_eq!(HttpMethod::infer_from_name("create_user"), HttpMethod::Post);
assert_eq!(HttpMethod::infer_from_name("update_user"), HttpMethod::Put);
assert_eq!(
HttpMethod::infer_from_name("delete_user"),
HttpMethod::Delete
);
assert_eq!(
HttpMethod::infer_from_name("do_something"),
HttpMethod::Post
); }
#[test]
fn test_path_inference() {
assert_eq!(infer_path("create_user", HttpMethod::Post), "/users");
assert_eq!(infer_path("get_user", HttpMethod::Get), "/users/{id}");
assert_eq!(infer_path("list_users", HttpMethod::Get), "/users");
assert_eq!(infer_path("delete_user", HttpMethod::Delete), "/users/{id}");
}
}