server_less_macros/lib.rs
1//! Proc macros for server-less.
2//!
3//! This crate provides attribute macros that transform impl blocks into protocol handlers,
4//! and derive macros for common patterns.
5
6use proc_macro::TokenStream;
7use proc_macro2::TokenStream as TokenStream2;
8#[cfg(feature = "graphql")]
9use syn::ItemEnum;
10#[cfg(feature = "graphql")]
11use syn::ItemStruct;
12use syn::{DeriveInput, ItemImpl, parse_macro_input};
13
14/// Compute the Levenshtein edit distance between two strings.
15fn levenshtein(a: &str, b: &str) -> usize {
16 let a: Vec<char> = a.chars().collect();
17 let b: Vec<char> = b.chars().collect();
18 let m = a.len();
19 let n = b.len();
20 let mut dp = vec![vec![0usize; n + 1]; m + 1];
21 for i in 0..=m {
22 dp[i][0] = i;
23 }
24 for j in 0..=n {
25 dp[0][j] = j;
26 }
27 for i in 1..=m {
28 for j in 1..=n {
29 dp[i][j] = if a[i - 1] == b[j - 1] {
30 dp[i - 1][j - 1]
31 } else {
32 1 + dp[i - 1][j - 1].min(dp[i - 1][j]).min(dp[i][j - 1])
33 };
34 }
35 }
36 dp[m][n]
37}
38
39/// Return the closest candidate to `input` within edit distance ≤ 2, or `None`.
40pub(crate) fn did_you_mean<'a>(input: &str, candidates: &[&'a str]) -> Option<&'a str> {
41 candidates
42 .iter()
43 .filter_map(|&c| {
44 let d = levenshtein(input, c);
45 if d <= 2 { Some((d, c)) } else { None }
46 })
47 .min_by_key(|&(d, _)| d)
48 .map(|(_, c)| c)
49}
50
51/// When `SERVER_LESS_DEBUG=1` is set at build time, print the generated token
52/// stream to stderr so implementors can inspect macro output without `cargo expand`.
53fn debug_emit(macro_name: &str, type_name: &str, tokens: &TokenStream2) {
54 if std::env::var("SERVER_LESS_DEBUG").as_deref() == Ok("1") {
55 eprintln!("--- server-less: #[{macro_name}] on {type_name} ---");
56 eprintln!("{tokens}");
57 eprintln!("--- end #[{macro_name}] on {type_name} ---");
58 }
59}
60
61fn type_name(ty: &syn::Type) -> String {
62 quote::quote!(#ty).to_string()
63}
64
65/// Strip the first `impl` block from a token stream.
66///
67/// Preset macros call multiple expand functions, each of which emits the
68/// original impl block followed by generated code. To avoid duplicate method
69/// definitions, the preset emits the impl block from the first expand call
70/// and strips it from subsequent calls.
71fn strip_first_impl(tokens: TokenStream2) -> TokenStream2 {
72 let file: syn::File = match syn::parse2(tokens.clone()) {
73 Ok(file) => file,
74 Err(err) => {
75 // Emit the original tokens (so the user's code is preserved) plus
76 // a compile_error! pointing at the parse failure. This surfaces the
77 // real problem instead of silently dropping generated impls.
78 let msg = format!("server-less: preset macro failed to parse generated tokens: {err}");
79 return quote::quote! {
80 #tokens
81 ::core::compile_error!(#msg);
82 };
83 }
84 };
85
86 let mut found_first = false;
87 let remaining: Vec<_> = file
88 .items
89 .into_iter()
90 .filter(|item| {
91 if !found_first && matches!(item, syn::Item::Impl(_)) {
92 found_first = true;
93 return false;
94 }
95 true
96 })
97 .collect();
98
99 quote::quote! { #(#remaining)* }
100}
101
102/// Priority-ordered list of protocol macro attribute names.
103///
104/// When multiple protocol macros are stacked on the same impl block, Rust expands
105/// each independently (outputs are concatenated, not pipelined). To avoid emitting
106/// the impl block multiple times, exactly ONE macro — the one with the highest
107/// priority that's present — takes responsibility for emitting it.
108const PROTOCOL_PRIORITY: &[&str] = &[
109 "cli", "http", "mcp", "jsonrpc", "ws", "graphql", "openapi", "openrpc",
110];
111
112/// Returns `true` if this protocol macro should emit the original impl block.
113///
114/// A macro emits the impl when no higher-priority protocol sibling is present on
115/// the same impl block. This ensures exactly one copy is emitted when macros are
116/// stacked, preventing duplicate method definitions.
117pub(crate) fn is_protocol_impl_emitter(impl_block: &ItemImpl, current: &str) -> bool {
118 let current_pos = PROTOCOL_PRIORITY
119 .iter()
120 .position(|&p| p == current)
121 .unwrap_or(usize::MAX);
122 // Emit if no sibling with LOWER index (higher priority) is present.
123 !impl_block.attrs.iter().any(|attr| {
124 PROTOCOL_PRIORITY[..current_pos]
125 .iter()
126 .any(|name| attr.path().is_ident(name))
127 })
128}
129
130
131#[cfg(feature = "asyncapi")]
132mod asyncapi;
133#[cfg(feature = "capnp")]
134mod capnp;
135#[cfg(feature = "cli")]
136mod cli;
137#[cfg(feature = "connect")]
138mod connect;
139mod context;
140mod error;
141#[cfg(feature = "graphql")]
142mod graphql;
143#[cfg(feature = "graphql")]
144mod graphql_enum;
145#[cfg(feature = "graphql")]
146mod graphql_input;
147#[cfg(feature = "grpc")]
148mod grpc;
149#[cfg(feature = "http")]
150mod http;
151#[cfg(feature = "jsonrpc")]
152mod jsonrpc;
153#[cfg(feature = "jsonschema")]
154mod jsonschema;
155#[cfg(feature = "markdown")]
156mod markdown;
157#[cfg(feature = "mcp")]
158mod mcp;
159#[cfg(any(feature = "http", feature = "openapi"))]
160mod openapi;
161#[cfg(any(feature = "http", feature = "openapi"))]
162mod openapi_gen;
163#[cfg(feature = "openrpc")]
164mod openrpc;
165#[cfg(feature = "smithy")]
166mod smithy;
167#[cfg(feature = "thrift")]
168mod thrift;
169#[cfg(feature = "ws")]
170mod ws;
171
172mod server_attrs;
173
174// Blessed preset modules
175#[cfg(feature = "cli")]
176mod program;
177#[cfg(feature = "jsonrpc")]
178mod rpc_preset;
179#[cfg(feature = "http")]
180mod server;
181#[cfg(feature = "mcp")]
182mod tool;
183
184/// Generate HTTP handlers from an impl block.
185///
186/// # Basic Usage
187///
188/// ```ignore
189/// use server_less::http;
190///
191/// #[http]
192/// impl UserService {
193/// async fn create_user(&self, name: String) -> User { /* ... */ }
194/// }
195/// ```
196///
197/// # With URL Prefix
198///
199/// ```ignore
200/// #[http(prefix = "/api/v1")]
201/// impl UserService {
202/// // POST /api/v1/users
203/// async fn create_user(&self, name: String) -> User { /* ... */ }
204/// }
205/// ```
206///
207/// # Per-Method Route Overrides
208///
209/// ```ignore
210/// #[http]
211/// impl UserService {
212/// // Override HTTP method: GET /data becomes POST /data
213/// #[route(method = "POST")]
214/// async fn get_data(&self, payload: String) -> String { /* ... */ }
215///
216/// // Override path: POST /users becomes POST /custom-endpoint
217/// #[route(path = "/custom-endpoint")]
218/// async fn create_user(&self, name: String) -> User { /* ... */ }
219///
220/// // Override both
221/// #[route(method = "PUT", path = "/special/{id}")]
222/// async fn do_something(&self, id: String) -> String { /* ... */ }
223///
224/// // Skip route generation (internal methods)
225/// #[route(skip)]
226/// fn internal_helper(&self) -> String { /* ... */ }
227///
228/// // Hide from OpenAPI but still generate route
229/// #[route(hidden)]
230/// fn secret_endpoint(&self) -> String { /* ... */ }
231/// }
232/// ```
233///
234/// # Parameter Handling
235///
236/// ```ignore
237/// #[http]
238/// impl BlogService {
239/// // Path parameters (id, post_id, etc. go in URL)
240/// async fn get_post(&self, post_id: u32) -> Post { /* ... */ }
241/// // GET /posts/{post_id}
242///
243/// // Query parameters (GET methods use query string)
244/// async fn search_posts(&self, query: String, tag: Option<String>) -> Vec<Post> {
245/// /* ... */
246/// }
247/// // GET /posts?query=rust&tag=tutorial
248///
249/// // Body parameters (POST/PUT/PATCH use JSON body)
250/// async fn create_post(&self, title: String, content: String) -> Post {
251/// /* ... */
252/// }
253/// // POST /posts with body: {"title": "...", "content": "..."}
254/// }
255/// ```
256///
257/// # Error Handling
258///
259/// ```ignore
260/// #[http]
261/// impl UserService {
262/// // Return Result for error handling
263/// async fn get_user(&self, id: u32) -> Result<User, MyError> {
264/// if id == 0 {
265/// return Err(MyError::InvalidId);
266/// }
267/// Ok(User { id, name: "Alice".into() })
268/// }
269///
270/// // Return Option - None becomes 404
271/// async fn find_user(&self, email: String) -> Option<User> {
272/// // Returns 200 with user or 404 if None
273/// None
274/// }
275/// }
276/// ```
277///
278/// # Server-Sent Events (SSE) Streaming
279///
280/// Return `impl Stream<Item = T>` to enable Server-Sent Events streaming.
281///
282/// **Important for Rust 2024:** You must add `+ use<>` to impl Trait return types
283/// to explicitly capture all generic parameters in scope. This is required by the
284/// Rust 2024 edition's stricter lifetime capture rules.
285///
286/// ```ignore
287/// use futures::stream::{self, Stream};
288///
289/// #[http]
290/// impl DataService {
291/// // Simple stream - emits values immediately
292/// // Note the `+ use<>` syntax for Rust 2024
293/// fn stream_numbers(&self, count: u32) -> impl Stream<Item = u32> + use<> {
294/// stream::iter(0..count)
295/// }
296///
297/// // Async stream with delays
298/// async fn stream_events(&self, n: u32) -> impl Stream<Item = Event> + use<> {
299/// stream::unfold(0, move |count| async move {
300/// if count >= n {
301/// return None;
302/// }
303/// tokio::time::sleep(Duration::from_secs(1)).await;
304/// Some((Event { id: count }, count + 1))
305/// })
306/// }
307/// }
308/// ```
309///
310/// Clients receive data as SSE:
311/// ```text
312/// data: {"id": 0}
313///
314/// data: {"id": 1}
315///
316/// data: {"id": 2}
317/// ```
318///
319/// **Why `+ use<>`?**
320/// - Rust 2024 requires explicit capture of generic parameters in return position impl Trait
321/// - `+ use<>` captures all type parameters and lifetimes from the function context
322/// - Without it, you'll get compilation errors about uncaptured parameters
323/// - See: examples/streaming_service.rs for a complete working example
324///
325/// # Real-World Example
326///
327/// ```ignore
328/// #[http(prefix = "/api/v1")]
329/// impl UserService {
330/// // GET /api/v1/users?page=0&limit=10
331/// async fn list_users(
332/// &self,
333/// #[param(default = 0)] page: u32,
334/// #[param(default = 20)] limit: u32,
335/// ) -> Vec<User> {
336/// /* ... */
337/// }
338///
339/// // GET /api/v1/users/{user_id}
340/// async fn get_user(&self, user_id: u32) -> Result<User, ApiError> {
341/// /* ... */
342/// }
343///
344/// // POST /api/v1/users with body: {"name": "...", "email": "..."}
345/// #[response(status = 201)]
346/// #[response(header = "Location", value = "/api/v1/users/{id}")]
347/// async fn create_user(&self, name: String, email: String) -> Result<User, ApiError> {
348/// /* ... */
349/// }
350///
351/// // PUT /api/v1/users/{user_id}
352/// async fn update_user(
353/// &self,
354/// user_id: u32,
355/// name: Option<String>,
356/// email: Option<String>,
357/// ) -> Result<User, ApiError> {
358/// /* ... */
359/// }
360///
361/// // DELETE /api/v1/users/{user_id}
362/// #[response(status = 204)]
363/// async fn delete_user(&self, user_id: u32) -> Result<(), ApiError> {
364/// /* ... */
365/// }
366/// }
367/// ```
368///
369/// # Generated Methods
370/// - `http_router() -> axum::Router` - Complete router with all endpoints
371/// - `http_routes() -> Vec<&'static str>` - List of route paths
372/// - `openapi_spec() -> serde_json::Value` - OpenAPI 3.0 specification (unless `openapi = false`)
373///
374/// # OpenAPI Control
375///
376/// By default, `#[http]` generates both HTTP routes and OpenAPI specs. You can disable
377/// OpenAPI generation:
378///
379/// ```ignore
380/// #[http(openapi = false)] // No openapi_spec() method generated
381/// impl MyService { /* ... */ }
382/// ```
383///
384/// For standalone OpenAPI generation without HTTP routing, see `#[openapi]`.
385#[cfg(feature = "http")]
386#[proc_macro_attribute]
387pub fn http(attr: TokenStream, item: TokenStream) -> TokenStream {
388 let args = parse_macro_input!(attr as http::HttpArgs);
389 let impl_block = parse_macro_input!(item as ItemImpl);
390 let name = type_name(&impl_block.self_ty);
391
392 match http::expand_http(args, impl_block) {
393 Ok(tokens) => {
394 debug_emit("http", &name, &tokens);
395 tokens.into()
396 }
397 Err(err) => err.to_compile_error().into(),
398 }
399}
400
401/// Generate OpenAPI specification without HTTP routing.
402///
403/// Generates OpenAPI 3.0 specs using the same naming conventions as `#[http]`,
404/// but without creating route handlers. Useful for:
405/// - Schema-first development
406/// - Documentation-only use cases
407/// - Separate OpenAPI generation from HTTP routing
408///
409/// # Basic Usage
410///
411/// ```ignore
412/// use server_less::openapi;
413///
414/// #[openapi]
415/// impl UserService {
416/// /// Create a new user
417/// fn create_user(&self, name: String, email: String) -> User { /* ... */ }
418///
419/// /// Get user by ID
420/// fn get_user(&self, id: String) -> Option<User> { /* ... */ }
421/// }
422///
423/// // Generate spec:
424/// let spec = UserService::openapi_spec();
425/// ```
426///
427/// # With URL Prefix
428///
429/// ```ignore
430/// #[openapi(prefix = "/api/v1")]
431/// impl UserService { /* ... */ }
432/// ```
433///
434/// # Generated Methods
435///
436/// - `openapi_spec() -> serde_json::Value` - OpenAPI 3.0 specification
437///
438/// # Combining with #[http]
439///
440/// If you want separate control over OpenAPI generation:
441///
442/// ```ignore
443/// // Option 1: Disable OpenAPI in http, use standalone macro
444/// #[http(openapi = false)]
445/// #[openapi(prefix = "/api")]
446/// impl MyService { /* ... */ }
447///
448/// // Option 2: Just use http with default (openapi = true)
449/// #[http]
450/// impl MyService { /* ... */ }
451/// ```
452#[cfg(any(feature = "http", feature = "openapi"))]
453#[proc_macro_attribute]
454pub fn openapi(attr: TokenStream, item: TokenStream) -> TokenStream {
455 let args = parse_macro_input!(attr as openapi::OpenApiArgs);
456 let impl_block = parse_macro_input!(item as ItemImpl);
457 let name = type_name(&impl_block.self_ty);
458
459 match openapi::expand_openapi(args, impl_block) {
460 Ok(tokens) => {
461 debug_emit("openapi", &name, &tokens);
462 tokens.into()
463 }
464 Err(err) => err.to_compile_error().into(),
465 }
466}
467
468/// Generate a CLI application from an impl block.
469///
470/// # Basic Usage
471///
472/// ```ignore
473/// use server_less::cli;
474///
475/// #[cli]
476/// impl MyApp {
477/// fn create_user(&self, name: String) { /* ... */ }
478/// }
479/// ```
480///
481/// # With All Options
482///
483/// ```ignore
484/// #[cli(
485/// name = "myapp",
486/// version = "1.0.0",
487/// about = "My awesome application"
488/// )]
489/// impl MyApp {
490/// /// Create a new user (becomes: myapp create-user <NAME>)
491/// fn create_user(&self, name: String) { /* ... */ }
492///
493/// /// Optional flags use Option<T>
494/// fn list_users(&self, limit: Option<usize>) { /* ... */ }
495/// }
496/// ```
497///
498/// # Generated Methods
499/// - `cli_app() -> clap::Command` - Complete CLI application
500/// - `cli_run(&self, matches: &ArgMatches)` - Execute matched command
501#[cfg(feature = "cli")]
502#[proc_macro_attribute]
503pub fn cli(attr: TokenStream, item: TokenStream) -> TokenStream {
504 let args = parse_macro_input!(attr as cli::CliArgs);
505 let impl_block = parse_macro_input!(item as ItemImpl);
506 let name = type_name(&impl_block.self_ty);
507
508 match cli::expand_cli(args, impl_block) {
509 Ok(tokens) => {
510 debug_emit("cli", &name, &tokens);
511 tokens.into()
512 }
513 Err(err) => err.to_compile_error().into(),
514 }
515}
516
517/// Generate MCP (Model Context Protocol) tools from an impl block.
518///
519/// # Basic Usage
520///
521/// ```ignore
522/// use server_less::mcp;
523///
524/// #[mcp]
525/// impl FileTools {
526/// fn read_file(&self, path: String) -> String { /* ... */ }
527/// }
528/// ```
529///
530/// # With Namespace
531///
532/// ```ignore
533/// #[mcp(namespace = "file")]
534/// impl FileTools {
535/// // Exposed as "file_read_file" tool
536/// fn read_file(&self, path: String) -> String { /* ... */ }
537/// }
538/// ```
539///
540/// # Streaming Support
541///
542/// Methods returning `impl Stream<Item = T>` are automatically collected into arrays:
543///
544/// ```ignore
545/// use futures::stream::{self, Stream};
546///
547/// #[mcp]
548/// impl DataService {
549/// // Returns JSON array: [0, 1, 2, 3, 4]
550/// fn stream_numbers(&self, count: u32) -> impl Stream<Item = u32> + use<> {
551/// stream::iter(0..count)
552/// }
553/// }
554///
555/// // Call with:
556/// service.mcp_call_async("stream_numbers", json!({"count": 5})).await
557/// // Returns: [0, 1, 2, 3, 4]
558/// ```
559///
560/// **Note:** Streaming methods require `mcp_call_async`, not `mcp_call`.
561///
562/// # Generated Methods
563/// - `mcp_tools() -> Vec<serde_json::Value>` - Tool definitions
564/// - `mcp_call(&self, name, args) -> Result<Value, String>` - Execute tool (sync only)
565/// - `mcp_call_async(&self, name, args).await` - Execute tool (supports async & streams)
566#[cfg(feature = "mcp")]
567#[proc_macro_attribute]
568pub fn mcp(attr: TokenStream, item: TokenStream) -> TokenStream {
569 let args = parse_macro_input!(attr as mcp::McpArgs);
570 let impl_block = parse_macro_input!(item as ItemImpl);
571 let name = type_name(&impl_block.self_ty);
572
573 match mcp::expand_mcp(args, impl_block) {
574 Ok(tokens) => {
575 debug_emit("mcp", &name, &tokens);
576 tokens.into()
577 }
578 Err(err) => err.to_compile_error().into(),
579 }
580}
581
582/// Generate WebSocket JSON-RPC handlers from an impl block.
583///
584/// Methods are exposed as JSON-RPC methods over WebSocket connections.
585/// Supports both sync and async methods.
586///
587/// # Basic Usage
588///
589/// ```ignore
590/// use server_less::ws;
591///
592/// #[ws(path = "/ws")]
593/// impl ChatService {
594/// fn send_message(&self, room: String, content: String) -> Message {
595/// // ...
596/// }
597/// }
598/// ```
599///
600/// # With Async Methods
601///
602/// ```ignore
603/// #[ws(path = "/ws")]
604/// impl ChatService {
605/// // Async methods work seamlessly
606/// async fn send_message(&self, room: String, content: String) -> Message {
607/// // Can await database, network calls, etc.
608/// }
609///
610/// // Mix sync and async
611/// fn get_rooms(&self) -> Vec<String> {
612/// // Synchronous method
613/// }
614/// }
615/// ```
616///
617/// # Error Handling
618///
619/// ```ignore
620/// #[ws(path = "/ws")]
621/// impl ChatService {
622/// fn send_message(&self, room: String, content: String) -> Result<Message, ChatError> {
623/// if room.is_empty() {
624/// return Err(ChatError::InvalidRoom);
625/// }
626/// Ok(Message::new(room, content))
627/// }
628/// }
629/// ```
630///
631/// # Client Usage
632///
633/// Clients send JSON-RPC 2.0 messages over WebSocket:
634///
635/// ```json
636/// // Request
637/// {
638/// "jsonrpc": "2.0",
639/// "method": "send_message",
640/// "params": {"room": "general", "content": "Hello!"},
641/// "id": 1
642/// }
643///
644/// // Response
645/// {
646/// "jsonrpc": "2.0",
647/// "result": {"id": 123, "room": "general", "content": "Hello!"},
648/// "id": 1
649/// }
650/// ```
651///
652/// # Generated Methods
653/// - `ws_router() -> axum::Router` - Router with WebSocket endpoint
654/// - `ws_handle_message(msg) -> String` - Sync message handler
655/// - `ws_handle_message_async(msg) -> String` - Async message handler
656/// - `ws_methods() -> Vec<&'static str>` - List of available methods
657#[cfg(feature = "ws")]
658#[proc_macro_attribute]
659pub fn ws(attr: TokenStream, item: TokenStream) -> TokenStream {
660 let args = parse_macro_input!(attr as ws::WsArgs);
661 let impl_block = parse_macro_input!(item as ItemImpl);
662 let name = type_name(&impl_block.self_ty);
663
664 match ws::expand_ws(args, impl_block) {
665 Ok(tokens) => {
666 debug_emit("ws", &name, &tokens);
667 tokens.into()
668 }
669 Err(err) => err.to_compile_error().into(),
670 }
671}
672
673/// Generate JSON-RPC 2.0 handlers over HTTP.
674///
675/// # Example
676///
677/// ```ignore
678/// use server_less::jsonrpc;
679///
680/// struct Calculator;
681///
682/// #[jsonrpc]
683/// impl Calculator {
684/// /// Add two numbers
685/// fn add(&self, a: i32, b: i32) -> i32 {
686/// a + b
687/// }
688///
689/// /// Multiply two numbers
690/// fn multiply(&self, a: i32, b: i32) -> i32 {
691/// a * b
692/// }
693/// }
694///
695/// // POST /rpc with {"jsonrpc": "2.0", "method": "add", "params": {"a": 1, "b": 2}, "id": 1}
696/// // Returns: {"jsonrpc": "2.0", "result": 3, "id": 1}
697/// ```
698///
699/// This generates:
700/// - `Calculator::jsonrpc_router()` returning an axum Router
701/// - `Calculator::jsonrpc_handle(request)` to handle JSON-RPC requests
702/// - `Calculator::jsonrpc_methods()` listing available methods
703///
704/// Supports JSON-RPC 2.0 features:
705/// - Named and positional parameters
706/// - Batch requests (array of requests)
707/// - Notifications (requests without id)
708#[cfg(feature = "jsonrpc")]
709#[proc_macro_attribute]
710pub fn jsonrpc(attr: TokenStream, item: TokenStream) -> TokenStream {
711 let args = parse_macro_input!(attr as jsonrpc::JsonRpcArgs);
712 let impl_block = parse_macro_input!(item as ItemImpl);
713 let name = type_name(&impl_block.self_ty);
714
715 match jsonrpc::expand_jsonrpc(args, impl_block) {
716 Ok(tokens) => {
717 debug_emit("jsonrpc", &name, &tokens);
718 tokens.into()
719 }
720 Err(err) => err.to_compile_error().into(),
721 }
722}
723
724/// Generate OpenRPC specification for JSON-RPC services.
725///
726/// OpenRPC is to JSON-RPC what OpenAPI is to REST APIs.
727///
728/// # Example
729///
730/// ```ignore
731/// use server_less::openrpc;
732///
733/// struct Calculator;
734///
735/// #[openrpc(title = "Calculator API", version = "1.0.0")]
736/// impl Calculator {
737/// /// Add two numbers
738/// fn add(&self, a: i32, b: i32) -> i32 { a + b }
739/// }
740///
741/// // Get OpenRPC spec as JSON
742/// let spec = Calculator::openrpc_spec();
743/// let json = Calculator::openrpc_json();
744///
745/// // Write to file
746/// Calculator::write_openrpc("openrpc.json")?;
747/// ```
748#[cfg(feature = "openrpc")]
749#[proc_macro_attribute]
750pub fn openrpc(attr: TokenStream, item: TokenStream) -> TokenStream {
751 let args = parse_macro_input!(attr as openrpc::OpenRpcArgs);
752 let impl_block = parse_macro_input!(item as ItemImpl);
753 let name = type_name(&impl_block.self_ty);
754
755 match openrpc::expand_openrpc(args, impl_block) {
756 Ok(tokens) => {
757 debug_emit("openrpc", &name, &tokens);
758 tokens.into()
759 }
760 Err(err) => err.to_compile_error().into(),
761 }
762}
763
764/// Generate Markdown API documentation from an impl block.
765///
766/// Creates human-readable documentation that can be used with
767/// any static site generator (VitePress, Docusaurus, MkDocs, etc.).
768///
769/// # Example
770///
771/// ```ignore
772/// use server_less::markdown;
773///
774/// struct UserService;
775///
776/// #[markdown(title = "User API")]
777/// impl UserService {
778/// /// Create a new user
779/// fn create_user(&self, name: String, email: String) -> User { ... }
780///
781/// /// Get user by ID
782/// fn get_user(&self, id: String) -> Option<User> { ... }
783/// }
784///
785/// // Get markdown string
786/// let docs = UserService::markdown_docs();
787///
788/// // Write to file
789/// UserService::write_markdown("docs/api.md")?;
790/// ```
791#[cfg(feature = "markdown")]
792#[proc_macro_attribute]
793pub fn markdown(attr: TokenStream, item: TokenStream) -> TokenStream {
794 let args = parse_macro_input!(attr as markdown::MarkdownArgs);
795 let impl_block = parse_macro_input!(item as ItemImpl);
796 let name = type_name(&impl_block.self_ty);
797
798 match markdown::expand_markdown(args, impl_block) {
799 Ok(tokens) => {
800 debug_emit("markdown", &name, &tokens);
801 tokens.into()
802 }
803 Err(err) => err.to_compile_error().into(),
804 }
805}
806
807/// Generate AsyncAPI specification for event-driven services.
808///
809/// AsyncAPI is to WebSockets/messaging what OpenAPI is to REST.
810///
811/// # Example
812///
813/// ```ignore
814/// use server_less::asyncapi;
815///
816/// struct ChatService;
817///
818/// #[asyncapi(title = "Chat API", server = "ws://localhost:8080")]
819/// impl ChatService {
820/// /// Send a message to a room
821/// fn send_message(&self, room: String, content: String) -> bool { true }
822///
823/// /// Get message history
824/// fn get_history(&self, room: String, limit: Option<u32>) -> Vec<String> { vec![] }
825/// }
826///
827/// // Get AsyncAPI spec
828/// let spec = ChatService::asyncapi_spec();
829/// let json = ChatService::asyncapi_json();
830///
831/// // Write to file
832/// ChatService::write_asyncapi("asyncapi.json")?;
833/// ```
834#[cfg(feature = "asyncapi")]
835#[proc_macro_attribute]
836pub fn asyncapi(attr: TokenStream, item: TokenStream) -> TokenStream {
837 let args = parse_macro_input!(attr as asyncapi::AsyncApiArgs);
838 let impl_block = parse_macro_input!(item as ItemImpl);
839 let name = type_name(&impl_block.self_ty);
840
841 match asyncapi::expand_asyncapi(args, impl_block) {
842 Ok(tokens) => {
843 debug_emit("asyncapi", &name, &tokens);
844 tokens.into()
845 }
846 Err(err) => err.to_compile_error().into(),
847 }
848}
849
850/// Generate Connect protocol schema from an impl block.
851///
852/// Connect is a modern RPC protocol from Buf that works over HTTP/1.1, HTTP/2, and HTTP/3.
853/// The generated schema is compatible with connect-go, connect-es, connect-swift, etc.
854///
855/// # Example
856///
857/// ```ignore
858/// use server_less::connect;
859///
860/// struct UserService;
861///
862/// #[connect(package = "users.v1")]
863/// impl UserService {
864/// fn get_user(&self, id: String) -> User { ... }
865/// }
866///
867/// // Get schema and endpoint paths
868/// let schema = UserService::connect_schema();
869/// let paths = UserService::connect_paths(); // ["/users.v1.UserService/GetUser", ...]
870/// ```
871#[cfg(feature = "connect")]
872#[proc_macro_attribute]
873pub fn connect(attr: TokenStream, item: TokenStream) -> TokenStream {
874 let args = parse_macro_input!(attr as connect::ConnectArgs);
875 let impl_block = parse_macro_input!(item as ItemImpl);
876 let name = type_name(&impl_block.self_ty);
877
878 match connect::expand_connect(args, impl_block) {
879 Ok(tokens) => {
880 debug_emit("connect", &name, &tokens);
881 tokens.into()
882 }
883 Err(err) => err.to_compile_error().into(),
884 }
885}
886
887/// Generate Protocol Buffers schema from an impl block.
888///
889/// # Example
890///
891/// ```ignore
892/// use server_less::grpc;
893///
894/// struct UserService;
895///
896/// #[grpc(package = "users")]
897/// impl UserService {
898/// /// Get user by ID
899/// fn get_user(&self, id: String) -> User { ... }
900///
901/// /// Create a new user
902/// fn create_user(&self, name: String, email: String) -> User { ... }
903/// }
904///
905/// // Get the proto schema
906/// let proto = UserService::proto_schema();
907///
908/// // Write to file for use with tonic-build
909/// UserService::write_proto("proto/users.proto")?;
910/// ```
911///
912/// The generated schema can be used with tonic-build in your build.rs
913/// to generate the full gRPC client/server implementation.
914#[cfg(feature = "grpc")]
915#[proc_macro_attribute]
916pub fn grpc(attr: TokenStream, item: TokenStream) -> TokenStream {
917 let args = parse_macro_input!(attr as grpc::GrpcArgs);
918 let impl_block = parse_macro_input!(item as ItemImpl);
919 let name = type_name(&impl_block.self_ty);
920
921 match grpc::expand_grpc(args, impl_block) {
922 Ok(tokens) => {
923 debug_emit("grpc", &name, &tokens);
924 tokens.into()
925 }
926 Err(err) => err.to_compile_error().into(),
927 }
928}
929
930/// Generate Cap'n Proto schema from an impl block.
931///
932/// # Example
933///
934/// ```ignore
935/// use server_less::capnp;
936///
937/// struct UserService;
938///
939/// #[capnp(id = "0x85150b117366d14b")]
940/// impl UserService {
941/// /// Get user by ID
942/// fn get_user(&self, id: String) -> String { ... }
943///
944/// /// Create a new user
945/// fn create_user(&self, name: String, email: String) -> String { ... }
946/// }
947///
948/// // Get the Cap'n Proto schema
949/// let schema = UserService::capnp_schema();
950///
951/// // Write to file for use with capnpc
952/// UserService::write_capnp("schema/users.capnp")?;
953/// ```
954///
955/// The generated schema can be used with capnpc to generate
956/// the full Cap'n Proto serialization code.
957#[cfg(feature = "capnp")]
958#[proc_macro_attribute]
959pub fn capnp(attr: TokenStream, item: TokenStream) -> TokenStream {
960 let args = parse_macro_input!(attr as capnp::CapnpArgs);
961 let impl_block = parse_macro_input!(item as ItemImpl);
962 let name = type_name(&impl_block.self_ty);
963
964 match capnp::expand_capnp(args, impl_block) {
965 Ok(tokens) => {
966 debug_emit("capnp", &name, &tokens);
967 tokens.into()
968 }
969 Err(err) => err.to_compile_error().into(),
970 }
971}
972
973/// Generate Apache Thrift schema from an impl block.
974///
975/// # Example
976///
977/// ```ignore
978/// use server_less::thrift;
979///
980/// struct UserService;
981///
982/// #[thrift(namespace = "users")]
983/// impl UserService {
984/// /// Get user by ID
985/// fn get_user(&self, id: String) -> String { ... }
986///
987/// /// Create a new user
988/// fn create_user(&self, name: String, email: String) -> String { ... }
989/// }
990///
991/// // Get the Thrift schema
992/// let schema = UserService::thrift_schema();
993///
994/// // Write to file for use with thrift compiler
995/// UserService::write_thrift("idl/users.thrift")?;
996/// ```
997///
998/// The generated schema can be used with the Thrift compiler to generate
999/// client/server code in various languages.
1000#[cfg(feature = "thrift")]
1001#[proc_macro_attribute]
1002pub fn thrift(attr: TokenStream, item: TokenStream) -> TokenStream {
1003 let args = parse_macro_input!(attr as thrift::ThriftArgs);
1004 let impl_block = parse_macro_input!(item as ItemImpl);
1005 let name = type_name(&impl_block.self_ty);
1006
1007 match thrift::expand_thrift(args, impl_block) {
1008 Ok(tokens) => {
1009 debug_emit("thrift", &name, &tokens);
1010 tokens.into()
1011 }
1012 Err(err) => err.to_compile_error().into(),
1013 }
1014}
1015
1016/// Generate Smithy IDL schema from an impl block.
1017///
1018/// Smithy is AWS's open-source interface definition language for defining APIs.
1019/// The generated schema follows Smithy 2.0 specification.
1020///
1021/// # Example
1022///
1023/// ```ignore
1024/// use server_less::smithy;
1025///
1026/// struct UserService;
1027///
1028/// #[smithy(namespace = "com.example.users")]
1029/// impl UserService {
1030/// /// Get user by ID
1031/// fn get_user(&self, id: String) -> User { ... }
1032///
1033/// /// Create a new user
1034/// fn create_user(&self, name: String, email: String) -> User { ... }
1035/// }
1036///
1037/// // Get Smithy schema
1038/// let schema = UserService::smithy_schema();
1039/// // Write to file
1040/// UserService::write_smithy("service.smithy")?;
1041/// ```
1042///
1043/// The generated schema can be used with the Smithy toolchain for code generation.
1044#[cfg(feature = "smithy")]
1045#[proc_macro_attribute]
1046pub fn smithy(attr: TokenStream, item: TokenStream) -> TokenStream {
1047 let args = parse_macro_input!(attr as smithy::SmithyArgs);
1048 let impl_block = parse_macro_input!(item as ItemImpl);
1049 let name = type_name(&impl_block.self_ty);
1050
1051 match smithy::expand_smithy(args, impl_block) {
1052 Ok(tokens) => {
1053 debug_emit("smithy", &name, &tokens);
1054 tokens.into()
1055 }
1056 Err(err) => err.to_compile_error().into(),
1057 }
1058}
1059
1060/// Generate JSON Schema from an impl block.
1061///
1062/// Generates JSON Schema definitions for request/response types.
1063/// Useful for API validation, documentation, and tooling.
1064///
1065/// # Example
1066///
1067/// ```ignore
1068/// use server_less::jsonschema;
1069///
1070/// struct UserService;
1071///
1072/// #[jsonschema(title = "User API")]
1073/// impl UserService {
1074/// /// Get user by ID
1075/// fn get_user(&self, id: String) -> User { ... }
1076///
1077/// /// Create a new user
1078/// fn create_user(&self, name: String, email: String) -> User { ... }
1079/// }
1080///
1081/// // Get JSON Schema
1082/// let schema = UserService::json_schema();
1083/// // Write to file
1084/// UserService::write_json_schema("schema.json")?;
1085/// ```
1086#[cfg(feature = "jsonschema")]
1087#[proc_macro_attribute]
1088pub fn jsonschema(attr: TokenStream, item: TokenStream) -> TokenStream {
1089 let args = parse_macro_input!(attr as jsonschema::JsonSchemaArgs);
1090 let impl_block = parse_macro_input!(item as ItemImpl);
1091 let name = type_name(&impl_block.self_ty);
1092
1093 match jsonschema::expand_jsonschema(args, impl_block) {
1094 Ok(tokens) => {
1095 debug_emit("jsonschema", &name, &tokens);
1096 tokens.into()
1097 }
1098 Err(err) => err.to_compile_error().into(),
1099 }
1100}
1101
1102/// Generate GraphQL schema from an impl block using async-graphql.
1103///
1104/// Methods are automatically classified as Queries or Mutations based on naming:
1105/// - Queries: `get_*`, `list_*`, `find_*`, `search_*`, `fetch_*`, `query_*`
1106/// - Mutations: everything else (create, update, delete, etc.)
1107///
1108/// # Basic Usage
1109///
1110/// ```ignore
1111/// use server_less::graphql;
1112///
1113/// #[graphql]
1114/// impl UserService {
1115/// // Query: returns single user
1116/// async fn get_user(&self, id: String) -> Option<User> {
1117/// // ...
1118/// }
1119///
1120/// // Query: returns list of users
1121/// async fn list_users(&self) -> Vec<User> {
1122/// // ...
1123/// }
1124///
1125/// // Mutation: creates new user
1126/// async fn create_user(&self, name: String, email: String) -> User {
1127/// // ...
1128/// }
1129/// }
1130/// ```
1131///
1132/// # Type Mappings
1133///
1134/// - `String`, `i32`, `bool`, etc. → GraphQL scalars
1135/// - `Option<T>` → nullable GraphQL field
1136/// - `Vec<T>` → GraphQL list `[T]`
1137/// - Custom structs → GraphQL objects (must derive SimpleObject)
1138///
1139/// ```ignore
1140/// use async_graphql::SimpleObject;
1141///
1142/// #[derive(SimpleObject)]
1143/// struct User {
1144/// id: String,
1145/// name: String,
1146/// email: Option<String>, // Nullable field
1147/// }
1148///
1149/// #[graphql]
1150/// impl UserService {
1151/// async fn get_user(&self, id: String) -> Option<User> {
1152/// // Returns User object with proper GraphQL schema
1153/// }
1154///
1155/// async fn list_users(&self) -> Vec<User> {
1156/// // Returns [User] in GraphQL
1157/// }
1158/// }
1159/// ```
1160///
1161/// # GraphQL Queries
1162///
1163/// ```graphql
1164/// # Query single user
1165/// query {
1166/// getUser(id: "123") {
1167/// id
1168/// name
1169/// email
1170/// }
1171/// }
1172///
1173/// # List all users
1174/// query {
1175/// listUsers {
1176/// id
1177/// name
1178/// }
1179/// }
1180///
1181/// # Mutation
1182/// mutation {
1183/// createUser(name: "Alice", email: "alice@example.com") {
1184/// id
1185/// name
1186/// }
1187/// }
1188/// ```
1189///
1190/// # Custom Scalars
1191///
1192/// Common custom scalar types are automatically supported:
1193///
1194/// ```ignore
1195/// use chrono::{DateTime, Utc};
1196/// use uuid::Uuid;
1197///
1198/// #[graphql]
1199/// impl EventService {
1200/// // UUID parameter
1201/// async fn get_event(&self, event_id: Uuid) -> Option<Event> { /* ... */ }
1202///
1203/// // DateTime parameter
1204/// async fn list_events(&self, since: DateTime<Utc>) -> Vec<Event> { /* ... */ }
1205///
1206/// // JSON parameter
1207/// async fn search_events(&self, filter: serde_json::Value) -> Vec<Event> { /* ... */ }
1208/// }
1209/// ```
1210///
1211/// Supported custom scalars:
1212/// - `chrono::DateTime<Utc>` → DateTime
1213/// - `uuid::Uuid` → UUID
1214/// - `url::Url` → Url
1215/// - `serde_json::Value` → JSON
1216///
1217/// # Generated Methods
1218/// - `graphql_schema() -> Schema` - async-graphql Schema
1219/// - `graphql_router() -> axum::Router` - Router with /graphql endpoint
1220/// - `graphql_sdl() -> String` - Schema Definition Language string
1221#[cfg(feature = "graphql")]
1222#[proc_macro_attribute]
1223pub fn graphql(attr: TokenStream, item: TokenStream) -> TokenStream {
1224 let args = parse_macro_input!(attr as graphql::GraphqlArgs);
1225 let impl_block = parse_macro_input!(item as ItemImpl);
1226 let name = type_name(&impl_block.self_ty);
1227
1228 match graphql::expand_graphql(args, impl_block) {
1229 Ok(tokens) => {
1230 debug_emit("graphql", &name, &tokens);
1231 tokens.into()
1232 }
1233 Err(err) => err.to_compile_error().into(),
1234 }
1235}
1236
1237/// Define a GraphQL enum type.
1238///
1239/// Generates a GraphQL Enum type definition from a Rust enum.
1240/// Only unit variants (no fields) are supported.
1241///
1242/// # Example
1243///
1244/// ```ignore
1245/// use server_less::graphql_enum;
1246///
1247/// #[graphql_enum]
1248/// #[derive(Clone, Debug)]
1249/// enum Status {
1250/// /// User is active
1251/// Active,
1252/// /// User is inactive
1253/// Inactive,
1254/// /// Awaiting approval
1255/// Pending,
1256/// }
1257///
1258/// // Then register with #[graphql]:
1259/// #[graphql(enums(Status))]
1260/// impl MyService {
1261/// pub fn get_status(&self) -> Status { Status::Active }
1262/// }
1263/// ```
1264///
1265/// # Generated Methods
1266///
1267/// - `__graphql_enum_type() -> async_graphql::dynamic::Enum` - Enum type definition
1268/// - `__to_graphql_value(&self) -> async_graphql::Value` - Convert to GraphQL value
1269///
1270/// # Variant Naming
1271///
1272/// Variant names are converted to SCREAMING_SNAKE_CASE for GraphQL:
1273/// - `Active` → `ACTIVE`
1274/// - `InProgress` → `IN_PROGRESS`
1275#[cfg(feature = "graphql")]
1276#[proc_macro_attribute]
1277pub fn graphql_enum(_attr: TokenStream, item: TokenStream) -> TokenStream {
1278 let item_enum = parse_macro_input!(item as ItemEnum);
1279 let name = item_enum.ident.to_string();
1280
1281 match graphql_enum::expand_graphql_enum(item_enum) {
1282 Ok(tokens) => {
1283 debug_emit("graphql_enum", &name, &tokens);
1284 tokens.into()
1285 }
1286 Err(err) => err.to_compile_error().into(),
1287 }
1288}
1289
1290/// Define a GraphQL input type.
1291///
1292/// Generates a GraphQL InputObject type definition from a Rust struct.
1293/// The struct must implement `serde::Deserialize` for input parsing.
1294///
1295/// # Example
1296///
1297/// ```ignore
1298/// use server_less::graphql_input;
1299/// use serde::Deserialize;
1300///
1301/// #[graphql_input]
1302/// #[derive(Clone, Debug, Deserialize)]
1303/// struct CreateUserInput {
1304/// /// User's name
1305/// name: String,
1306/// /// User's email address
1307/// email: String,
1308/// /// Optional age
1309/// age: Option<i32>,
1310/// }
1311///
1312/// // Then register with #[graphql]:
1313/// #[graphql(inputs(CreateUserInput))]
1314/// impl UserService {
1315/// pub fn create_user(&self, input: CreateUserInput) -> User { /* ... */ }
1316/// }
1317/// ```
1318///
1319/// # Generated Methods
1320///
1321/// - `__graphql_input_type() -> async_graphql::dynamic::InputObject` - Input type definition
1322/// - `__from_graphql_value(value) -> Result<Self, String>` - Parse from GraphQL value
1323///
1324/// # Field Naming
1325///
1326/// Field names are converted to camelCase for GraphQL:
1327/// - `user_name` → `userName`
1328/// - `email_address` → `emailAddress`
1329#[cfg(feature = "graphql")]
1330#[proc_macro_attribute]
1331pub fn graphql_input(_attr: TokenStream, item: TokenStream) -> TokenStream {
1332 let item_struct = parse_macro_input!(item as ItemStruct);
1333 let name = item_struct.ident.to_string();
1334
1335 match graphql_input::expand_graphql_input(item_struct) {
1336 Ok(tokens) => {
1337 debug_emit("graphql_input", &name, &tokens);
1338 tokens.into()
1339 }
1340 Err(err) => err.to_compile_error().into(),
1341 }
1342}
1343
1344/// Coordinate multiple protocol handlers into a single server.
1345///
1346/// # Example
1347///
1348/// ```ignore
1349/// use server_less::{http, ws, jsonrpc, serve};
1350///
1351/// struct MyService;
1352///
1353/// #[http]
1354/// #[ws]
1355/// #[jsonrpc]
1356/// #[serve(http, ws, jsonrpc)]
1357/// impl MyService {
1358/// fn list_items(&self) -> Vec<String> { vec![] }
1359/// }
1360///
1361/// // Now you can:
1362/// // - service.serve("0.0.0.0:3000").await // start server
1363/// // - service.router() // get combined router
1364/// ```
1365///
1366/// # Arguments
1367///
1368/// - `http` - Include the HTTP router (REST API)
1369/// - `ws` - Include the WebSocket router (WS JSON-RPC)
1370/// - `jsonrpc` - Include the JSON-RPC HTTP router
1371/// - `graphql` - Include the GraphQL router
1372/// - `health = "/path"` - Custom health check path (default: `/health`)
1373#[cfg(feature = "http")]
1374#[proc_macro_attribute]
1375pub fn serve(attr: TokenStream, item: TokenStream) -> TokenStream {
1376 let args = parse_macro_input!(attr as http::ServeArgs);
1377 let impl_block = parse_macro_input!(item as ItemImpl);
1378 let name = type_name(&impl_block.self_ty);
1379
1380 match http::expand_serve(args, impl_block) {
1381 Ok(tokens) => {
1382 debug_emit("serve", &name, &tokens);
1383 tokens.into()
1384 }
1385 Err(err) => err.to_compile_error().into(),
1386 }
1387}
1388
1389/// Helper attribute for method-level HTTP route customization.
1390///
1391/// This attribute is used within `#[http]` impl blocks to customize
1392/// individual method routing. It is a no-op on its own.
1393///
1394/// # Example
1395///
1396/// ```ignore
1397/// #[http(prefix = "/api")]
1398/// impl MyService {
1399/// #[route(method = "POST", path = "/custom")]
1400/// fn my_method(&self) { }
1401///
1402/// #[route(skip)]
1403/// fn internal_method(&self) { }
1404///
1405/// #[route(hidden)] // Hidden from OpenAPI but still routed
1406/// fn secret(&self) { }
1407/// }
1408/// ```
1409#[cfg(feature = "http")]
1410#[proc_macro_attribute]
1411pub fn route(_attr: TokenStream, item: TokenStream) -> TokenStream {
1412 // Pass through unchanged - the #[http] macro parses these attributes
1413 item
1414}
1415
1416/// Helper attribute for method-level HTTP response customization.
1417///
1418/// This attribute is used within `#[http]` impl blocks to customize
1419/// individual method responses. It is a no-op on its own.
1420///
1421/// # Supported Options
1422///
1423/// - `status = <code>` - Custom HTTP status code (e.g., 201, 204)
1424/// - `content_type = "<type>"` - Custom content type
1425/// - `header = "<name>", value = "<value>"` - Add custom response header
1426///
1427/// Multiple `#[response(...)]` attributes can be combined on a single method.
1428///
1429/// # Examples
1430///
1431/// ```ignore
1432/// #[http(prefix = "/api")]
1433/// impl MyService {
1434/// // Custom status code for creation
1435/// #[response(status = 201)]
1436/// fn create_item(&self, name: String) -> Item { /* ... */ }
1437///
1438/// // No content response
1439/// #[response(status = 204)]
1440/// fn delete_item(&self, id: String) { /* ... */ }
1441///
1442/// // Binary response with custom content type
1443/// #[response(content_type = "application/octet-stream")]
1444/// fn download(&self, id: String) -> Vec<u8> { /* ... */ }
1445///
1446/// // Add custom headers
1447/// #[response(header = "X-Custom", value = "foo")]
1448/// fn with_header(&self) -> String { /* ... */ }
1449///
1450/// // Combine multiple response attributes
1451/// #[response(status = 201)]
1452/// #[response(header = "Location", value = "/api/items/123")]
1453/// #[response(header = "X-Request-Id", value = "abc")]
1454/// fn create_with_headers(&self, name: String) -> Item { /* ... */ }
1455/// }
1456/// ```
1457#[cfg(feature = "http")]
1458#[proc_macro_attribute]
1459pub fn response(_attr: TokenStream, item: TokenStream) -> TokenStream {
1460 // Pass through unchanged - the #[http] macro parses these attributes
1461 item
1462}
1463
1464/// Helper attribute for parameter-level HTTP customization.
1465///
1466/// This attribute is used on function parameters within `#[http]` impl blocks
1467/// to customize parameter extraction and naming. It is a no-op on its own.
1468///
1469/// **Note:** Requires nightly Rust with `#![feature(register_tool)]` and
1470/// `#![register_tool(param)]` at the crate root.
1471///
1472/// # Supported Options
1473///
1474/// - `name = "<wire_name>"` - Use a different name on the wire (e.g., `q` instead of `query`)
1475/// - `default = <value>` - Provide a default value for optional parameters
1476/// - `query` - Force parameter to come from query string
1477/// - `path` - Force parameter to come from URL path
1478/// - `body` - Force parameter to come from request body
1479/// - `header` - Extract parameter from HTTP header
1480///
1481/// # Location Inference
1482///
1483/// When no location is specified, parameters are inferred based on conventions:
1484/// - Parameters named `id` or ending in `_id` → path parameters
1485/// - POST/PUT/PATCH methods → body parameters
1486/// - GET/DELETE methods → query parameters
1487///
1488/// # Examples
1489///
1490/// ```ignore
1491/// #![feature(register_tool)]
1492/// #![register_tool(param)]
1493///
1494/// #[http(prefix = "/api")]
1495/// impl SearchService {
1496/// // Rename parameter: code uses `query`, API accepts `q`
1497/// fn search(&self, #[param(name = "q")] query: String) -> Vec<Result> {
1498/// /* ... */
1499/// }
1500///
1501/// // Default value for pagination
1502/// fn list_items(
1503/// &self,
1504/// #[param(default = 0)] offset: u32,
1505/// #[param(default = 10)] limit: u32,
1506/// ) -> Vec<Item> {
1507/// /* ... */
1508/// }
1509///
1510/// // Extract API key from header
1511/// fn protected_endpoint(
1512/// &self,
1513/// #[param(header, name = "X-API-Key")] api_key: String,
1514/// data: String,
1515/// ) -> String {
1516/// /* ... */
1517/// }
1518///
1519/// // Override location inference: force to query even though method is POST
1520/// fn search_posts(
1521/// &self,
1522/// #[param(query)] filter: String,
1523/// #[param(body)] content: String,
1524/// ) -> Vec<Post> {
1525/// /* ... */
1526/// }
1527///
1528/// // Combine multiple options
1529/// fn advanced(
1530/// &self,
1531/// #[param(query, name = "page", default = 1)] page_num: u32,
1532/// ) -> Vec<Item> {
1533/// /* ... */
1534/// }
1535/// }
1536/// ```
1537///
1538/// # OpenAPI Integration
1539///
1540/// - Parameters with `name` are documented with their wire names
1541/// - Parameters with `default` are marked as not required
1542/// - Location overrides are reflected in OpenAPI specs
1543#[cfg(feature = "http")]
1544#[proc_macro_attribute]
1545pub fn param(_attr: TokenStream, item: TokenStream) -> TokenStream {
1546 // Pass through unchanged - the #[http] macro parses these attributes
1547 item
1548}
1549
1550// ============================================================================
1551// Blessed Presets
1552// ============================================================================
1553
1554/// Blessed preset: HTTP server with OpenAPI and serve.
1555///
1556/// Combines `#[http]` + `#[serve(http)]` into a single attribute.
1557///
1558/// # Example
1559///
1560/// ```ignore
1561/// use server_less::server;
1562///
1563/// #[derive(Clone)]
1564/// struct MyApi;
1565///
1566/// #[server]
1567/// impl MyApi {
1568/// pub fn list_items(&self) -> Vec<String> { vec![] }
1569/// pub fn create_item(&self, name: String) -> String { name }
1570/// }
1571///
1572/// // Equivalent to:
1573/// // #[http]
1574/// // #[serve(http)]
1575/// // impl MyApi { ... }
1576/// ```
1577///
1578/// # Options
1579///
1580/// - `prefix` - URL prefix (e.g., `#[server(prefix = "/api")]`)
1581/// - `openapi` - Toggle OpenAPI generation (default: true)
1582/// - `health` - Custom health check path (default: `/health`)
1583#[cfg(feature = "http")]
1584#[proc_macro_attribute]
1585pub fn server(attr: TokenStream, item: TokenStream) -> TokenStream {
1586 // When applied to a method inside an impl block (e.g. `#[server(skip)]`),
1587 // pass through unchanged. The enclosing protocol macro reads these
1588 // attributes from the ItemImpl tokens; `#[server]` just needs to not error.
1589 let item2: proc_macro2::TokenStream = item.clone().into();
1590 if syn::parse2::<ItemImpl>(item2).is_err() {
1591 return item;
1592 }
1593 let args = parse_macro_input!(attr as server::ServerArgs);
1594 let impl_block = parse_macro_input!(item as ItemImpl);
1595 let name = type_name(&impl_block.self_ty);
1596
1597 match server::expand_server(args, impl_block) {
1598 Ok(tokens) => {
1599 debug_emit("server", &name, &tokens);
1600 tokens.into()
1601 }
1602 Err(err) => err.to_compile_error().into(),
1603 }
1604}
1605
1606/// Blessed preset: JSON-RPC server with OpenRPC spec and serve.
1607///
1608/// Combines `#[jsonrpc]` + `#[openrpc]` + `#[serve(jsonrpc)]` into a single attribute.
1609/// OpenRPC and serve are included when their features are enabled, and gracefully
1610/// omitted otherwise.
1611///
1612/// # Example
1613///
1614/// ```ignore
1615/// use server_less::rpc;
1616///
1617/// #[derive(Clone)]
1618/// struct Calculator;
1619///
1620/// #[rpc]
1621/// impl Calculator {
1622/// pub fn add(&self, a: i32, b: i32) -> i32 { a + b }
1623/// pub fn multiply(&self, a: i32, b: i32) -> i32 { a * b }
1624/// }
1625/// ```
1626///
1627/// # Options
1628///
1629/// - `path` - JSON-RPC endpoint path (e.g., `#[rpc(path = "/api")]`)
1630/// - `openrpc` - Toggle OpenRPC spec generation (default: true)
1631/// - `health` - Custom health check path (default: `/health`)
1632#[cfg(feature = "jsonrpc")]
1633#[proc_macro_attribute]
1634pub fn rpc(attr: TokenStream, item: TokenStream) -> TokenStream {
1635 let args = parse_macro_input!(attr as rpc_preset::RpcArgs);
1636 let impl_block = parse_macro_input!(item as ItemImpl);
1637 let name = type_name(&impl_block.self_ty);
1638
1639 match rpc_preset::expand_rpc(args, impl_block) {
1640 Ok(tokens) => {
1641 debug_emit("rpc", &name, &tokens);
1642 tokens.into()
1643 }
1644 Err(err) => err.to_compile_error().into(),
1645 }
1646}
1647
1648/// Blessed preset: MCP tools with JSON Schema.
1649///
1650/// Combines `#[mcp]` + `#[jsonschema]` into a single attribute.
1651/// JSON Schema is included when the feature is enabled, and gracefully
1652/// omitted otherwise.
1653///
1654/// # Example
1655///
1656/// ```ignore
1657/// use server_less::tool;
1658///
1659/// struct FileTools;
1660///
1661/// #[tool(namespace = "file")]
1662/// impl FileTools {
1663/// pub fn read_file(&self, path: String) -> String { String::new() }
1664/// pub fn write_file(&self, path: String, content: String) -> bool { true }
1665/// }
1666/// ```
1667///
1668/// # Options
1669///
1670/// - `namespace` - MCP tool namespace prefix
1671/// - `jsonschema` - Toggle JSON Schema generation (default: true)
1672#[cfg(feature = "mcp")]
1673#[proc_macro_attribute]
1674pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
1675 let args = parse_macro_input!(attr as tool::ToolArgs);
1676 let impl_block = parse_macro_input!(item as ItemImpl);
1677 let name = type_name(&impl_block.self_ty);
1678
1679 match tool::expand_tool(args, impl_block) {
1680 Ok(tokens) => {
1681 debug_emit("tool", &name, &tokens);
1682 tokens.into()
1683 }
1684 Err(err) => err.to_compile_error().into(),
1685 }
1686}
1687
1688/// Blessed preset: CLI application with Markdown docs.
1689///
1690/// Combines `#[cli]` + `#[markdown]` into a single attribute.
1691/// Markdown docs are included when the feature is enabled, and gracefully
1692/// omitted otherwise.
1693///
1694/// Named `program` instead of `cli` to avoid collision with the existing
1695/// `#[cli]` attribute macro.
1696///
1697/// # Example
1698///
1699/// ```ignore
1700/// use server_less::program;
1701///
1702/// struct MyApp;
1703///
1704/// #[program(name = "myctl", version = "1.0.0")]
1705/// impl MyApp {
1706/// pub fn create_user(&self, name: String) { println!("Created {}", name); }
1707/// pub fn list_users(&self) { println!("Listing users..."); }
1708/// }
1709/// ```
1710///
1711/// # Options
1712///
1713/// - `name` - CLI application name
1714/// - `version` - CLI version string
1715/// - `about` - CLI description
1716/// - `markdown` - Toggle Markdown docs generation (default: true)
1717#[cfg(feature = "cli")]
1718#[proc_macro_attribute]
1719pub fn program(attr: TokenStream, item: TokenStream) -> TokenStream {
1720 let args = parse_macro_input!(attr as program::ProgramArgs);
1721 let impl_block = parse_macro_input!(item as ItemImpl);
1722 let name = type_name(&impl_block.self_ty);
1723
1724 match program::expand_program(args, impl_block) {
1725 Ok(tokens) => {
1726 debug_emit("program", &name, &tokens);
1727 tokens.into()
1728 }
1729 Err(err) => err.to_compile_error().into(),
1730 }
1731}
1732
1733/// Derive macro for error types that implement `IntoErrorCode`.
1734///
1735/// # Example
1736///
1737/// ```ignore
1738/// use server_less::ServerlessError;
1739///
1740/// #[derive(ServerlessError)]
1741/// enum MyError {
1742/// #[error(code = NotFound, message = "User not found")]
1743/// UserNotFound,
1744/// #[error(code = 400)] // HTTP status also works
1745/// InvalidInput(String),
1746/// // Code inferred from variant name
1747/// Unauthorized,
1748/// }
1749/// ```
1750///
1751/// This generates:
1752/// - `impl IntoErrorCode for MyError`
1753/// - `impl Display for MyError`
1754/// - `impl Error for MyError`
1755///
1756/// # Attributes
1757///
1758/// - `#[error(code = X)]` - Set error code (ErrorCode variant or HTTP status)
1759/// - `#[error(message = "...")]` - Set custom message
1760///
1761/// Without attributes, the error code is inferred from the variant name.
1762#[proc_macro_derive(ServerlessError, attributes(error))]
1763pub fn serverless_error(input: TokenStream) -> TokenStream {
1764 let input = parse_macro_input!(input as DeriveInput);
1765 let name = input.ident.to_string();
1766
1767 match error::expand_serverless_error(input) {
1768 Ok(tokens) => {
1769 debug_emit("ServerlessError", &name, &tokens);
1770 tokens.into()
1771 }
1772 Err(err) => err.to_compile_error().into(),
1773 }
1774}