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}