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