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/// Method metadata extracted from an impl block.
15/// Used internally by macros but exposed for advanced use cases.
16#[derive(Debug, Clone)]
17pub struct MethodInfo {
18    /// Method name (e.g., "create_user")
19    pub name: String,
20    /// Documentation string from /// comments
21    pub docs: Option<String>,
22    /// Parameter names and their type strings
23    pub params: Vec<ParamInfo>,
24    /// Return type string
25    pub return_type: String,
26    /// Whether the method is async
27    pub is_async: bool,
28    /// Whether the return type is a Stream
29    pub is_streaming: bool,
30    /// Whether the return type is `Option<T>`
31    pub is_optional: bool,
32    /// Whether the return type is `Result<T, E>`
33    pub is_result: bool,
34}
35
36/// Parameter metadata
37#[derive(Debug, Clone)]
38pub struct ParamInfo {
39    /// Parameter name
40    pub name: String,
41    /// Type as string
42    pub ty: String,
43    /// Whether this is an `Option<T>`
44    pub is_optional: bool,
45    /// Whether this looks like an ID parameter (ends with _id or is named id)
46    pub is_id: bool,
47}
48
49/// HTTP method inferred from function name
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum HttpMethod {
52    Get,
53    Post,
54    Put,
55    Patch,
56    Delete,
57}
58
59impl HttpMethod {
60    /// Infer HTTP method from function name prefix
61    pub fn infer_from_name(name: &str) -> Self {
62        if name.starts_with("get_")
63            || name.starts_with("fetch_")
64            || name.starts_with("read_")
65            || name.starts_with("list_")
66            || name.starts_with("find_")
67            || name.starts_with("search_")
68        {
69            HttpMethod::Get
70        } else if name.starts_with("create_")
71            || name.starts_with("add_")
72            || name.starts_with("new_")
73        {
74            HttpMethod::Post
75        } else if name.starts_with("update_") || name.starts_with("set_") {
76            HttpMethod::Put
77        } else if name.starts_with("patch_") || name.starts_with("modify_") {
78            HttpMethod::Patch
79        } else if name.starts_with("delete_") || name.starts_with("remove_") {
80            HttpMethod::Delete
81        } else {
82            // Default to POST for RPC-style methods
83            HttpMethod::Post
84        }
85    }
86
87    pub fn as_str(&self) -> &'static str {
88        match self {
89            HttpMethod::Get => "GET",
90            HttpMethod::Post => "POST",
91            HttpMethod::Put => "PUT",
92            HttpMethod::Patch => "PATCH",
93            HttpMethod::Delete => "DELETE",
94        }
95    }
96}
97
98/// Infer URL path from method name
99pub fn infer_path(method_name: &str, http_method: HttpMethod) -> String {
100    // Strip common prefixes to get the resource name
101    let resource = method_name
102        .strip_prefix("get_")
103        .or_else(|| method_name.strip_prefix("fetch_"))
104        .or_else(|| method_name.strip_prefix("read_"))
105        .or_else(|| method_name.strip_prefix("list_"))
106        .or_else(|| method_name.strip_prefix("find_"))
107        .or_else(|| method_name.strip_prefix("search_"))
108        .or_else(|| method_name.strip_prefix("create_"))
109        .or_else(|| method_name.strip_prefix("add_"))
110        .or_else(|| method_name.strip_prefix("new_"))
111        .or_else(|| method_name.strip_prefix("update_"))
112        .or_else(|| method_name.strip_prefix("set_"))
113        .or_else(|| method_name.strip_prefix("patch_"))
114        .or_else(|| method_name.strip_prefix("modify_"))
115        .or_else(|| method_name.strip_prefix("delete_"))
116        .or_else(|| method_name.strip_prefix("remove_"))
117        .unwrap_or(method_name);
118
119    // Pluralize for collection endpoints
120    let path_resource = if resource.ends_with('s') {
121        resource.to_string()
122    } else {
123        format!("{resource}s")
124    };
125
126    match http_method {
127        // Collection operations
128        HttpMethod::Post => format!("/{path_resource}"),
129        HttpMethod::Get
130            if method_name.starts_with("list_")
131                || method_name.starts_with("search_")
132                || method_name.starts_with("find_") =>
133        {
134            format!("/{path_resource}")
135        }
136        // Single resource operations
137        HttpMethod::Get | HttpMethod::Put | HttpMethod::Patch | HttpMethod::Delete => {
138            format!("/{path_resource}/{{id}}")
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_http_method_inference() {
149        assert_eq!(HttpMethod::infer_from_name("get_user"), HttpMethod::Get);
150        assert_eq!(HttpMethod::infer_from_name("list_users"), HttpMethod::Get);
151        assert_eq!(HttpMethod::infer_from_name("create_user"), HttpMethod::Post);
152        assert_eq!(HttpMethod::infer_from_name("update_user"), HttpMethod::Put);
153        assert_eq!(
154            HttpMethod::infer_from_name("delete_user"),
155            HttpMethod::Delete
156        );
157        assert_eq!(
158            HttpMethod::infer_from_name("do_something"),
159            HttpMethod::Post
160        ); // RPC fallback
161    }
162
163    #[test]
164    fn test_path_inference() {
165        assert_eq!(infer_path("create_user", HttpMethod::Post), "/users");
166        assert_eq!(infer_path("get_user", HttpMethod::Get), "/users/{id}");
167        assert_eq!(infer_path("list_users", HttpMethod::Get), "/users");
168        assert_eq!(infer_path("delete_user", HttpMethod::Delete), "/users/{id}");
169    }
170}