Skip to main content

clawless_derive/
lib.rs

1#![cfg_attr(not(doctest),doc = include_str!("../README.md"))]
2#![warn(missing_docs)]
3
4use proc_macro::TokenStream;
5use quote::quote;
6use syn::{ItemFn, parse_macro_input};
7
8use crate::command::CommandGenerator;
9use crate::inventory::InventoryGenerator;
10
11mod command;
12mod inventory;
13
14/// Set up the commands module for a Clawless application
15///
16/// This macro generates the root command for the command-line application and allows subcommands to
17/// be registered under it. It should be called inside `src/commands.rs` or `src/commands/mod.rs` to
18/// follow Clawless's convention.
19///
20/// # Example
21///
22/// ```rust,ignore
23/// // src/commands.rs
24/// mod greet;
25/// mod deploy;
26///
27/// clawless::commands!();
28/// ```
29#[proc_macro]
30pub fn commands(_input: TokenStream) -> TokenStream {
31    let output = quote! {
32        use clawless::prelude::*;
33        #[derive(Debug, clawless::clap::Args)]
34        struct ClawlessEntryPoint {}
35
36        #[clawless::command(require_subcommand, root = true)]
37        async fn clawless(_args: ClawlessEntryPoint, _context: clawless::context::Context) -> clawless::CommandResult {
38            Ok(())
39        }
40    };
41    output.into()
42}
43
44/// Initialize and run a Clawless application
45///
46/// This macro generates the `main` function for a Clawless application.
47/// It should be called in `src/main.rs` after declaring the `commands` module.
48///
49/// # Example
50///
51/// ```rust,ignore
52/// // src/main.rs
53/// mod commands;
54///
55/// clawless::main!();
56/// ```
57#[proc_macro]
58pub fn main(_input: TokenStream) -> TokenStream {
59    let output = quote! {
60        fn main() -> Result<(), Box<dyn std::error::Error>> {
61            let cancellation = clawless::cancellation::Cancellation::new();
62            let context = clawless::context::Context::try_new(cancellation.clone())?;
63
64            let rt = clawless::tokio::runtime::Runtime::new()?;
65            rt.block_on(async {
66                clawless::tokio::spawn(
67                    clawless::signal::wait_for_shutdown(cancellation)
68                );
69
70                let app = commands::clawless_init();
71                commands::clawless_exec(app.get_matches(), context).await
72            })?;
73
74            Ok(())
75        }
76    };
77    output.into()
78}
79
80/// Add a command to a Clawless application
81///
82/// This macro attribute can be used to register a function as a (sub)command in
83/// a Clawless application. The name of the function will be used as the name of
84/// the command, and it will be automatically registered as a subcommand under
85/// its parent module.
86///
87/// Command functions must accept exactly two parameters:
88/// 1. An `args` parameter: a `clap::Args` struct with the command's arguments
89/// 2. A `context` parameter: the `Context` providing access to the application environment
90///    and the cancellation token for cooperative shutdown
91///
92/// # Attributes
93///
94/// - `alias = "name"` - Add a visible alias for the command. Can be repeated for multiple aliases.
95/// - `require_subcommand` - Require a subcommand; show help if the command is invoked without one.
96///
97/// # Requiring Subcommands
98///
99/// Use `require_subcommand` to create a command that serves as a container for subcommands. When
100/// this attribute is set, invoking the command without a subcommand will display help instead of
101/// running the command body. This is useful for organizing related commands under a common prefix.
102///
103/// For example, a CLI might have `db migrate`, `db seed`, and `db reset` commands, where `db`
104/// itself requires a subcommand and doesn't perform any action on its own.
105///
106/// # Examples
107///
108/// Basic command:
109///
110/// ```rust,ignore
111/// use clawless::prelude::*;
112///
113/// #[derive(Debug, Args)]
114/// pub struct GreetArgs {
115///     #[arg(short, long)]
116///     name: String,
117/// }
118///
119/// #[command]
120/// pub async fn greet(args: GreetArgs, context: Context) -> CommandResult {
121///     println!("Hello, {}!", args.name);
122///     Ok(())
123/// }
124/// ```
125///
126/// Command with alias:
127///
128/// ```rust,ignore
129/// use clawless::prelude::*;
130///
131/// #[derive(Debug, Args)]
132/// pub struct GenerateArgs {}
133///
134/// // Users can run `mycli generate` or `mycli g`
135/// #[command(alias = "g")]
136/// pub async fn generate(args: GenerateArgs, context: Context) -> CommandResult {
137///     Ok(())
138/// }
139/// ```
140///
141/// Command that requires a subcommand:
142///
143/// ```rust,ignore
144/// use clawless::prelude::*;
145///
146/// #[derive(Debug, Args)]
147/// pub struct DbArgs {}
148///
149/// // Running `mycli db` shows help; users must specify a subcommand like `mycli db migrate`
150/// #[command(require_subcommand, alias = "d")]
151/// pub async fn db(args: DbArgs, context: Context) -> CommandResult {
152///     Ok(())
153/// }
154/// ```
155#[proc_macro_attribute]
156pub fn command(attrs: TokenStream, input: TokenStream) -> TokenStream {
157    let input_function = parse_macro_input!(input as ItemFn);
158
159    let command_generator = match CommandGenerator::new(attrs.into(), input_function.clone()) {
160        Ok(generator) => generator,
161        Err(e) => return e.into_compile_error().into(),
162    };
163    let inventory_generator = InventoryGenerator::new(&command_generator);
164
165    let inventory_struct_for_subcommands = inventory_generator.inventory();
166    let submit_command_to_inventory = inventory_generator.submit_command();
167
168    let initialization_function_for_command = command_generator.initialization_function();
169    let wrapper_function_for_command = command_generator.wrapper_function();
170
171    let output = quote! {
172        #inventory_struct_for_subcommands
173
174        #input_function
175
176        #initialization_function_for_command
177
178        #wrapper_function_for_command
179
180        #submit_command_to_inventory
181    };
182
183    output.into()
184}