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, Vars, data, unwrap_valr};
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 = serde_json::from_value(value)?;
158 let ctx = Ctx::<data::JustLut<Val>>::new(&filter_compiled.lut, Vars::new([]));
159 let out = filter_compiled.id.run((ctx, val)).map(unwrap_valr);
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#[cfg(all(feature = "cli", feature = "jsonschema"))]
210#[derive(Clone)]
211pub struct SchemaValueParser<T: Clone + Send + Sync + 'static> {
212 variants: Option<std::sync::Arc<[&'static str]>>,
217 _marker: std::marker::PhantomData<T>,
218}
219
220#[cfg(all(feature = "cli", feature = "jsonschema"))]
221impl<T> Default for SchemaValueParser<T>
222where
223 T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
224{
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230#[cfg(all(feature = "cli", feature = "jsonschema"))]
231impl<T> SchemaValueParser<T>
232where
233 T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
234{
235 pub fn new() -> Self {
236 let variants = extract_enum_variants::<T>().map(|strings| {
237 let leaked: Vec<&'static str> = strings
238 .into_iter()
239 .map(|s| Box::leak(s.into_boxed_str()) as &'static str)
240 .collect();
241 leaked.into()
242 });
243 Self {
244 variants,
245 _marker: std::marker::PhantomData,
246 }
247 }
248}
249
250#[cfg(all(feature = "cli", feature = "jsonschema"))]
251fn extract_enum_variants<T: schemars::JsonSchema>() -> Option<Vec<String>> {
252 let schema_value = serde_json::to_value(schemars::schema_for!(T)).ok()?;
253 let enum_values = schema_value.get("enum")?.as_array()?;
254 let variants: Vec<String> = enum_values
255 .iter()
256 .filter_map(|v| v.as_str().map(String::from))
257 .collect();
258 if variants.is_empty() {
259 None
260 } else {
261 Some(variants)
262 }
263}
264
265#[cfg(all(feature = "cli", feature = "jsonschema"))]
266impl<T> ::clap::builder::TypedValueParser for SchemaValueParser<T>
267where
268 T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
269{
270 type Value = T;
271
272 fn parse_ref(
273 &self,
274 _cmd: &::clap::Command,
275 _arg: Option<&::clap::Arg>,
276 value: &std::ffi::OsStr,
277 ) -> Result<T, ::clap::Error> {
278 let s = value
279 .to_str()
280 .ok_or_else(|| ::clap::Error::new(::clap::error::ErrorKind::InvalidUtf8))?;
281 s.parse::<T>()
282 .map_err(|_| ::clap::Error::new(::clap::error::ErrorKind::InvalidValue))
283 }
284
285 fn possible_values(
286 &self,
287 ) -> Option<Box<dyn Iterator<Item = ::clap::builder::PossibleValue> + '_>> {
288 let variants = self.variants.as_ref()?;
289 Some(Box::new(
290 variants
291 .iter()
292 .copied()
293 .map(::clap::builder::PossibleValue::new),
294 ))
295 }
296}
297
298#[derive(Debug, Clone)]
309pub struct MethodInfo {
310 pub name: String,
312 pub docs: Option<String>,
314 pub params: Vec<ParamInfo>,
316 pub return_type: String,
318 pub is_async: bool,
320 pub is_streaming: bool,
322 pub is_optional: bool,
324 pub is_result: bool,
326 pub group: Option<String>,
328}
329
330#[derive(Debug, Clone)]
335pub struct ParamInfo {
336 pub name: String,
338 pub ty: String,
340 pub is_optional: bool,
342 pub is_id: bool,
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub enum HttpMethod {
349 Get,
350 Post,
351 Put,
352 Patch,
353 Delete,
354}
355
356impl HttpMethod {
357 pub fn infer_from_name(name: &str) -> Self {
359 if name.starts_with("get_")
360 || name.starts_with("fetch_")
361 || name.starts_with("read_")
362 || name.starts_with("list_")
363 || name.starts_with("find_")
364 || name.starts_with("search_")
365 {
366 HttpMethod::Get
367 } else if name.starts_with("create_")
368 || name.starts_with("add_")
369 || name.starts_with("new_")
370 {
371 HttpMethod::Post
372 } else if name.starts_with("update_") || name.starts_with("set_") {
373 HttpMethod::Put
374 } else if name.starts_with("patch_") || name.starts_with("modify_") {
375 HttpMethod::Patch
376 } else if name.starts_with("delete_") || name.starts_with("remove_") {
377 HttpMethod::Delete
378 } else {
379 HttpMethod::Post
381 }
382 }
383
384 pub fn as_str(&self) -> &'static str {
385 match self {
386 HttpMethod::Get => "GET",
387 HttpMethod::Post => "POST",
388 HttpMethod::Put => "PUT",
389 HttpMethod::Patch => "PATCH",
390 HttpMethod::Delete => "DELETE",
391 }
392 }
393}
394
395pub fn infer_path(method_name: &str, http_method: HttpMethod) -> String {
397 let resource = method_name
399 .strip_prefix("get_")
400 .or_else(|| method_name.strip_prefix("fetch_"))
401 .or_else(|| method_name.strip_prefix("read_"))
402 .or_else(|| method_name.strip_prefix("list_"))
403 .or_else(|| method_name.strip_prefix("find_"))
404 .or_else(|| method_name.strip_prefix("search_"))
405 .or_else(|| method_name.strip_prefix("create_"))
406 .or_else(|| method_name.strip_prefix("add_"))
407 .or_else(|| method_name.strip_prefix("new_"))
408 .or_else(|| method_name.strip_prefix("update_"))
409 .or_else(|| method_name.strip_prefix("set_"))
410 .or_else(|| method_name.strip_prefix("patch_"))
411 .or_else(|| method_name.strip_prefix("modify_"))
412 .or_else(|| method_name.strip_prefix("delete_"))
413 .or_else(|| method_name.strip_prefix("remove_"))
414 .unwrap_or(method_name);
415
416 let path_resource = if resource.ends_with('s') {
418 resource.to_string()
419 } else {
420 format!("{resource}s")
421 };
422
423 match http_method {
424 HttpMethod::Post => format!("/{path_resource}"),
426 HttpMethod::Get
427 if method_name.starts_with("list_")
428 || method_name.starts_with("search_")
429 || method_name.starts_with("find_") =>
430 {
431 format!("/{path_resource}")
432 }
433 HttpMethod::Get | HttpMethod::Put | HttpMethod::Patch | HttpMethod::Delete => {
435 format!("/{path_resource}/{{id}}")
436 }
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_http_method_inference() {
446 assert_eq!(HttpMethod::infer_from_name("get_user"), HttpMethod::Get);
447 assert_eq!(HttpMethod::infer_from_name("list_users"), HttpMethod::Get);
448 assert_eq!(HttpMethod::infer_from_name("create_user"), HttpMethod::Post);
449 assert_eq!(HttpMethod::infer_from_name("update_user"), HttpMethod::Put);
450 assert_eq!(
451 HttpMethod::infer_from_name("delete_user"),
452 HttpMethod::Delete
453 );
454 assert_eq!(
455 HttpMethod::infer_from_name("do_something"),
456 HttpMethod::Post
457 ); }
459
460 #[test]
461 fn test_path_inference() {
462 assert_eq!(infer_path("create_user", HttpMethod::Post), "/users");
463 assert_eq!(infer_path("get_user", HttpMethod::Get), "/users/{id}");
464 assert_eq!(infer_path("list_users", HttpMethod::Get), "/users");
465 assert_eq!(infer_path("delete_user", HttpMethod::Delete), "/users/{id}");
466 }
467}