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