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