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