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")]
32pub trait CliSubcommand {
33 fn cli_command() -> ::clap::Command;
35
36 fn cli_dispatch(&self, matches: &::clap::ArgMatches) -> Result<(), Box<dyn std::error::Error>>;
38
39 fn cli_dispatch_async<'a>(
44 &'a self,
45 matches: &'a ::clap::ArgMatches,
46 ) -> impl std::future::Future<Output = Result<(), Box<dyn std::error::Error>>> + 'a {
47 async move { self.cli_dispatch(matches) }
48 }
49}
50
51#[cfg(feature = "mcp")]
56pub trait McpNamespace {
57 fn mcp_namespace_tools() -> Vec<serde_json::Value>;
59
60 fn mcp_namespace_tool_names() -> Vec<String>;
62
63 fn mcp_namespace_call(
65 &self,
66 name: &str,
67 args: serde_json::Value,
68 ) -> Result<serde_json::Value, String>;
69
70 fn mcp_namespace_call_async(
72 &self,
73 name: &str,
74 args: serde_json::Value,
75 ) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
76}
77
78#[cfg(feature = "jsonrpc")]
83pub trait JsonRpcMount {
84 fn jsonrpc_mount_methods() -> Vec<String>;
86
87 fn jsonrpc_mount_dispatch(
89 &self,
90 method: &str,
91 params: serde_json::Value,
92 ) -> Result<serde_json::Value, String>;
93
94 fn jsonrpc_mount_dispatch_async(
96 &self,
97 method: &str,
98 params: serde_json::Value,
99 ) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
100}
101
102#[cfg(feature = "ws")]
107pub trait WsMount {
108 fn ws_mount_methods() -> Vec<String>;
110
111 fn ws_mount_dispatch(
113 &self,
114 method: &str,
115 params: serde_json::Value,
116 ) -> Result<serde_json::Value, String>;
117
118 fn ws_mount_dispatch_async(
120 &self,
121 method: &str,
122 params: serde_json::Value,
123 ) -> impl std::future::Future<Output = Result<serde_json::Value, String>> + Send;
124}
125
126#[cfg(feature = "http")]
131pub trait HttpMount: Send + Sync + 'static {
132 fn http_mount_router(self: ::std::sync::Arc<Self>) -> ::axum::Router;
134
135 fn http_mount_openapi_paths() -> Vec<server_less_openapi::OpenApiPath>
139 where
140 Self: Sized;
141}
142
143#[cfg(feature = "cli")]
158pub fn cli_format_output(
159 value: serde_json::Value,
160 jsonl: bool,
161 json: bool,
162 jq: Option<&str>,
163) -> Result<String, Box<dyn std::error::Error>> {
164 if let Some(filter) = jq {
165 use jaq_core::load::{Arena, File as JaqFile, Loader};
166 use jaq_core::{Compiler, Ctx, Vars, data, unwrap_valr};
167 use jaq_json::Val;
168
169 let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs()));
170 let arena = Arena::default();
171
172 let program = JaqFile {
173 code: filter,
174 path: (),
175 };
176
177 let modules = loader
178 .load(&arena, program)
179 .map_err(|errs| format!("jq parse error: {:?}", errs))?;
180
181 let filter_compiled = Compiler::default()
182 .with_funs(jaq_std::funs().chain(jaq_json::funs()))
183 .compile(modules)
184 .map_err(|errs| format!("jq compile error: {:?}", errs))?;
185
186 let val: Val = serde_json::from_value(value)?;
187 let ctx = Ctx::<data::JustLut<Val>>::new(&filter_compiled.lut, Vars::new([]));
188 let out = filter_compiled.id.run((ctx, val)).map(unwrap_valr);
189
190 let mut results = Vec::new();
191 for result in out {
192 match result {
193 Ok(v) => results.push(v.to_string()),
194 Err(e) => return Err(format!("jq runtime error: {:?}", e).into()),
195 }
196 }
197
198 Ok(results.join("\n"))
199 } else if jsonl {
200 match value {
201 serde_json::Value::Array(items) => {
202 let lines: Vec<String> = items
203 .iter()
204 .map(serde_json::to_string)
205 .collect::<Result<_, _>>()?;
206 Ok(lines.join("\n"))
207 }
208 other => Ok(serde_json::to_string(&other)?),
209 }
210 } else if json {
211 Ok(serde_json::to_string(&value)?)
212 } else {
213 Ok(serde_json::to_string_pretty(&value)?)
214 }
215}
216
217#[cfg(feature = "jsonschema")]
223pub fn cli_schema_for<T: schemars::JsonSchema>() -> serde_json::Value {
224 serde_json::to_value(schemars::schema_for!(T))
225 .unwrap_or_else(|_| serde_json::json!({"type": "object"}))
226}
227
228#[cfg(all(feature = "cli", feature = "jsonschema"))]
239#[derive(Clone)]
240pub struct SchemaValueParser<T: Clone + Send + Sync + 'static> {
241 variants: Option<std::sync::Arc<[&'static str]>>,
246 _marker: std::marker::PhantomData<T>,
247}
248
249#[cfg(all(feature = "cli", feature = "jsonschema"))]
250impl<T> Default for SchemaValueParser<T>
251where
252 T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
253{
254 fn default() -> Self {
255 Self::new()
256 }
257}
258
259#[cfg(all(feature = "cli", feature = "jsonschema"))]
260impl<T> SchemaValueParser<T>
261where
262 T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
263{
264 pub fn new() -> Self {
265 let variants = extract_enum_variants::<T>().map(|strings| {
266 let leaked: Vec<&'static str> = strings
267 .into_iter()
268 .map(|s| Box::leak(s.into_boxed_str()) as &'static str)
269 .collect();
270 leaked.into()
271 });
272 Self {
273 variants,
274 _marker: std::marker::PhantomData,
275 }
276 }
277}
278
279#[cfg(all(feature = "cli", feature = "jsonschema"))]
280fn extract_enum_variants<T: schemars::JsonSchema>() -> Option<Vec<String>> {
281 let schema_value = serde_json::to_value(schemars::schema_for!(T)).ok()?;
282 let enum_values = schema_value.get("enum")?.as_array()?;
283 let variants: Vec<String> = enum_values
284 .iter()
285 .filter_map(|v| v.as_str().map(String::from))
286 .collect();
287 if variants.is_empty() {
288 None
289 } else {
290 Some(variants)
291 }
292}
293
294#[cfg(all(feature = "cli", feature = "jsonschema"))]
295impl<T> ::clap::builder::TypedValueParser for SchemaValueParser<T>
296where
297 T: schemars::JsonSchema + std::str::FromStr + Clone + Send + Sync + 'static,
298 <T as std::str::FromStr>::Err: std::fmt::Display,
299{
300 type Value = T;
301
302 fn parse_ref(
303 &self,
304 _cmd: &::clap::Command,
305 _arg: Option<&::clap::Arg>,
306 value: &std::ffi::OsStr,
307 ) -> Result<T, ::clap::Error> {
308 let s = value
309 .to_str()
310 .ok_or_else(|| ::clap::Error::new(::clap::error::ErrorKind::InvalidUtf8))?;
311 s.parse::<T>()
312 .map_err(|e| ::clap::Error::raw(::clap::error::ErrorKind::ValueValidation, e))
313 }
314
315 fn possible_values(
316 &self,
317 ) -> Option<Box<dyn Iterator<Item = ::clap::builder::PossibleValue> + '_>> {
318 let variants = self.variants.as_ref()?;
319 Some(Box::new(
320 variants
321 .iter()
322 .copied()
323 .map(::clap::builder::PossibleValue::new),
324 ))
325 }
326}
327
328#[derive(Debug, Clone)]
339pub struct MethodInfo {
340 pub name: String,
342 pub docs: Option<String>,
344 pub params: Vec<ParamInfo>,
346 pub return_type: String,
348 pub is_async: bool,
350 pub is_streaming: bool,
352 pub is_optional: bool,
354 pub is_result: bool,
356 pub group: Option<String>,
358}
359
360#[derive(Debug, Clone)]
365pub struct ParamInfo {
366 pub name: String,
368 pub ty: String,
370 pub is_optional: bool,
372 pub is_id: bool,
374}
375
376#[derive(Debug, Clone, Copy, PartialEq, Eq)]
378pub enum HttpMethod {
379 Get,
380 Post,
381 Put,
382 Patch,
383 Delete,
384}
385
386impl HttpMethod {
387 pub fn infer_from_name(name: &str) -> Self {
389 if name.starts_with("get_")
390 || name.starts_with("fetch_")
391 || name.starts_with("read_")
392 || name.starts_with("list_")
393 || name.starts_with("find_")
394 || name.starts_with("search_")
395 {
396 HttpMethod::Get
397 } else if name.starts_with("create_")
398 || name.starts_with("add_")
399 || name.starts_with("new_")
400 {
401 HttpMethod::Post
402 } else if name.starts_with("update_") || name.starts_with("set_") {
403 HttpMethod::Put
404 } else if name.starts_with("patch_") || name.starts_with("modify_") {
405 HttpMethod::Patch
406 } else if name.starts_with("delete_") || name.starts_with("remove_") {
407 HttpMethod::Delete
408 } else {
409 HttpMethod::Post
411 }
412 }
413
414 pub fn as_str(&self) -> &'static str {
415 match self {
416 HttpMethod::Get => "GET",
417 HttpMethod::Post => "POST",
418 HttpMethod::Put => "PUT",
419 HttpMethod::Patch => "PATCH",
420 HttpMethod::Delete => "DELETE",
421 }
422 }
423}
424
425fn pluralize(word: &str) -> String {
432 if word.ends_with('s')
433 || word.ends_with('x')
434 || word.ends_with('z')
435 || word.ends_with("ch")
436 || word.ends_with("sh")
437 {
438 format!("{word}es")
439 } else if word.ends_with('y')
440 && word.len() >= 2
441 && !matches!(
442 word.as_bytes()[word.len() - 2],
443 b'a' | b'e' | b'i' | b'o' | b'u'
444 )
445 {
446 format!("{}ies", &word[..word.len() - 1])
448 } else {
449 format!("{word}s")
450 }
451}
452
453pub fn infer_path(method_name: &str, http_method: HttpMethod) -> String {
455 let resource = method_name
457 .strip_prefix("get_")
458 .or_else(|| method_name.strip_prefix("fetch_"))
459 .or_else(|| method_name.strip_prefix("read_"))
460 .or_else(|| method_name.strip_prefix("list_"))
461 .or_else(|| method_name.strip_prefix("find_"))
462 .or_else(|| method_name.strip_prefix("search_"))
463 .or_else(|| method_name.strip_prefix("create_"))
464 .or_else(|| method_name.strip_prefix("add_"))
465 .or_else(|| method_name.strip_prefix("new_"))
466 .or_else(|| method_name.strip_prefix("update_"))
467 .or_else(|| method_name.strip_prefix("set_"))
468 .or_else(|| method_name.strip_prefix("patch_"))
469 .or_else(|| method_name.strip_prefix("modify_"))
470 .or_else(|| method_name.strip_prefix("delete_"))
471 .or_else(|| method_name.strip_prefix("remove_"))
472 .unwrap_or(method_name);
473
474 let path_resource = if resource.ends_with('s') {
479 resource.to_string()
480 } else {
481 pluralize(resource)
482 };
483
484 match http_method {
485 HttpMethod::Post => format!("/{path_resource}"),
487 HttpMethod::Get
488 if method_name.starts_with("list_")
489 || method_name.starts_with("search_")
490 || method_name.starts_with("find_") =>
491 {
492 format!("/{path_resource}")
493 }
494 HttpMethod::Get | HttpMethod::Put | HttpMethod::Patch | HttpMethod::Delete => {
496 format!("/{path_resource}/{{id}}")
497 }
498 }
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
506 fn test_pluralize() {
507 assert_eq!(pluralize("index"), "indexes");
509 assert_eq!(pluralize("status"), "statuses");
510 assert_eq!(pluralize("match"), "matches");
511 assert_eq!(pluralize("box"), "boxes");
512 assert_eq!(pluralize("buzz"), "buzzes");
513 assert_eq!(pluralize("brush"), "brushes");
514 assert_eq!(pluralize("query"), "queries");
516 assert_eq!(pluralize("key"), "keys");
518 assert_eq!(pluralize("item"), "items");
520 }
521
522 #[test]
523 fn test_http_method_inference() {
524 assert_eq!(HttpMethod::infer_from_name("get_user"), HttpMethod::Get);
525 assert_eq!(HttpMethod::infer_from_name("list_users"), HttpMethod::Get);
526 assert_eq!(HttpMethod::infer_from_name("create_user"), HttpMethod::Post);
527 assert_eq!(HttpMethod::infer_from_name("update_user"), HttpMethod::Put);
528 assert_eq!(
529 HttpMethod::infer_from_name("delete_user"),
530 HttpMethod::Delete
531 );
532 assert_eq!(
533 HttpMethod::infer_from_name("do_something"),
534 HttpMethod::Post
535 ); }
537
538 #[test]
539 fn test_path_inference() {
540 assert_eq!(infer_path("create_user", HttpMethod::Post), "/users");
541 assert_eq!(infer_path("get_user", HttpMethod::Get), "/users/{id}");
542 assert_eq!(infer_path("list_users", HttpMethod::Get), "/users");
543 assert_eq!(infer_path("delete_user", HttpMethod::Delete), "/users/{id}");
544 }
545}