1pub mod error;
6pub mod extract;
7#[cfg(feature = "config")]
8pub mod config;
9
10pub use error::{
11 ErrorCode, ErrorResponse, HttpStatusFallback, HttpStatusHelper, IntoErrorCode,
12 SchemaValidationError,
13};
14pub use extract::Context;
15
16#[cfg(feature = "ws")]
17pub use extract::WsSender;
18
19#[cfg(feature = "cli")]
24pub trait CliSubcommand {
25 fn cli_command() -> ::clap::Command;
27
28 fn cli_dispatch(&self, matches: &::clap::ArgMatches) -> Result<(), Box<dyn std::error::Error>>;
30
31 fn cli_dispatch_async<'a>(
36 &'a self,
37 matches: &'a ::clap::ArgMatches,
38 ) -> impl std::future::Future<Output = Result<(), Box<dyn std::error::Error>>> + 'a {
39 async move { self.cli_dispatch(matches) }
40 }
41}
42
43#[cfg(feature = "mcp")]
48pub trait McpNamespace {
49 fn mcp_namespace_tools() -> Vec<serde_json::Value>;
51
52 fn mcp_namespace_tool_names() -> Vec<String>;
54
55 fn mcp_namespace_call(
57 &self,
58 name: &str,
59 args: serde_json::Value,
60 ) -> Result<serde_json::Value, String>;
61
62 fn mcp_namespace_call_async(
64 &self,
65 name: &str,
66 args: serde_json::Value,
67 ) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
68}
69
70#[cfg(feature = "jsonrpc")]
75pub trait JsonRpcMount {
76 fn jsonrpc_mount_methods() -> Vec<String>;
78
79 fn jsonrpc_mount_dispatch(
81 &self,
82 method: &str,
83 params: serde_json::Value,
84 ) -> Result<serde_json::Value, String>;
85
86 fn jsonrpc_mount_dispatch_async(
88 &self,
89 method: &str,
90 params: serde_json::Value,
91 ) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
92}
93
94#[cfg(feature = "ws")]
99pub trait WsMount {
100 fn ws_mount_methods() -> Vec<String>;
102
103 fn ws_mount_dispatch(
105 &self,
106 method: &str,
107 params: serde_json::Value,
108 ) -> Result<serde_json::Value, String>;
109
110 fn ws_mount_dispatch_async(
112 &self,
113 method: &str,
114 params: serde_json::Value,
115 ) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
116}
117
118#[cfg(feature = "http")]
123pub trait HttpMount: Send + Sync + 'static {
124 fn http_mount_router(self: ::std::sync::Arc<Self>) -> ::axum::Router;
126
127 fn http_mount_openapi_paths() -> Vec<server_less_openapi::OpenApiPath>
131 where
132 Self: Sized;
133}
134
135#[cfg(feature = "cli")]
150pub fn cli_format_output(
151 value: serde_json::Value,
152 jsonl: bool,
153 json: bool,
154 jq: Option<&str>,
155) -> Result<String, Box<dyn std::error::Error>> {
156 if let Some(filter) = jq {
157 use jaq_core::load::{Arena, File as JaqFile, Loader};
158 use jaq_core::{Compiler, Ctx, Vars, data, unwrap_valr};
159 use jaq_json::Val;
160
161 let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs()));
162 let arena = Arena::default();
163
164 let program = JaqFile {
165 code: filter,
166 path: (),
167 };
168
169 let modules = loader
170 .load(&arena, program)
171 .map_err(|errs| format!("jq parse error: {:?}", errs))?;
172
173 let filter_compiled = Compiler::default()
174 .with_funs(jaq_std::funs().chain(jaq_json::funs()))
175 .compile(modules)
176 .map_err(|errs| format!("jq compile error: {:?}", errs))?;
177
178 let val: Val = serde_json::from_value(value)?;
179 let ctx = Ctx::<data::JustLut<Val>>::new(&filter_compiled.lut, Vars::new([]));
180 let out = filter_compiled.id.run((ctx, val)).map(unwrap_valr);
181
182 let mut results = Vec::new();
183 for result in out {
184 match result {
185 Ok(v) => results.push(v.to_string()),
186 Err(e) => return Err(format!("jq runtime error: {:?}", e).into()),
187 }
188 }
189
190 Ok(results.join("\n"))
191 } else if jsonl {
192 match value {
193 serde_json::Value::Array(items) => {
194 let lines: Vec<String> = items
195 .iter()
196 .map(serde_json::to_string)
197 .collect::<Result<_, _>>()?;
198 Ok(lines.join("\n"))
199 }
200 other => Ok(serde_json::to_string(&other)?),
201 }
202 } else if json {
203 Ok(serde_json::to_string(&value)?)
204 } else {
205 Ok(serde_json::to_string_pretty(&value)?)
206 }
207}
208
209#[cfg(feature = "jsonschema")]
215pub fn cli_schema_for<T: schemars::JsonSchema>() -> serde_json::Value {
216 serde_json::to_value(schemars::schema_for!(T))
217 .unwrap_or_else(|_| serde_json::json!({"type": "object"}))
218}
219
220#[cfg(all(feature = "cli", feature = "jsonschema"))]
231#[derive(Clone)]
232pub struct SchemaValueParser<T: Clone + Send + Sync + 'static> {
233 variants: Option<std::sync::Arc<[&'static str]>>,
238 _marker: std::marker::PhantomData<T>,
239}
240
241#[cfg(all(feature = "cli", feature = "jsonschema"))]
242impl<T> Default for SchemaValueParser<T>
243where
244 T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
245{
246 fn default() -> Self {
247 Self::new()
248 }
249}
250
251#[cfg(all(feature = "cli", feature = "jsonschema"))]
252impl<T> SchemaValueParser<T>
253where
254 T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
255{
256 pub fn new() -> Self {
257 let variants = extract_enum_variants::<T>().map(|strings| {
258 let leaked: Vec<&'static str> = strings
259 .into_iter()
260 .map(|s| Box::leak(s.into_boxed_str()) as &'static str)
261 .collect();
262 leaked.into()
263 });
264 Self {
265 variants,
266 _marker: std::marker::PhantomData,
267 }
268 }
269}
270
271#[cfg(all(feature = "cli", feature = "jsonschema"))]
272fn extract_enum_variants<T: schemars::JsonSchema>() -> Option<Vec<String>> {
273 let schema_value = serde_json::to_value(schemars::schema_for!(T)).ok()?;
274 let enum_values = schema_value.get("enum")?.as_array()?;
275 let variants: Vec<String> = enum_values
276 .iter()
277 .filter_map(|v| v.as_str().map(String::from))
278 .collect();
279 if variants.is_empty() {
280 None
281 } else {
282 Some(variants)
283 }
284}
285
286#[cfg(all(feature = "cli", feature = "jsonschema"))]
287impl<T> ::clap::builder::TypedValueParser for SchemaValueParser<T>
288where
289 T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
290{
291 type Value = T;
292
293 fn parse_ref(
294 &self,
295 _cmd: &::clap::Command,
296 _arg: Option<&::clap::Arg>,
297 value: &std::ffi::OsStr,
298 ) -> Result<T, ::clap::Error> {
299 let s = value
300 .to_str()
301 .ok_or_else(|| ::clap::Error::new(::clap::error::ErrorKind::InvalidUtf8))?;
302 s.parse::<T>()
303 .map_err(|_| ::clap::Error::new(::clap::error::ErrorKind::InvalidValue))
304 }
305
306 fn possible_values(
307 &self,
308 ) -> Option<Box<dyn Iterator<Item = ::clap::builder::PossibleValue> + '_>> {
309 let variants = self.variants.as_ref()?;
310 Some(Box::new(
311 variants
312 .iter()
313 .copied()
314 .map(::clap::builder::PossibleValue::new),
315 ))
316 }
317}
318
319#[derive(Debug, Clone)]
330pub struct MethodInfo {
331 pub name: String,
333 pub docs: Option<String>,
335 pub params: Vec<ParamInfo>,
337 pub return_type: String,
339 pub is_async: bool,
341 pub is_streaming: bool,
343 pub is_optional: bool,
345 pub is_result: bool,
347 pub group: Option<String>,
349}
350
351#[derive(Debug, Clone)]
356pub struct ParamInfo {
357 pub name: String,
359 pub ty: String,
361 pub is_optional: bool,
363 pub is_id: bool,
365}
366
367#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub enum HttpMethod {
370 Get,
371 Post,
372 Put,
373 Patch,
374 Delete,
375}
376
377impl HttpMethod {
378 pub fn infer_from_name(name: &str) -> Self {
380 if name.starts_with("get_")
381 || name.starts_with("fetch_")
382 || name.starts_with("read_")
383 || name.starts_with("list_")
384 || name.starts_with("find_")
385 || name.starts_with("search_")
386 {
387 HttpMethod::Get
388 } else if name.starts_with("create_")
389 || name.starts_with("add_")
390 || name.starts_with("new_")
391 {
392 HttpMethod::Post
393 } else if name.starts_with("update_") || name.starts_with("set_") {
394 HttpMethod::Put
395 } else if name.starts_with("patch_") || name.starts_with("modify_") {
396 HttpMethod::Patch
397 } else if name.starts_with("delete_") || name.starts_with("remove_") {
398 HttpMethod::Delete
399 } else {
400 HttpMethod::Post
402 }
403 }
404
405 pub fn as_str(&self) -> &'static str {
406 match self {
407 HttpMethod::Get => "GET",
408 HttpMethod::Post => "POST",
409 HttpMethod::Put => "PUT",
410 HttpMethod::Patch => "PATCH",
411 HttpMethod::Delete => "DELETE",
412 }
413 }
414}
415
416fn pluralize(word: &str) -> String {
423 if word.ends_with('s')
424 || word.ends_with('x')
425 || word.ends_with('z')
426 || word.ends_with("ch")
427 || word.ends_with("sh")
428 {
429 format!("{word}es")
430 } else if word.ends_with('y')
431 && word.len() >= 2
432 && !matches!(
433 word.as_bytes()[word.len() - 2],
434 b'a' | b'e' | b'i' | b'o' | b'u'
435 )
436 {
437 format!("{}ies", &word[..word.len() - 1])
439 } else {
440 format!("{word}s")
441 }
442}
443
444pub fn infer_path(method_name: &str, http_method: HttpMethod) -> String {
446 let resource = method_name
448 .strip_prefix("get_")
449 .or_else(|| method_name.strip_prefix("fetch_"))
450 .or_else(|| method_name.strip_prefix("read_"))
451 .or_else(|| method_name.strip_prefix("list_"))
452 .or_else(|| method_name.strip_prefix("find_"))
453 .or_else(|| method_name.strip_prefix("search_"))
454 .or_else(|| method_name.strip_prefix("create_"))
455 .or_else(|| method_name.strip_prefix("add_"))
456 .or_else(|| method_name.strip_prefix("new_"))
457 .or_else(|| method_name.strip_prefix("update_"))
458 .or_else(|| method_name.strip_prefix("set_"))
459 .or_else(|| method_name.strip_prefix("patch_"))
460 .or_else(|| method_name.strip_prefix("modify_"))
461 .or_else(|| method_name.strip_prefix("delete_"))
462 .or_else(|| method_name.strip_prefix("remove_"))
463 .unwrap_or(method_name);
464
465 let path_resource = if resource.ends_with('s') {
470 resource.to_string()
471 } else {
472 pluralize(resource)
473 };
474
475 match http_method {
476 HttpMethod::Post => format!("/{path_resource}"),
478 HttpMethod::Get
479 if method_name.starts_with("list_")
480 || method_name.starts_with("search_")
481 || method_name.starts_with("find_") =>
482 {
483 format!("/{path_resource}")
484 }
485 HttpMethod::Get | HttpMethod::Put | HttpMethod::Patch | HttpMethod::Delete => {
487 format!("/{path_resource}/{{id}}")
488 }
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495
496 #[test]
497 fn test_pluralize() {
498 assert_eq!(pluralize("index"), "indexes");
500 assert_eq!(pluralize("status"), "statuses");
501 assert_eq!(pluralize("match"), "matches");
502 assert_eq!(pluralize("box"), "boxes");
503 assert_eq!(pluralize("buzz"), "buzzes");
504 assert_eq!(pluralize("brush"), "brushes");
505 assert_eq!(pluralize("query"), "queries");
507 assert_eq!(pluralize("key"), "keys");
509 assert_eq!(pluralize("item"), "items");
511 }
512
513 #[test]
514 fn test_http_method_inference() {
515 assert_eq!(HttpMethod::infer_from_name("get_user"), HttpMethod::Get);
516 assert_eq!(HttpMethod::infer_from_name("list_users"), HttpMethod::Get);
517 assert_eq!(HttpMethod::infer_from_name("create_user"), HttpMethod::Post);
518 assert_eq!(HttpMethod::infer_from_name("update_user"), HttpMethod::Put);
519 assert_eq!(
520 HttpMethod::infer_from_name("delete_user"),
521 HttpMethod::Delete
522 );
523 assert_eq!(
524 HttpMethod::infer_from_name("do_something"),
525 HttpMethod::Post
526 ); }
528
529 #[test]
530 fn test_path_inference() {
531 assert_eq!(infer_path("create_user", HttpMethod::Post), "/users");
532 assert_eq!(infer_path("get_user", HttpMethod::Get), "/users/{id}");
533 assert_eq!(infer_path("list_users", HttpMethod::Get), "/users");
534 assert_eq!(infer_path("delete_user", HttpMethod::Delete), "/users/{id}");
535 }
536}