coralstack_cmd_ipc_macros/lib.rs
1//! Procedural macros for [`coralstack-cmd-ipc`].
2//!
3//! This crate provides four attribute macros that keep command and
4//! event definitions concise and colocated with their handlers:
5//!
6//! - [`macro@command`] — mark one async fn/method as a command.
7//! - [`macro@command_service`] — mark an `impl` block; every method
8//! inside tagged with `#[command("id")]` becomes a registered command
9//! and the block gains a generated `register(self, ®istry)`
10//! helper that installs every command in one call.
11//! - [`macro@event`] — mark a payload struct as a typed event; emits
12//! the `impl Event` and auto-derives `Serialize` / `Deserialize` /
13//! `JsonSchema`.
14//! - [`macro@payload`] — mark any struct (typically command requests
15//! and responses) to auto-derive `Serialize` / `Deserialize` /
16//! `JsonSchema` without requiring the user crate to depend on
17//! `serde` / `schemars` directly.
18//!
19//! All four resolve their derives against the `serde` / `schemars`
20//! re-exports published by `coralstack-cmd-ipc`, so user crates depend
21//! on `coralstack-cmd-ipc` alone.
22//!
23//! See the `coralstack-cmd-ipc` crate docs for usage examples and
24//! end-to-end integration tests.
25
26mod attr_args;
27mod command_attr;
28mod commands_attr;
29mod event_attr;
30mod payload_attr;
31
32use proc_macro::TokenStream;
33
34/// Attach to an `async fn` (free-standing or inside a
35/// `#[command_service] impl` block) to register it as a typed command.
36///
37/// Usage inside a `#[command_service] impl Service` block:
38///
39/// ```ignore
40/// #[command_service]
41/// impl Service {
42/// #[command("math.add", description = "Add two numbers")]
43/// async fn add(&self, req: AddReq) -> Result<i64, CommandError> { ... }
44/// }
45/// ```
46///
47/// Usage as a free function:
48///
49/// ```ignore
50/// #[command("greet")]
51/// async fn greet(name: String) -> Result<String, CommandError> { ... }
52/// // Exposes `register_greet(®istry).await?` to install the command.
53/// ```
54#[proc_macro_attribute]
55pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream {
56 command_attr::expand(attr.into(), item.into())
57 .unwrap_or_else(syn::Error::into_compile_error)
58 .into()
59}
60
61/// Attach to an `impl` block whose methods are tagged with `#[command]`.
62///
63/// Rewrites the block so every `#[command("id")]` method becomes a typed
64/// command wrapper struct implementing `Command`, and adds an inherent
65/// `register` async method to the host type that installs every such
66/// command on a `&CommandRegistry`.
67///
68/// ```ignore
69/// #[command_service]
70/// impl MathService {
71/// #[command("math.add")]
72/// async fn add(&self, req: AddReq) -> Result<i64, CommandError> { ... }
73///
74/// #[command("math.sub")]
75/// async fn sub(&self, req: SubReq) -> Result<i64, CommandError> { ... }
76/// }
77///
78/// // Registers both commands with one call:
79/// MathService.register(®istry).await?;
80/// ```
81#[proc_macro_attribute]
82pub fn command_service(attr: TokenStream, item: TokenStream) -> TokenStream {
83 commands_attr::expand(attr.into(), item.into())
84 .unwrap_or_else(syn::Error::into_compile_error)
85 .into()
86}
87
88/// Attach to a payload struct to register it as a typed event.
89///
90/// The macro auto-derives `Serialize`, `Deserialize`, and `JsonSchema`
91/// against the `serde` / `schemars` re-exports from `coralstack-cmd-ipc`,
92/// so user crates don't need to pull those dependencies into their own
93/// `Cargo.toml`. It also emits the `impl Event` for the struct with the
94/// id and wire-level schema.
95///
96/// Unit structs emit no payload on the wire — the natural way to declare
97/// a void event.
98///
99/// ```ignore
100/// /// Worker has finished initializing.
101/// #[event("worker.ready")]
102/// pub struct WorkerReady {
103/// pub worker_id: String,
104/// pub command_count: u32,
105/// }
106///
107/// // Void event — no payload on the wire.
108/// #[event("worker.tick")]
109/// pub struct WorkerTick;
110///
111/// // Emit with full type safety:
112/// registry.emit(WorkerReady { worker_id: "w1".into(), command_count: 2 })?;
113/// registry.emit(WorkerTick)?;
114///
115/// // Subscribe — callback receives a deserialized `WorkerReady`:
116/// let _unsub = registry.on::<WorkerReady>(|event| {
117/// println!("{} ready", event.worker_id);
118/// });
119/// ```
120#[proc_macro_attribute]
121pub fn event(attr: TokenStream, item: TokenStream) -> TokenStream {
122 event_attr::expand(attr.into(), item.into())
123 .unwrap_or_else(syn::Error::into_compile_error)
124 .into()
125}
126
127/// Attach to a plain data struct to auto-derive `Serialize`,
128/// `Deserialize`, and `JsonSchema`. Use for command request / response
129/// types (and any other struct you want those traits on) so user crates
130/// only need to depend on `coralstack-cmd-ipc`.
131///
132/// ```ignore
133/// use coralstack_cmd_ipc::prelude::*;
134///
135/// #[payload]
136/// struct AddReq { a: i64, b: i64 }
137///
138/// #[payload]
139/// struct AddRes { sum: i64 }
140/// ```
141///
142/// Users who need extra derives (`Clone`, `Debug`, `PartialEq`, custom
143/// `#[serde(...)]` attributes) add them normally — `#[payload]` is
144/// additive.
145#[proc_macro_attribute]
146pub fn payload(attr: TokenStream, item: TokenStream) -> TokenStream {
147 payload_attr::expand(attr.into(), item.into())
148 .unwrap_or_else(syn::Error::into_compile_error)
149 .into()
150}