Skip to main content

standout_macros/
lib.rs

1//! Proc macros for Standout.
2//!
3//! This crate provides macros for compile-time resource embedding and
4//! declarative command dispatch configuration.
5//!
6//! # Available Macros
7//!
8//! ## Embedding Macros
9//!
10//! - [`embed_templates!`] - Embed template files (`.jinja`, `.jinja2`, `.j2`, `.txt`)
11//! - [`embed_styles!`] - Embed stylesheet files (`.css`, `.yaml`, `.yml`)
12//!
13//! ## Derive Macros
14//!
15//! - [`Dispatch`] - Generate dispatch configuration from clap `Subcommand` enums
16//! - [`Tabular`] - Generate `TabularSpec` from struct field annotations
17//! - [`TabularRow`] - Generate optimized row extraction without JSON serialization
18//! - [`Seekable`] - Generate query-enabled accessor functions for Seeker
19//!
20//! ## Attribute Macros
21//!
22//! - [`handler`] - Transform pure functions into Standout-compatible handlers
23//!
24//! # Design Philosophy
25//!
26//! These macros return [`EmbeddedSource`] types that contain:
27//!
28//! 1. Embedded content (baked into binary at compile time)
29//! 2. Source path (for debug hot-reload)
30//!
31//! This design enables:
32//!
33//! - Release builds: Use embedded content, zero file I/O
34//! - Debug builds: Hot-reload from disk if source path exists
35//!
36//! # Examples
37//!
38//! For working examples, see:
39//! - `standout/tests/embed_macros.rs` - embedding macros
40//! - `standout/tests/dispatch_derive.rs` - dispatch derive macro
41//!
42//! [`EmbeddedSource`]: standout::EmbeddedSource
43//! [`RenderSetup`]: standout::RenderSetup
44
45mod command;
46mod dispatch;
47mod embed;
48mod handler;
49mod seeker;
50mod tabular;
51
52use proc_macro::TokenStream;
53use syn::{parse_macro_input, DeriveInput, LitStr};
54
55/// Embeds all template files from a directory at compile time.
56///
57/// This macro walks the specified directory, reads all files with recognized
58/// template extensions, and returns an [`EmbeddedTemplates`] source that can
59/// be used with [`RenderSetup`] or converted to a [`TemplateRegistry`].
60///
61/// # Supported Extensions
62///
63/// Files are recognized by extension (in priority order):
64/// - `.jinja` (highest priority)
65/// - `.jinja2`
66/// - `.j2`
67/// - `.txt` (lowest priority)
68///
69/// When multiple files share the same base name with different extensions
70/// (e.g., `config.jinja` and `config.txt`), the higher-priority extension wins
71/// for extensionless lookups.
72///
73/// # Hot Reload Behavior
74///
75/// - Release builds: Uses embedded content (zero file I/O)
76/// - Debug builds: Reads from disk if source path exists (hot-reload)
77///
78/// For working examples, see `standout/tests/embed_macros.rs`.
79///
80/// # Compile-Time Errors
81///
82/// The macro will fail to compile if:
83/// - The directory doesn't exist
84/// - The directory is not readable
85/// - Any file content is not valid UTF-8
86///
87/// [`EmbeddedTemplates`]: standout::EmbeddedTemplates
88/// [`RenderSetup`]: standout::RenderSetup
89/// [`TemplateRegistry`]: standout::TemplateRegistry
90#[proc_macro]
91pub fn embed_templates(input: TokenStream) -> TokenStream {
92    let path_lit = parse_macro_input!(input as LitStr);
93    embed::embed_templates_impl(path_lit).into()
94}
95
96/// Embeds all stylesheet files from a directory at compile time.
97///
98/// This macro walks the specified directory, reads all files with recognized
99/// stylesheet extensions, and returns an [`EmbeddedStyles`] source that can
100/// be used with [`RenderSetup`] or converted to a [`StylesheetRegistry`].
101///
102/// # Supported Extensions
103///
104/// Files are recognized by extension (in priority order):
105/// - `.css` (highest priority — preferred format)
106/// - `.yaml` (legacy)
107/// - `.yml` (legacy, lowest priority)
108///
109/// The format is auto-detected from the content itself at parse time, so the
110/// extension only matters for file discovery and priority resolution.
111///
112/// When multiple files share the same base name with different extensions
113/// (e.g., `dark.css` and `dark.yaml`), the higher-priority extension wins.
114///
115/// # Hot Reload Behavior
116///
117/// - Release builds: Uses embedded content (zero file I/O)
118/// - Debug builds: Reads from disk if source path exists (hot-reload)
119///
120/// For working examples, see `standout/tests/embed_macros.rs`.
121///
122/// # Compile-Time Errors
123///
124/// The macro will fail to compile if:
125/// - The directory doesn't exist
126/// - The directory is not readable
127/// - Any file content is not valid UTF-8
128///
129/// [`EmbeddedStyles`]: standout::EmbeddedStyles
130/// [`RenderSetup`]: standout::RenderSetup
131/// [`StylesheetRegistry`]: standout::StylesheetRegistry
132#[proc_macro]
133pub fn embed_styles(input: TokenStream) -> TokenStream {
134    let path_lit = parse_macro_input!(input as LitStr);
135    embed::embed_styles_impl(path_lit).into()
136}
137
138/// Derives dispatch configuration from a clap `Subcommand` enum.
139///
140/// This macro eliminates boilerplate command-to-handler mappings by using
141/// naming conventions with explicit overrides when needed.
142///
143/// For working examples, see `standout/tests/dispatch_derive.rs`.
144///
145/// # Convention-Based Defaults
146///
147/// - Handler: `{handlers_module}::{variant_snake_case}`
148///   - `Add` → `handlers::add`
149///   - `ListAll` → `handlers::list_all`
150/// - Template: `{variant_snake_case}.j2`
151///
152/// # Container Attributes
153///
154/// | Attribute | Required | Description |
155/// |-----------|----------|-------------|
156/// | `handlers = path` | Yes | Module containing handler functions |
157///
158/// # Variant Attributes
159///
160/// | Attribute | Description | Default |
161/// |-----------|-------------|---------|
162/// | `handler = path` | Handler function | `{handlers}::{snake_case}` |
163/// | `template = "path"` | Template file | `{snake_case}.j2` |
164/// | `pre_dispatch = fn` | Pre-dispatch hook | None |
165/// | `post_dispatch = fn` | Post-dispatch hook | None |
166/// | `post_output = fn` | Post-output hook | None |
167/// | `nested` | Treat as nested subcommand | false |
168/// | `skip` | Skip this variant | false |
169///
170/// # Generated Code
171///
172/// Generates a `dispatch_config()` method returning a closure for
173/// use with `App::builder().commands()`.
174#[proc_macro_derive(Dispatch, attributes(dispatch))]
175pub fn dispatch_derive(input: TokenStream) -> TokenStream {
176    let input = parse_macro_input!(input as DeriveInput);
177    dispatch::dispatch_derive_impl(input)
178        .unwrap_or_else(|e| e.to_compile_error())
179        .into()
180}
181
182/// Derives a `TabularSpec` from struct field annotations.
183///
184/// This macro generates an implementation of the `Tabular` trait, which provides
185/// a `tabular_spec()` method that returns a `TabularSpec` for the struct.
186///
187/// For working examples, see `standout/tests/tabular_derive.rs`.
188///
189/// # Field Attributes
190///
191/// | Attribute | Type | Description |
192/// |-----------|------|-------------|
193/// | `width` | `usize` or `"fill"` or `"Nfr"` | Column width strategy |
194/// | `min` | `usize` | Minimum width (for bounded) |
195/// | `max` | `usize` | Maximum width (for bounded) |
196/// | `align` | `"left"`, `"right"`, `"center"` | Text alignment |
197/// | `anchor` | `"left"`, `"right"` | Column position |
198/// | `overflow` | `"truncate"`, `"wrap"`, `"clip"`, `"expand"` | Overflow handling |
199/// | `truncate_at` | `"end"`, `"start"`, `"middle"` | Truncation position |
200/// | `style` | string | Style name for the column |
201/// | `style_from_value` | flag | Use cell value as style name |
202/// | `header` | string | Header title (default: field name) |
203/// | `null_repr` | string | Representation for null values |
204/// | `key` | string | Data extraction key (supports dot notation) |
205/// | `skip` | flag | Exclude this field from the spec |
206///
207/// # Container Attributes
208///
209/// | Attribute | Type | Description |
210/// |-----------|------|-------------|
211/// | `separator` | string | Column separator (default: "  ") |
212/// | `prefix` | string | Row prefix |
213/// | `suffix` | string | Row suffix |
214///
215/// # Example
216///
217/// ```ignore
218/// use standout::tabular::Tabular;
219/// use serde::Serialize;
220///
221/// #[derive(Serialize, Tabular)]
222/// #[tabular(separator = " │ ")]
223/// struct Task {
224///     #[col(width = 8, style = "muted")]
225///     id: String,
226///
227///     #[col(width = "fill", overflow = "wrap")]
228///     title: String,
229///
230///     #[col(width = 12, align = "right")]
231///     status: String,
232/// }
233///
234/// let spec = Task::tabular_spec();
235/// ```
236#[proc_macro_derive(Tabular, attributes(col, tabular))]
237pub fn tabular_derive(input: TokenStream) -> TokenStream {
238    let input = parse_macro_input!(input as DeriveInput);
239    tabular::tabular_derive_impl(input)
240        .unwrap_or_else(|e| e.to_compile_error())
241        .into()
242}
243
244/// Derives optimized row extraction for tabular formatting.
245///
246/// This macro generates an implementation of the `TabularRow` trait, which provides
247/// a `to_row()` method that converts the struct to a `Vec<String>` without JSON serialization.
248///
249/// For working examples, see `standout/tests/tabular_derive.rs`.
250///
251/// # Field Attributes
252///
253/// | Attribute | Description |
254/// |-----------|-------------|
255/// | `skip` | Exclude this field from the row |
256///
257/// # Example
258///
259/// ```ignore
260/// use standout::tabular::TabularRow;
261///
262/// #[derive(TabularRow)]
263/// struct Task {
264///     id: String,
265///     title: String,
266///
267///     #[col(skip)]
268///     internal_state: u32,
269///
270///     status: String,
271/// }
272///
273/// let task = Task {
274///     id: "TSK-001".to_string(),
275///     title: "Implement feature".to_string(),
276///     internal_state: 42,
277///     status: "pending".to_string(),
278/// };
279///
280/// let row = task.to_row();
281/// assert_eq!(row, vec!["TSK-001", "Implement feature", "pending"]);
282/// ```
283#[proc_macro_derive(TabularRow, attributes(col))]
284pub fn tabular_row_derive(input: TokenStream) -> TokenStream {
285    let input = parse_macro_input!(input as DeriveInput);
286    tabular::tabular_row_derive_impl(input)
287        .unwrap_or_else(|e| e.to_compile_error())
288        .into()
289}
290
291/// Derives the `Seekable` trait for query-enabled structs.
292///
293/// This macro generates an implementation of the `Seekable` trait from
294/// `standout-seeker`, enabling type-safe field access for query operations.
295///
296/// # Field Attributes
297///
298/// | Attribute | Description |
299/// |-----------|-------------|
300/// | `String` | String field (supports Eq, Ne, Contains, StartsWith, EndsWith, Regex) |
301/// | `Number` | Numeric field (supports Eq, Ne, Gt, Gte, Lt, Lte) |
302/// | `Timestamp` | Timestamp field (supports Eq, Ne, Before, After, Gt, Gte, Lt, Lte) |
303/// | `Enum` | Enum field (supports Eq, Ne, In) - requires `SeekerEnum` impl |
304/// | `Bool` | Boolean field (supports Eq, Ne, Is) |
305/// | `skip` | Exclude this field from queries |
306/// | `rename = "..."` | Use a custom name for queries |
307///
308/// # Generated Code
309///
310/// The macro generates:
311///
312/// 1. Field name constants (e.g., `Task::NAME`, `Task::PRIORITY`)
313/// 2. Implementation of `Seekable::seeker_field_value()`
314///
315/// # Example
316///
317/// ```ignore
318/// use standout_macros::Seekable;
319/// use standout_seeker::{Query, Seekable};
320///
321/// #[derive(Seekable)]
322/// struct Task {
323/// struct Task {
324///     #[seek(String)]
325///     name: String,
326///
327///     #[seek(Number)]
328///     priority: u8,
329///
330///     #[seek(Bool)]
331///     done: bool,
332///
333///     #[seek(skip)]
334///     internal_id: u64,
335/// }
336///
337/// let tasks = vec![
338///     Task { name: "Write docs".into(), priority: 3, done: false, internal_id: 1 },
339///     Task { name: "Fix bug".into(), priority: 5, done: true, internal_id: 2 },
340/// ];
341///
342/// let query = Query::new()
343///     .and_gte(Task::PRIORITY, 3u8)
344///     .not_eq(Task::DONE, true)
345///     .build();
346///
347/// let results = query.filter(&tasks, Task::accessor);
348/// assert_eq!(results.len(), 1);
349/// assert_eq!(results[0].name, "Write docs");
350/// ```
351///
352/// # Enum Fields
353///
354/// For enum fields, implement `SeekerEnum` on your enum type:
355///
356/// ```ignore
357/// use standout_seeker::SeekerEnum;
358///
359/// #[derive(Clone, Copy)]
360/// enum Status { Pending, Active, Done }
361///
362/// impl SeekerEnum for Status {
363///     fn seeker_discriminant(&self) -> u32 {
364///         match self {
365///             Status::Pending => 0,
366///             Status::Active => 1,
367///             Status::Done => 2,
368///         }
369///     }
370/// }
371///
372/// #[derive(Seekable)]
373/// struct Task {
374///     #[seek(Enum)]
375///     status: Status,
376/// }
377/// ```
378///
379/// # Timestamp Fields
380///
381/// For timestamp fields, implement `SeekerTimestamp` on your datetime type:
382///
383/// ```ignore
384/// use standout_seeker::{SeekerTimestamp, Timestamp};
385///
386/// struct MyDateTime(i64);
387///
388/// impl SeekerTimestamp for MyDateTime {
389///     fn seeker_timestamp(&self) -> Timestamp {
390///         Timestamp::from_millis(self.0)
391///     }
392/// }
393///
394/// #[derive(Seekable)]
395/// struct Event {
396///     #[seek(Timestamp)]
397///     created_at: MyDateTime,
398/// }
399/// ```
400#[proc_macro_derive(Seekable, attributes(seek))]
401pub fn seekable_derive(input: TokenStream) -> TokenStream {
402    let input = parse_macro_input!(input as DeriveInput);
403    seeker::seekable_derive_impl(input)
404        .unwrap_or_else(|e| e.to_compile_error())
405        .into()
406}
407
408/// Transforms a pure function into a Standout-compatible handler.
409///
410/// This macro generates a wrapper function that extracts CLI arguments from
411/// `ArgMatches` and calls your pure function. The original function is preserved
412/// for direct testing.
413///
414/// # Parameter Annotations
415///
416/// | Annotation | Type | Description |
417/// |------------|------|-------------|
418/// | `#[flag]` | `bool` | Boolean CLI flag |
419/// | `#[flag(name = "x")]` | `bool` | Flag with custom CLI name |
420/// | `#[arg]` | `T` | Required CLI argument |
421/// | `#[arg]` | `Option<T>` | Optional CLI argument |
422/// | `#[arg]` | `Vec<T>` | Multiple CLI arguments |
423/// | `#[arg(name = "x")]` | `T` | Argument with custom CLI name |
424/// | `#[ctx]` | `&CommandContext` | Access to command context |
425/// | `#[matches]` | `&ArgMatches` | Raw matches (escape hatch) |
426///
427/// # Return Type Handling
428///
429/// | Return Type | Behavior |
430/// |-------------|----------|
431/// | `Result<T, E>` | Passed through (dispatch auto-wraps in Output::Render) |
432/// | `Result<(), E>` | Wrapped in `HandlerResult<()>` with `Output::Silent` |
433///
434/// # Generated Code
435///
436/// For a function `fn foo(...)`, the macro generates `fn foo__handler(...)`.
437///
438/// # Example
439///
440/// ```rust,ignore
441/// use standout_macros::handler;
442///
443/// // Pure function - easy to test
444/// #[handler]
445/// pub fn list(#[flag] all: bool, #[arg] limit: Option<usize>) -> Result<Vec<Item>, Error> {
446///     storage::list(all, limit)
447/// }
448///
449/// // Generates:
450/// // pub fn list__handler(m: &ArgMatches) -> Result<Vec<Item>, Error> {
451/// //     let all = m.get_flag("all");
452/// //     let limit = m.get_one::<usize>("limit").cloned();
453/// //     list(all, limit)
454/// // }
455///
456/// // Use with Dispatch derive:
457/// #[derive(Subcommand, Dispatch)]
458/// #[dispatch(handlers = handlers)]
459/// enum Commands {
460///     #[dispatch(handler = list)]  // Uses list__handler
461///     List { ... },
462/// }
463/// ```
464///
465/// # Testing
466///
467/// The original function is preserved, so you can test it directly:
468///
469/// ```rust,ignore
470/// #[test]
471/// fn test_list() {
472///     let result = list(true, Some(10));
473///     assert!(result.is_ok());
474/// }
475/// ```
476#[proc_macro_attribute]
477pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
478    let attr = proc_macro2::TokenStream::from(attr);
479    let item = proc_macro2::TokenStream::from(item);
480    handler::handler_impl(attr, item)
481        .unwrap_or_else(|e| e.to_compile_error())
482        .into()
483}
484
485/// Defines a complete command with handler, clap Command, and template from a single source.
486///
487/// This macro extends `#[handler]` to generate both the handler wrapper AND the complete
488/// clap `Command` definition. This eliminates mismatches between handler expectations
489/// and CLI definitions since everything is derived from one source.
490///
491/// # Command Attributes
492///
493/// | Attribute | Type | Required | Description |
494/// |-----------|------|----------|-------------|
495/// | `name` | string | Yes | Command name |
496/// | `about` | string | No | Short description |
497/// | `long_about` | string | No | Detailed description |
498/// | `visible_alias` | string | No | Command alias |
499/// | `hide` | bool | No | Hide from help |
500/// | `template` | string | No | Template name (defaults to command name) |
501///
502/// # Parameter Annotations
503///
504/// ## Flags (`#[flag(...)]`)
505///
506/// | Attribute | Type | Description |
507/// |-----------|------|-------------|
508/// | `short` | char | Short flag (e.g., `-a`) |
509/// | `long` | string | Long flag, defaults to param name with `_` → `-` |
510/// | `help` | string | Help text |
511/// | `hide` | bool | Hide from help |
512///
513/// ## Arguments (`#[arg(...)]`)
514///
515/// | Attribute | Type | Description |
516/// |-----------|------|-------------|
517/// | `short` | char | Short option (e.g., `-f`) |
518/// | `long` | string | Long option, defaults to param name with `_` → `-` |
519/// | `help` | string | Help text |
520/// | `value_name` | string | Placeholder in help |
521/// | `default` | string | Default value |
522/// | `hide` | bool | Hide from help |
523/// | `positional` | bool | Positional argument (no `--` prefix) |
524///
525/// ## Pass-through
526///
527/// | Annotation | Type | Description |
528/// |------------|------|-------------|
529/// | `#[ctx]` | `&CommandContext` | Access command context |
530/// | `#[matches]` | `&ArgMatches` | Access raw matches |
531///
532/// # Generated Code
533///
534/// For a function `fn foo(...)`, the macro generates:
535///
536/// - `fn foo(...)` - original function (preserved for testing)
537/// - `fn foo__handler(...)` - wrapper for dispatch
538/// - `fn foo__expected_args()` - verification metadata
539/// - `fn foo__command()` - clap `Command` definition
540/// - `fn foo__template()` - template name
541/// - `struct foo_Handler` - implements `Handler` trait
542///
543/// # Template Convention
544///
545/// The `template` attribute is optional. When omitted, it defaults to the command name.
546/// For example, `#[command(name = "list")]` will use template `"list"`.
547///
548/// # Example
549///
550/// ```rust,ignore
551/// use standout_macros::command;
552///
553/// #[command(name = "list", about = "List all items")]
554/// fn list_items(
555///     #[flag(short = 'a', help = "Show all")] all: bool,
556///     #[arg(short = 'f', help = "Filter")] filter: Option<String>,
557/// ) -> Result<Vec<Item>, Error> {
558///     storage::list(all, filter)
559/// }
560///
561/// // Use:
562/// // - list_items__command() returns the clap Command
563/// // - list_items__template() returns "list"
564/// // - list_items_Handler implements Handler
565/// ```
566#[proc_macro_attribute]
567pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream {
568    let attr = proc_macro2::TokenStream::from(attr);
569    let item = proc_macro2::TokenStream::from(item);
570    command::command_impl(attr, item)
571        .unwrap_or_else(|e| e.to_compile_error())
572        .into()
573}