Skip to main content

clawless_derive/
lib.rs

1#![cfg_attr(not(doctest),doc = include_str!("../README.md"))]
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{ItemFn, parse_macro_input};
6
7use crate::generator::{ApplicationGenerator, CommandGenerator, Generator};
8use crate::inventory::InventoryGenerator;
9
10/// Code generators shared by the `#[command]` and `#[application]` macros
11mod generator;
12/// Compile-time code that links a subcommand to its parent module
13mod inventory;
14
15/// Writes an informational message via the `Output` on `Context`
16///
17/// Expands to `context.output().message(format!(...)).await.expect("event channel closed")`,
18/// where `context` resolves to the local variable in the calling function's scope. This is the
19/// macro equivalent of `Output::message`.
20///
21/// The macro must be called from an async function because the core `Output` methods are async.
22///
23/// # Examples
24///
25/// ```rust,ignore
26/// use clawless::prelude::*;
27///
28/// #[command]
29/// pub async fn deploy(args: DeployArgs, context: Context) -> CommandResult {
30///     message!("deploying to {}", args.target);
31///     Ok(())
32/// }
33/// ```
34#[proc_macro]
35pub fn message(input: TokenStream) -> TokenStream {
36    output_format_macro(input, "message")
37}
38
39/// Writes a supplementary detail via the `Output` on `Context`
40///
41/// Expands to `context.output().detail(format!(...)).await.expect("event channel closed")`,
42/// where `context` resolves to the local variable in the calling function's scope. Detail is only
43/// shown when the user passes `--verbose`. This is the macro equivalent of `Output::detail`.
44///
45/// The macro must be called from an async function because the core `Output` methods are async.
46///
47/// # Examples
48///
49/// ```rust,ignore
50/// use clawless::prelude::*;
51///
52/// #[command]
53/// pub async fn deploy(args: DeployArgs, context: Context) -> CommandResult {
54///     detail!("config loaded from {}", args.config_path);
55///     Ok(())
56/// }
57/// ```
58#[proc_macro]
59pub fn detail(input: TokenStream) -> TokenStream {
60    output_format_macro(input, "detail")
61}
62
63/// Writes an artifact value via the `Output` on `Context`
64///
65/// Expands to `context.output().artifact(...).await.expect("event channel closed")`, where
66/// `context` resolves to the local variable in the calling function's scope. Unlike `message!`
67/// and `detail!`, this macro does not use `format!` — it takes an expression that implements
68/// `Display`, `Serialize`, and `Debug`. This is the macro equivalent of `Output::artifact`.
69///
70/// The macro must be called from an async function because the core `Output` methods are async.
71///
72/// # Examples
73///
74/// ```rust,ignore
75/// use clawless::prelude::*;
76///
77/// #[command]
78/// pub async fn count(args: CountArgs, context: Context) -> CommandResult {
79///     let count = WordCount { words: 42 };
80///     artifact!(count);
81///     Ok(())
82/// }
83/// ```
84#[proc_macro]
85pub fn artifact(input: TokenStream) -> TokenStream {
86    let input = proc_macro2::TokenStream::from(input);
87    let context = proc_macro2::Ident::new("context", proc_macro2::Span::call_site());
88    quote! {
89        #context.output().artifact(#input)
90            .await
91            .expect("event channel closed")
92    }
93    .into()
94}
95
96/// Expands one of the output macros into a call on the local `context`
97///
98/// The `message!`, `detail!`, and `artifact!` macros differ only in the `Output` method that
99/// they call. They therefore share one expansion.
100///
101/// This function builds the `context` identifier with [`Span::call_site`]. The identifier then
102/// resolves to the local variable of the caller, not to a hygienic name.
103///
104/// [`Span::call_site`]: proc_macro2::Span::call_site
105fn output_format_macro(input: TokenStream, method: &str) -> TokenStream {
106    let input = proc_macro2::TokenStream::from(input);
107    let context = proc_macro2::Ident::new("context", proc_macro2::Span::call_site());
108    let method = proc_macro2::Ident::new(method, proc_macro2::Span::call_site());
109    quote! {
110        #context.output().#method(format!(#input))
111            .await
112            .expect("event channel closed")
113    }
114    .into()
115}
116
117/// Set up the commands module for a Clawless application
118///
119/// This macro generates the root command for the command-line application and allows subcommands to
120/// be registered under it. It should be called inside `src/commands.rs` or `src/commands/mod.rs` to
121/// follow Clawless's convention.
122///
123/// # Example
124///
125/// ```rust,ignore
126/// // src/commands.rs
127/// mod greet;
128/// mod deploy;
129///
130/// clawless::commands!();
131/// ```
132#[proc_macro]
133pub fn commands(_input: TokenStream) -> TokenStream {
134    let output = quote! {
135        use clawless::prelude::*;
136        #[derive(Debug, clawless::clap::Args)]
137        struct ClawlessEntryPoint {}
138
139        #[clawless::command(require_subcommand, root = true)]
140        pub async fn clawless(_args: ClawlessEntryPoint, _context: clawless::context::Context) -> clawless::CommandResult {
141            Ok(())
142        }
143    };
144    output.into()
145}
146
147/// Initialize and run a Clawless application
148///
149/// This macro generates the `main` function for a Clawless application. It uses two-phase
150/// dispatch: first it parses arguments and resolves the subcommand tree to find the leaf, then
151/// it matches on the `ResolvedLeaf` variant to delegate to the appropriate runner.
152///
153/// # Example
154///
155/// ```rust,ignore
156/// // src/main.rs
157/// mod commands;
158///
159/// clawless::main!();
160/// ```
161#[proc_macro]
162pub fn main(_input: TokenStream) -> TokenStream {
163    let output = quote! {
164        fn main() -> Result<(), Box<dyn std::error::Error>> {
165            let app = clawless::output::OutputFlags::augment_command(commands::clawless_init());
166            let matches = app.get_matches();
167            let leaf = commands::clawless_resolve(matches);
168
169            match leaf {
170                clawless::resolved_leaf::ResolvedLeaf::Command { matches, exec } => {
171                    clawless::runner::CommandRunner::run(matches, exec)
172                }
173                clawless::resolved_leaf::ResolvedLeaf::Application { matches, exec } => {
174                    clawless::tui::runner::ApplicationRunner::run(matches, exec)
175                }
176            }
177        }
178    };
179    output.into()
180}
181
182/// Add a command to a Clawless application
183///
184/// This macro attribute can be used to register a function as a (sub)command in
185/// a Clawless application. The name of the function will be used as the name of
186/// the command, and it will be automatically registered as a subcommand under
187/// its parent module.
188///
189/// Command functions must accept exactly two parameters:
190/// 1. An `args` parameter: a `clap::Args` struct with the command's arguments
191/// 2. A `context` parameter: the `Context` providing access to the application environment
192///    and the cancellation token for cooperative shutdown
193///
194/// # Attributes
195///
196/// - `alias = "name"` - Add a visible alias for the command. Can be repeated for multiple aliases.
197/// - `require_subcommand` - Require a subcommand; show help if the command is invoked without one.
198///
199/// # Requiring Subcommands
200///
201/// Use `require_subcommand` to create a command that serves as a container for subcommands. When
202/// this attribute is set, invoking the command without a subcommand will display help instead of
203/// running the command body. This is useful for organizing related commands under a common prefix.
204///
205/// For example, a CLI might have `db migrate`, `db seed`, and `db reset` commands, where `db`
206/// itself requires a subcommand and doesn't perform any action on its own.
207///
208/// # Examples
209///
210/// Basic command:
211///
212/// ```rust,ignore
213/// use clawless::prelude::*;
214///
215/// #[derive(Debug, Args)]
216/// pub struct GreetArgs {
217///     #[arg(short, long)]
218///     name: String,
219/// }
220///
221/// #[command]
222/// pub async fn greet(args: GreetArgs, context: Context) -> CommandResult {
223///     message!("Hello, {}!", args.name);
224///     Ok(())
225/// }
226/// ```
227///
228/// Command with alias:
229///
230/// ```rust,ignore
231/// use clawless::prelude::*;
232///
233/// #[derive(Debug, Args)]
234/// pub struct GenerateArgs {}
235///
236/// // Users can run `mycli generate` or `mycli g`
237/// #[command(alias = "g")]
238/// pub async fn generate(args: GenerateArgs, context: Context) -> CommandResult {
239///     Ok(())
240/// }
241/// ```
242///
243/// Command that requires a subcommand:
244///
245/// ```rust,ignore
246/// use clawless::prelude::*;
247///
248/// #[derive(Debug, Args)]
249/// pub struct DbArgs {}
250///
251/// // Running `mycli db` shows help; users must specify a subcommand like `mycli db migrate`
252/// #[command(require_subcommand, alias = "d")]
253/// pub async fn db(args: DbArgs, context: Context) -> CommandResult {
254///     Ok(())
255/// }
256/// ```
257#[proc_macro_attribute]
258pub fn command(attrs: TokenStream, input: TokenStream) -> TokenStream {
259    let input_function = parse_macro_input!(input as ItemFn);
260
261    let command_generator = match CommandGenerator::new(attrs.into(), input_function.clone()) {
262        Ok(generator) => generator,
263        Err(e) => return e.into_compile_error().into(),
264    };
265    let inventory_generator = InventoryGenerator::new(&command_generator);
266
267    let inventory_struct_for_subcommands = inventory_generator.inventory();
268    let submit_command_to_inventory = inventory_generator.submit();
269
270    let initialization_function_for_command = command_generator.initialization_function();
271    let resolve_function_for_command = command_generator.resolve_function();
272
273    let output = quote! {
274        #inventory_struct_for_subcommands
275
276        #input_function
277
278        #initialization_function_for_command
279
280        #resolve_function_for_command
281
282        #submit_command_to_inventory
283    };
284
285    output.into()
286}
287
288/// Add a TUI application to a Clawless project
289///
290/// This macro attribute registers a function as a TUI application in a Clawless project. Unlike
291/// `#[command]`, which creates a stateless CLI command rendered through a push-based presenter,
292/// `#[application]` creates a stateful TUI application that queries a pull-based projection.
293///
294/// Application functions must accept exactly three parameters:
295/// 1. An `args` parameter: a `clap::Args` struct with the application's arguments
296/// 2. A `context` parameter: the `Context` for emitting events and cooperative shutdown
297/// 3. A `projection` parameter: the `Projection` for querying accumulated state
298///
299/// # Attributes
300///
301/// - `alias = "name"` - Add a visible alias for the application. Can be repeated.
302/// - `require_subcommand` - Require a subcommand; show help if invoked without one.
303///
304/// # Examples
305///
306/// ```rust,ignore
307/// use clawless::prelude::*;
308/// use clawless::tui::projection::Projection;
309///
310/// #[derive(Debug, Args)]
311/// pub struct DashboardArgs {
312///     #[arg(short, long, default_value = "3000")]
313///     port: u16,
314/// }
315///
316/// /// Interactive project dashboard
317/// #[application]
318/// pub async fn dashboard(
319///     args: DashboardArgs,
320///     context: Context,
321///     projection: Projection,
322/// ) -> CommandResult {
323///     Ok(())
324/// }
325/// ```
326///
327#[proc_macro_attribute]
328pub fn application(attrs: TokenStream, input: TokenStream) -> TokenStream {
329    let input_function = parse_macro_input!(input as ItemFn);
330
331    let application_generator =
332        match ApplicationGenerator::new(attrs.into(), input_function.clone()) {
333            Ok(generator) => generator,
334            Err(e) => return e.into_compile_error().into(),
335        };
336    let inventory_generator = InventoryGenerator::new(&application_generator);
337
338    let inventory_struct_for_subcommands = inventory_generator.inventory();
339    let submit_application_to_inventory = inventory_generator.submit();
340
341    let initialization_function = application_generator.initialization_function();
342    let resolve_function = application_generator.resolve_function();
343
344    let output = quote! {
345        #inventory_struct_for_subcommands
346
347        #input_function
348
349        #initialization_function
350
351        #resolve_function
352
353        #submit_application_to_inventory
354    };
355
356    output.into()
357}