Skip to main content

server_less_core/
lib.rs

1//! Core traits and types for server-less.
2//!
3//! This crate provides the foundational types that server-less macros generate code against.
4
5pub 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/// Trait for types that can be mounted as CLI subcommand groups.
15///
16/// Implemented automatically by `#[cli]` on an impl block. Allows nested
17/// composition: a parent CLI can mount a child's commands as a subcommand group.
18#[cfg(feature = "cli")]
19pub trait CliSubcommand {
20    /// Build the clap Command tree for this type's subcommands.
21    fn cli_command() -> ::clap::Command;
22
23    /// Dispatch a matched subcommand to the appropriate method.
24    fn cli_dispatch(&self, matches: &::clap::ArgMatches) -> Result<(), Box<dyn std::error::Error>>;
25}
26
27/// Trait for types that can be mounted as MCP tool namespaces.
28///
29/// Implemented automatically by `#[mcp]` on an impl block. Allows nested
30/// composition: a parent MCP server can mount a child's tools with a name prefix.
31#[cfg(feature = "mcp")]
32pub trait McpNamespace {
33    /// Get tool definitions for this namespace.
34    fn mcp_namespace_tools() -> Vec<serde_json::Value>;
35
36    /// Get tool names for this namespace (without prefix).
37    fn mcp_namespace_tool_names() -> Vec<String>;
38
39    /// Call a tool by name (sync). Returns error for async-only methods.
40    fn mcp_namespace_call(
41        &self,
42        name: &str,
43        args: serde_json::Value,
44    ) -> Result<serde_json::Value, String>;
45
46    /// Call a tool by name (async).
47    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/// Trait for types that can be mounted as JSON-RPC method namespaces.
55///
56/// Implemented automatically by `#[jsonrpc]` on an impl block. Allows nested
57/// composition: a parent JSON-RPC server can mount a child's methods with a dot-separated prefix.
58#[cfg(feature = "jsonrpc")]
59pub trait JsonRpcMount {
60    /// Get method names for this mount (without prefix).
61    fn jsonrpc_mount_methods() -> Vec<String>;
62
63    /// Dispatch a method call (async).
64    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/// Trait for types that can be mounted as WebSocket method namespaces.
72///
73/// Implemented automatically by `#[ws]` on an impl block. Allows nested
74/// composition: a parent WebSocket server can mount a child's methods with a dot-separated prefix.
75#[cfg(feature = "ws")]
76pub trait WsMount {
77    /// Get method names for this mount (without prefix).
78    fn ws_mount_methods() -> Vec<String>;
79
80    /// Dispatch a method call (sync). Returns error for async-only methods.
81    fn ws_mount_dispatch(
82        &self,
83        method: &str,
84        params: serde_json::Value,
85    ) -> Result<serde_json::Value, String>;
86
87    /// Dispatch a method call (async).
88    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/// Trait for types that can be mounted as HTTP route groups.
96///
97/// Implemented automatically by `#[http]` on an impl block. Allows nested
98/// composition: a parent HTTP server can mount a child's routes under a path prefix.
99#[cfg(feature = "http")]
100pub trait HttpMount: Send + Sync + 'static {
101    /// Build an axum Router for this mount's routes.
102    fn http_mount_router(self: ::std::sync::Arc<Self>) -> ::axum::Router;
103
104    /// Get OpenAPI path definitions for this mount.
105    fn http_mount_openapi_paths() -> Vec<crate::HttpMountPathInfo>
106    where
107        Self: Sized;
108}
109
110/// Simplified path info for HttpMount composition.
111#[cfg(feature = "http")]
112#[derive(Debug, Clone)]
113pub struct HttpMountPathInfo {
114    /// The path (relative to the mount point).
115    pub path: String,
116    /// The HTTP method (get, post, etc.).
117    pub method: String,
118    /// Summary text.
119    pub summary: Option<String>,
120}
121
122/// Format CLI output according to output flags.
123///
124/// - `jsonl`: one JSON object per line for array values
125/// - `json`: machine-readable JSON (no whitespace)
126/// - `jq`: filter using jaq (jq implemented in Rust, no external binary needed)
127/// - Default: pretty-printed JSON
128#[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, RcIter};
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::from(value);
158        let inputs = RcIter::new(core::iter::empty());
159        let out = filter_compiled.run((Ctx::new([], &inputs), val));
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/// Generate a JSON Schema for a type at runtime using schemars.
189///
190/// Called by `--output-schema` in `#[cli]`-generated code when the `jsonschema`
191/// feature is enabled. Users must `#[derive(schemars::JsonSchema)]` on their
192/// return types to use this.
193#[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/// Runtime method metadata with string-based types.
200///
201/// This is a simplified, serialization-friendly representation of method
202/// information intended for runtime introspection and tooling. Types are
203/// stored as strings rather than `syn` AST nodes.
204///
205/// **Not to be confused with [`server_less_parse::MethodInfo`]**, which is
206/// the richer, `syn`-based representation used internally by proc macros
207/// during code generation. The parse version retains full type information
208/// (`syn::Type`, `syn::Ident`) and supports `#[param(...)]` attributes.
209#[derive(Debug, Clone)]
210pub struct MethodInfo {
211    /// Method name (e.g., "create_user")
212    pub name: String,
213    /// Documentation string from /// comments
214    pub docs: Option<String>,
215    /// Parameter names and their type strings
216    pub params: Vec<ParamInfo>,
217    /// Return type string
218    pub return_type: String,
219    /// Whether the method is async
220    pub is_async: bool,
221    /// Whether the return type is a Stream
222    pub is_streaming: bool,
223    /// Whether the return type is `Option<T>`
224    pub is_optional: bool,
225    /// Whether the return type is `Result<T, E>`
226    pub is_result: bool,
227}
228
229/// Runtime parameter metadata with string-based types.
230///
231/// See [`MethodInfo`] for the relationship between this type and
232/// `server_less_parse::ParamInfo`.
233#[derive(Debug, Clone)]
234pub struct ParamInfo {
235    /// Parameter name
236    pub name: String,
237    /// Type as string
238    pub ty: String,
239    /// Whether this is an `Option<T>`
240    pub is_optional: bool,
241    /// Whether this looks like an ID parameter (ends with _id or is named id)
242    pub is_id: bool,
243}
244
245/// HTTP method inferred from function name
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub enum HttpMethod {
248    Get,
249    Post,
250    Put,
251    Patch,
252    Delete,
253}
254
255impl HttpMethod {
256    /// Infer HTTP method from function name prefix
257    pub fn infer_from_name(name: &str) -> Self {
258        if name.starts_with("get_")
259            || name.starts_with("fetch_")
260            || name.starts_with("read_")
261            || name.starts_with("list_")
262            || name.starts_with("find_")
263            || name.starts_with("search_")
264        {
265            HttpMethod::Get
266        } else if name.starts_with("create_")
267            || name.starts_with("add_")
268            || name.starts_with("new_")
269        {
270            HttpMethod::Post
271        } else if name.starts_with("update_") || name.starts_with("set_") {
272            HttpMethod::Put
273        } else if name.starts_with("patch_") || name.starts_with("modify_") {
274            HttpMethod::Patch
275        } else if name.starts_with("delete_") || name.starts_with("remove_") {
276            HttpMethod::Delete
277        } else {
278            // Default to POST for RPC-style methods
279            HttpMethod::Post
280        }
281    }
282
283    pub fn as_str(&self) -> &'static str {
284        match self {
285            HttpMethod::Get => "GET",
286            HttpMethod::Post => "POST",
287            HttpMethod::Put => "PUT",
288            HttpMethod::Patch => "PATCH",
289            HttpMethod::Delete => "DELETE",
290        }
291    }
292}
293
294/// Infer URL path from method name
295pub fn infer_path(method_name: &str, http_method: HttpMethod) -> String {
296    // Strip common prefixes to get the resource name
297    let resource = method_name
298        .strip_prefix("get_")
299        .or_else(|| method_name.strip_prefix("fetch_"))
300        .or_else(|| method_name.strip_prefix("read_"))
301        .or_else(|| method_name.strip_prefix("list_"))
302        .or_else(|| method_name.strip_prefix("find_"))
303        .or_else(|| method_name.strip_prefix("search_"))
304        .or_else(|| method_name.strip_prefix("create_"))
305        .or_else(|| method_name.strip_prefix("add_"))
306        .or_else(|| method_name.strip_prefix("new_"))
307        .or_else(|| method_name.strip_prefix("update_"))
308        .or_else(|| method_name.strip_prefix("set_"))
309        .or_else(|| method_name.strip_prefix("patch_"))
310        .or_else(|| method_name.strip_prefix("modify_"))
311        .or_else(|| method_name.strip_prefix("delete_"))
312        .or_else(|| method_name.strip_prefix("remove_"))
313        .unwrap_or(method_name);
314
315    // Pluralize for collection endpoints
316    let path_resource = if resource.ends_with('s') {
317        resource.to_string()
318    } else {
319        format!("{resource}s")
320    };
321
322    match http_method {
323        // Collection operations
324        HttpMethod::Post => format!("/{path_resource}"),
325        HttpMethod::Get
326            if method_name.starts_with("list_")
327                || method_name.starts_with("search_")
328                || method_name.starts_with("find_") =>
329        {
330            format!("/{path_resource}")
331        }
332        // Single resource operations
333        HttpMethod::Get | HttpMethod::Put | HttpMethod::Patch | HttpMethod::Delete => {
334            format!("/{path_resource}/{{id}}")
335        }
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn test_http_method_inference() {
345        assert_eq!(HttpMethod::infer_from_name("get_user"), HttpMethod::Get);
346        assert_eq!(HttpMethod::infer_from_name("list_users"), HttpMethod::Get);
347        assert_eq!(HttpMethod::infer_from_name("create_user"), HttpMethod::Post);
348        assert_eq!(HttpMethod::infer_from_name("update_user"), HttpMethod::Put);
349        assert_eq!(
350            HttpMethod::infer_from_name("delete_user"),
351            HttpMethod::Delete
352        );
353        assert_eq!(
354            HttpMethod::infer_from_name("do_something"),
355            HttpMethod::Post
356        ); // RPC fallback
357    }
358
359    #[test]
360    fn test_path_inference() {
361        assert_eq!(infer_path("create_user", HttpMethod::Post), "/users");
362        assert_eq!(infer_path("get_user", HttpMethod::Get), "/users/{id}");
363        assert_eq!(infer_path("list_users", HttpMethod::Get), "/users");
364        assert_eq!(infer_path("delete_user", HttpMethod::Delete), "/users/{id}");
365    }
366}