1pub mod error;
6pub mod extract;
7
8pub use error::{ErrorCode, ErrorResponse, IntoErrorCode, SchemaValidationError};
9pub use extract::Context;
10
11#[cfg(feature = "ws")]
12pub use extract::WsSender;
13
14#[cfg(feature = "cli")]
19pub trait CliSubcommand {
20 fn cli_command() -> ::clap::Command;
22
23 fn cli_dispatch(&self, matches: &::clap::ArgMatches) -> Result<(), Box<dyn std::error::Error>>;
25}
26
27#[cfg(feature = "mcp")]
32pub trait McpNamespace {
33 fn mcp_namespace_tools() -> Vec<serde_json::Value>;
35
36 fn mcp_namespace_tool_names() -> Vec<String>;
38
39 fn mcp_namespace_call(
41 &self,
42 name: &str,
43 args: serde_json::Value,
44 ) -> Result<serde_json::Value, String>;
45
46 fn mcp_namespace_call_async(
48 &self,
49 name: &str,
50 args: serde_json::Value,
51 ) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
52}
53
54#[cfg(feature = "jsonrpc")]
59pub trait JsonRpcMount {
60 fn jsonrpc_mount_methods() -> Vec<String>;
62
63 fn jsonrpc_mount_dispatch(
65 &self,
66 method: &str,
67 params: serde_json::Value,
68 ) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
69}
70
71#[cfg(feature = "ws")]
76pub trait WsMount {
77 fn ws_mount_methods() -> Vec<String>;
79
80 fn ws_mount_dispatch(
82 &self,
83 method: &str,
84 params: serde_json::Value,
85 ) -> Result<serde_json::Value, String>;
86
87 fn ws_mount_dispatch_async(
89 &self,
90 method: &str,
91 params: serde_json::Value,
92 ) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
93}
94
95#[cfg(feature = "http")]
100pub trait HttpMount: Send + Sync + 'static {
101 fn http_mount_router(self: ::std::sync::Arc<Self>) -> ::axum::Router;
103
104 fn http_mount_openapi_paths() -> Vec<crate::HttpMountPathInfo>
106 where
107 Self: Sized;
108}
109
110#[cfg(feature = "http")]
112#[derive(Debug, Clone)]
113pub struct HttpMountPathInfo {
114 pub path: String,
116 pub method: String,
118 pub summary: Option<String>,
120}
121
122#[cfg(feature = "cli")]
129pub fn cli_format_output(
130 value: serde_json::Value,
131 jsonl: bool,
132 json: bool,
133 jq: Option<&str>,
134) -> Result<String, Box<dyn std::error::Error>> {
135 if let Some(filter) = jq {
136 use jaq_core::load::{Arena, File as JaqFile, Loader};
137 use jaq_core::{Compiler, Ctx, RcIter};
138 use jaq_json::Val;
139
140 let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs()));
141 let arena = Arena::default();
142
143 let program = JaqFile {
144 code: filter,
145 path: (),
146 };
147
148 let modules = loader
149 .load(&arena, program)
150 .map_err(|errs| format!("jq parse error: {:?}", errs))?;
151
152 let filter_compiled = Compiler::default()
153 .with_funs(jaq_std::funs().chain(jaq_json::funs()))
154 .compile(modules)
155 .map_err(|errs| format!("jq compile error: {:?}", errs))?;
156
157 let val = Val::from(value);
158 let inputs = RcIter::new(core::iter::empty());
159 let out = filter_compiled.run((Ctx::new([], &inputs), val));
160
161 let mut results = Vec::new();
162 for result in out {
163 match result {
164 Ok(v) => results.push(v.to_string()),
165 Err(e) => return Err(format!("jq runtime error: {:?}", e).into()),
166 }
167 }
168
169 Ok(results.join("\n"))
170 } else if jsonl {
171 match value {
172 serde_json::Value::Array(items) => {
173 let lines: Vec<String> = items
174 .iter()
175 .map(serde_json::to_string)
176 .collect::<Result<_, _>>()?;
177 Ok(lines.join("\n"))
178 }
179 other => Ok(serde_json::to_string(&other)?),
180 }
181 } else if json {
182 Ok(serde_json::to_string(&value)?)
183 } else {
184 Ok(serde_json::to_string_pretty(&value)?)
185 }
186}
187
188#[cfg(feature = "jsonschema")]
194pub fn cli_schema_for<T: schemars::JsonSchema>() -> serde_json::Value {
195 serde_json::to_value(schemars::schema_for!(T))
196 .unwrap_or_else(|_| serde_json::json!({"type": "object"}))
197}
198
199#[derive(Debug, Clone)]
210pub struct MethodInfo {
211 pub name: String,
213 pub docs: Option<String>,
215 pub params: Vec<ParamInfo>,
217 pub return_type: String,
219 pub is_async: bool,
221 pub is_streaming: bool,
223 pub is_optional: bool,
225 pub is_result: bool,
227}
228
229#[derive(Debug, Clone)]
234pub struct ParamInfo {
235 pub name: String,
237 pub ty: String,
239 pub is_optional: bool,
241 pub is_id: bool,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub enum HttpMethod {
248 Get,
249 Post,
250 Put,
251 Patch,
252 Delete,
253}
254
255impl HttpMethod {
256 pub fn infer_from_name(name: &str) -> Self {
258 if name.starts_with("get_")
259 || name.starts_with("fetch_")
260 || name.starts_with("read_")
261 || name.starts_with("list_")
262 || name.starts_with("find_")
263 || name.starts_with("search_")
264 {
265 HttpMethod::Get
266 } else if name.starts_with("create_")
267 || name.starts_with("add_")
268 || name.starts_with("new_")
269 {
270 HttpMethod::Post
271 } else if name.starts_with("update_") || name.starts_with("set_") {
272 HttpMethod::Put
273 } else if name.starts_with("patch_") || name.starts_with("modify_") {
274 HttpMethod::Patch
275 } else if name.starts_with("delete_") || name.starts_with("remove_") {
276 HttpMethod::Delete
277 } else {
278 HttpMethod::Post
280 }
281 }
282
283 pub fn as_str(&self) -> &'static str {
284 match self {
285 HttpMethod::Get => "GET",
286 HttpMethod::Post => "POST",
287 HttpMethod::Put => "PUT",
288 HttpMethod::Patch => "PATCH",
289 HttpMethod::Delete => "DELETE",
290 }
291 }
292}
293
294pub fn infer_path(method_name: &str, http_method: HttpMethod) -> String {
296 let resource = method_name
298 .strip_prefix("get_")
299 .or_else(|| method_name.strip_prefix("fetch_"))
300 .or_else(|| method_name.strip_prefix("read_"))
301 .or_else(|| method_name.strip_prefix("list_"))
302 .or_else(|| method_name.strip_prefix("find_"))
303 .or_else(|| method_name.strip_prefix("search_"))
304 .or_else(|| method_name.strip_prefix("create_"))
305 .or_else(|| method_name.strip_prefix("add_"))
306 .or_else(|| method_name.strip_prefix("new_"))
307 .or_else(|| method_name.strip_prefix("update_"))
308 .or_else(|| method_name.strip_prefix("set_"))
309 .or_else(|| method_name.strip_prefix("patch_"))
310 .or_else(|| method_name.strip_prefix("modify_"))
311 .or_else(|| method_name.strip_prefix("delete_"))
312 .or_else(|| method_name.strip_prefix("remove_"))
313 .unwrap_or(method_name);
314
315 let path_resource = if resource.ends_with('s') {
317 resource.to_string()
318 } else {
319 format!("{resource}s")
320 };
321
322 match http_method {
323 HttpMethod::Post => format!("/{path_resource}"),
325 HttpMethod::Get
326 if method_name.starts_with("list_")
327 || method_name.starts_with("search_")
328 || method_name.starts_with("find_") =>
329 {
330 format!("/{path_resource}")
331 }
332 HttpMethod::Get | HttpMethod::Put | HttpMethod::Patch | HttpMethod::Delete => {
334 format!("/{path_resource}/{{id}}")
335 }
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn test_http_method_inference() {
345 assert_eq!(HttpMethod::infer_from_name("get_user"), HttpMethod::Get);
346 assert_eq!(HttpMethod::infer_from_name("list_users"), HttpMethod::Get);
347 assert_eq!(HttpMethod::infer_from_name("create_user"), HttpMethod::Post);
348 assert_eq!(HttpMethod::infer_from_name("update_user"), HttpMethod::Put);
349 assert_eq!(
350 HttpMethod::infer_from_name("delete_user"),
351 HttpMethod::Delete
352 );
353 assert_eq!(
354 HttpMethod::infer_from_name("do_something"),
355 HttpMethod::Post
356 ); }
358
359 #[test]
360 fn test_path_inference() {
361 assert_eq!(infer_path("create_user", HttpMethod::Post), "/users");
362 assert_eq!(infer_path("get_user", HttpMethod::Get), "/users/{id}");
363 assert_eq!(infer_path("list_users", HttpMethod::Get), "/users");
364 assert_eq!(infer_path("delete_user", HttpMethod::Delete), "/users/{id}");
365 }
366}