1pub mod error;
6pub mod extract;
7#[cfg(feature = "config")]
8pub mod config;
9
10#[cfg(feature = "config")]
15#[doc(hidden)]
16pub use toml as __toml;
17
18pub use error::{
19 ErrorCode, ErrorResponse, HttpStatusFallback, HttpStatusHelper, IntoErrorCode,
20 SchemaValidationError,
21};
22pub use extract::Context;
23
24#[cfg(feature = "ws")]
25pub use extract::WsSender;
26
27#[cfg(feature = "cli")]
38#[derive(Clone, Debug, serde::Serialize)]
39pub struct CliManualNode {
40 pub path: String,
44 pub description: Option<String>,
46 pub input_schema: serde_json::Value,
48 pub output_schema: serde_json::Value,
50}
51
52#[cfg(feature = "cli")]
58pub fn cli_manual_to_json(nodes: &[CliManualNode]) -> serde_json::Value {
59 let mut map = serde_json::Map::new();
60 for node in nodes {
61 map.insert(
62 node.path.clone(),
63 serde_json::json!({
64 "description": node.description,
65 "input_schema": node.input_schema,
66 "output_schema": node.output_schema,
67 }),
68 );
69 }
70 serde_json::Value::Object(map)
71}
72
73#[cfg(feature = "cli")]
79pub fn cli_manual_to_text(nodes: &[CliManualNode]) -> String {
80 let mut out = String::new();
81 for (i, node) in nodes.iter().enumerate() {
82 if i > 0 {
83 out.push('\n');
84 }
85 let heading = if node.path.is_empty() {
86 "(default)"
87 } else {
88 node.path.as_str()
89 };
90 out.push_str(&format!("# {heading}\n"));
91 if let Some(desc) = &node.description
92 && !desc.is_empty()
93 {
94 out.push_str(&format!("\n{desc}\n"));
95 }
96 if let Some(props) = node
97 .input_schema
98 .get("properties")
99 .and_then(|p| p.as_object())
100 && !props.is_empty()
101 {
102 out.push_str("\nParameters:\n");
103 let required: std::collections::HashSet<&str> = node
104 .input_schema
105 .get("required")
106 .and_then(|r| r.as_array())
107 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
108 .unwrap_or_default();
109 for (name, schema) in props {
110 let ty = schema
111 .get("type")
112 .and_then(|t| t.as_str())
113 .unwrap_or("value");
114 let req = if required.contains(name.as_str()) {
115 " (required)"
116 } else {
117 ""
118 };
119 out.push_str(&format!(" {name}: {ty}{req}\n"));
120 }
121 }
122 }
123 out
124}
125
126#[cfg(feature = "cli")]
131pub trait CliSubcommand {
132 fn cli_command() -> ::clap::Command;
134
135 fn cli_manual_nodes(&self, _prefix: &str) -> Vec<CliManualNode> {
144 Vec::new()
145 }
146
147 fn cli_dispatch(&self, matches: &::clap::ArgMatches) -> Result<(), Box<dyn std::error::Error>>;
149
150 fn cli_dispatch_async<'a>(
155 &'a self,
156 matches: &'a ::clap::ArgMatches,
157 ) -> impl std::future::Future<Output = Result<(), Box<dyn std::error::Error>>> + 'a {
158 async move { self.cli_dispatch(matches) }
159 }
160}
161
162#[cfg(feature = "mcp")]
167pub trait McpNamespace {
168 fn mcp_namespace_tools() -> Vec<serde_json::Value>;
170
171 fn mcp_namespace_tool_names() -> Vec<String>;
173
174 fn mcp_namespace_call(
176 &self,
177 name: &str,
178 args: serde_json::Value,
179 ) -> Result<serde_json::Value, String>;
180
181 fn mcp_namespace_call_async(
183 &self,
184 name: &str,
185 args: serde_json::Value,
186 ) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
187}
188
189#[cfg(feature = "jsonrpc")]
194pub trait JsonRpcMount {
195 fn jsonrpc_mount_methods() -> Vec<String>;
197
198 fn jsonrpc_mount_dispatch(
200 &self,
201 method: &str,
202 params: serde_json::Value,
203 ) -> Result<serde_json::Value, String>;
204
205 fn jsonrpc_mount_dispatch_async(
207 &self,
208 method: &str,
209 params: serde_json::Value,
210 ) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
211}
212
213#[cfg(feature = "ws")]
218pub trait WsMount {
219 fn ws_mount_methods() -> Vec<String>;
221
222 fn ws_mount_dispatch(
224 &self,
225 method: &str,
226 params: serde_json::Value,
227 ) -> Result<serde_json::Value, String>;
228
229 fn ws_mount_dispatch_async(
231 &self,
232 method: &str,
233 params: serde_json::Value,
234 ) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
235}
236
237#[cfg(feature = "http")]
242pub trait HttpMount: Send + Sync + 'static {
243 fn http_mount_router(self: ::std::sync::Arc<Self>) -> ::axum::Router;
245
246 fn http_mount_openapi_paths() -> Vec<server_less_openapi::OpenApiPath>
250 where
251 Self: Sized;
252}
253
254#[cfg(feature = "cli")]
269pub fn cli_format_output(
270 value: serde_json::Value,
271 jsonl: bool,
272 json: bool,
273 jq: Option<&str>,
274) -> Result<String, Box<dyn std::error::Error>> {
275 if let Some(filter) = jq {
276 use jaq_core::load::{Arena, File as JaqFile, Loader};
277 use jaq_core::{Compiler, Ctx, Vars, data, unwrap_valr};
278 use jaq_json::Val;
279
280 let loader =
281 Loader::new(jaq_core::defs().chain(jaq_std::defs()).chain(jaq_json::defs()));
282 let arena = Arena::default();
283
284 let program = JaqFile {
285 code: filter,
286 path: (),
287 };
288
289 let modules = loader
290 .load(&arena, program)
291 .map_err(|errs| format!("jq parse error: {:?}", errs))?;
292
293 let filter_compiled = Compiler::default()
294 .with_funs(jaq_core::funs().chain(jaq_std::funs()).chain(jaq_json::funs()))
295 .compile(modules)
296 .map_err(|errs| format!("jq compile error: {:?}", errs))?;
297
298 let val: Val = serde_json::from_value(value)?;
299 let ctx = Ctx::<data::JustLut<Val>>::new(&filter_compiled.lut, Vars::new([]));
300 let out = filter_compiled.id.run((ctx, val)).map(unwrap_valr);
301
302 let mut results = Vec::new();
303 for result in out {
304 match result {
305 Ok(v) => results.push(v.to_string()),
306 Err(e) => return Err(format!("jq runtime error: {:?}", e).into()),
307 }
308 }
309
310 Ok(results.join("\n"))
311 } else if jsonl {
312 match value {
313 serde_json::Value::Array(items) => {
314 let lines: Vec<String> = items
315 .iter()
316 .map(serde_json::to_string)
317 .collect::<Result<_, _>>()?;
318 Ok(lines.join("\n"))
319 }
320 other => Ok(serde_json::to_string(&other)?),
321 }
322 } else if json {
323 Ok(serde_json::to_string(&value)?)
324 } else {
325 Ok(serde_json::to_string_pretty(&value)?)
326 }
327}
328
329#[cfg(feature = "jsonschema")]
335pub fn cli_schema_for<T: schemars::JsonSchema>() -> serde_json::Value {
336 serde_json::to_value(schemars::schema_for!(T))
337 .unwrap_or_else(|_| serde_json::json!({"type": "object"}))
338}
339
340#[cfg(all(feature = "cli", feature = "jsonschema"))]
351#[derive(Clone)]
352pub struct SchemaValueParser<T: Clone + Send + Sync + 'static> {
353 variants: Option<std::sync::Arc<[&'static str]>>,
358 _marker: std::marker::PhantomData<T>,
359}
360
361#[cfg(all(feature = "cli", feature = "jsonschema"))]
362impl<T> Default for SchemaValueParser<T>
363where
364 T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
365{
366 fn default() -> Self {
367 Self::new()
368 }
369}
370
371#[cfg(all(feature = "cli", feature = "jsonschema"))]
372impl<T> SchemaValueParser<T>
373where
374 T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
375{
376 pub fn new() -> Self {
378 let variants = extract_enum_variants::<T>().map(|strings| {
379 let leaked: Vec<&'static str> = strings
380 .into_iter()
381 .map(|s| Box::leak(s.into_boxed_str()) as &'static str)
382 .collect();
383 leaked.into()
384 });
385 Self {
386 variants,
387 _marker: std::marker::PhantomData,
388 }
389 }
390}
391
392#[cfg(all(feature = "cli", feature = "jsonschema"))]
393fn extract_enum_variants<T: schemars::JsonSchema>() -> Option<Vec<String>> {
394 let schema_value = serde_json::to_value(schemars::schema_for!(T)).ok()?;
395 let enum_values = schema_value.get("enum")?.as_array()?;
396 let variants: Vec<String> = enum_values
397 .iter()
398 .filter_map(|v| v.as_str().map(String::from))
399 .collect();
400 if variants.is_empty() {
401 None
402 } else {
403 Some(variants)
404 }
405}
406
407#[cfg(all(feature = "cli", feature = "jsonschema"))]
408impl<T> ::clap::builder::TypedValueParser for SchemaValueParser<T>
409where
410 T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
411 <T as std::str::FromStr>::Err: std::fmt::Display,
412{
413 type Value = T;
414
415 fn parse_ref(
416 &self,
417 _cmd: &::clap::Command,
418 _arg: Option<&::clap::Arg>,
419 value: &std::ffi::OsStr,
420 ) -> Result<T, ::clap::Error> {
421 let s = value
422 .to_str()
423 .ok_or_else(|| ::clap::Error::new(::clap::error::ErrorKind::InvalidUtf8))?;
424 s.parse::<T>()
425 .map_err(|e| ::clap::Error::raw(::clap::error::ErrorKind::ValueValidation, e))
426 }
427
428 fn possible_values(&self) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
429 self.variants.as_ref().map(|variants| {
430 let v: Vec<_> = variants
431 .iter()
432 .map(|s| clap::builder::PossibleValue::new(*s))
433 .collect();
434 Box::new(v.into_iter()) as Box<dyn Iterator<Item = clap::builder::PossibleValue>>
435 })
436 }
437}
438
439#[derive(Debug, Clone)]
450pub struct MethodInfo {
451 pub name: String,
453 pub docs: Option<String>,
455 pub params: Vec<ParamInfo>,
457 pub return_type: String,
459 pub is_async: bool,
461 pub is_streaming: bool,
463 pub is_optional: bool,
465 pub is_result: bool,
467 pub group: Option<String>,
469}
470
471#[derive(Debug, Clone)]
476pub struct ParamInfo {
477 pub name: String,
479 pub ty: String,
481 pub is_optional: bool,
483 pub is_id: bool,
485}
486
487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
492pub enum HttpMethod {
493 Get,
494 Post,
495 Put,
496 Patch,
497 Delete,
498}
499
500impl HttpMethod {
501 pub fn infer_from_name(name: &str) -> Self {
503 if name.starts_with("get_")
504 || name.starts_with("fetch_")
505 || name.starts_with("read_")
506 || name.starts_with("list_")
507 || name.starts_with("find_")
508 || name.starts_with("search_")
509 {
510 HttpMethod::Get
511 } else if name.starts_with("create_")
512 || name.starts_with("add_")
513 || name.starts_with("new_")
514 {
515 HttpMethod::Post
516 } else if name.starts_with("update_") || name.starts_with("set_") {
517 HttpMethod::Put
518 } else if name.starts_with("patch_") || name.starts_with("modify_") {
519 HttpMethod::Patch
520 } else if name.starts_with("delete_") || name.starts_with("remove_") {
521 HttpMethod::Delete
522 } else {
523 HttpMethod::Post
525 }
526 }
527
528 pub fn as_str(&self) -> &'static str {
530 match self {
531 HttpMethod::Get => "GET",
532 HttpMethod::Post => "POST",
533 HttpMethod::Put => "PUT",
534 HttpMethod::Patch => "PATCH",
535 HttpMethod::Delete => "DELETE",
536 }
537 }
538}
539
540fn pluralize(word: &str) -> String {
547 if word.ends_with('s')
548 || word.ends_with('x')
549 || word.ends_with('z')
550 || word.ends_with("ch")
551 || word.ends_with("sh")
552 {
553 format!("{word}es")
554 } else if word.ends_with('y')
555 && word.len() >= 2
556 && !matches!(
557 word.as_bytes()[word.len() - 2],
558 b'a' | b'e' | b'i' | b'o' | b'u'
559 )
560 {
561 format!("{}ies", &word[..word.len() - 1])
563 } else {
564 format!("{word}s")
565 }
566}
567
568pub fn infer_path(method_name: &str, http_method: HttpMethod) -> String {
575 let resource = method_name
577 .strip_prefix("get_")
578 .or_else(|| method_name.strip_prefix("fetch_"))
579 .or_else(|| method_name.strip_prefix("read_"))
580 .or_else(|| method_name.strip_prefix("list_"))
581 .or_else(|| method_name.strip_prefix("find_"))
582 .or_else(|| method_name.strip_prefix("search_"))
583 .or_else(|| method_name.strip_prefix("create_"))
584 .or_else(|| method_name.strip_prefix("add_"))
585 .or_else(|| method_name.strip_prefix("new_"))
586 .or_else(|| method_name.strip_prefix("update_"))
587 .or_else(|| method_name.strip_prefix("set_"))
588 .or_else(|| method_name.strip_prefix("patch_"))
589 .or_else(|| method_name.strip_prefix("modify_"))
590 .or_else(|| method_name.strip_prefix("delete_"))
591 .or_else(|| method_name.strip_prefix("remove_"))
592 .unwrap_or(method_name);
593
594 let path_resource = if resource.ends_with('s') {
599 resource.to_string()
600 } else {
601 pluralize(resource)
602 };
603
604 match http_method {
605 HttpMethod::Post => format!("/{path_resource}"),
607 HttpMethod::Get
608 if method_name.starts_with("list_")
609 || method_name.starts_with("search_")
610 || method_name.starts_with("find_") =>
611 {
612 format!("/{path_resource}")
613 }
614 HttpMethod::Get | HttpMethod::Put | HttpMethod::Patch | HttpMethod::Delete => {
616 format!("/{path_resource}/{{id}}")
617 }
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
626 fn test_pluralize() {
627 assert_eq!(pluralize("index"), "indexes");
629 assert_eq!(pluralize("status"), "statuses");
630 assert_eq!(pluralize("match"), "matches");
631 assert_eq!(pluralize("box"), "boxes");
632 assert_eq!(pluralize("buzz"), "buzzes");
633 assert_eq!(pluralize("brush"), "brushes");
634 assert_eq!(pluralize("query"), "queries");
636 assert_eq!(pluralize("key"), "keys");
638 assert_eq!(pluralize("item"), "items");
640 }
641
642 #[test]
643 fn test_http_method_inference() {
644 assert_eq!(HttpMethod::infer_from_name("get_user"), HttpMethod::Get);
645 assert_eq!(HttpMethod::infer_from_name("list_users"), HttpMethod::Get);
646 assert_eq!(HttpMethod::infer_from_name("create_user"), HttpMethod::Post);
647 assert_eq!(HttpMethod::infer_from_name("update_user"), HttpMethod::Put);
648 assert_eq!(
649 HttpMethod::infer_from_name("delete_user"),
650 HttpMethod::Delete
651 );
652 assert_eq!(
653 HttpMethod::infer_from_name("do_something"),
654 HttpMethod::Post
655 ); }
657
658 #[test]
659 fn test_path_inference() {
660 assert_eq!(infer_path("create_user", HttpMethod::Post), "/users");
661 assert_eq!(infer_path("get_user", HttpMethod::Get), "/users/{id}");
662 assert_eq!(infer_path("list_users", HttpMethod::Get), "/users");
663 assert_eq!(infer_path("delete_user", HttpMethod::Delete), "/users/{id}");
664 }
665}