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