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 (`.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 dispatch;
46mod embed;
47mod handler;
48mod seeker;
49mod tabular;
50
51use proc_macro::TokenStream;
52use syn::{parse_macro_input, DeriveInput, LitStr};
53
54/// Embeds all template files from a directory at compile time.
55///
56/// This macro walks the specified directory, reads all files with recognized
57/// template extensions, and returns an [`EmbeddedTemplates`] source that can
58/// be used with [`RenderSetup`] or converted to a [`TemplateRegistry`].
59///
60/// # Supported Extensions
61///
62/// Files are recognized by extension (in priority order):
63/// - `.jinja` (highest priority)
64/// - `.jinja2`
65/// - `.j2`
66/// - `.txt` (lowest priority)
67///
68/// When multiple files share the same base name with different extensions
69/// (e.g., `config.jinja` and `config.txt`), the higher-priority extension wins
70/// for extensionless lookups.
71///
72/// # Hot Reload Behavior
73///
74/// - Release builds: Uses embedded content (zero file I/O)
75/// - Debug builds: Reads from disk if source path exists (hot-reload)
76///
77/// For working examples, see `standout/tests/embed_macros.rs`.
78///
79/// # Compile-Time Errors
80///
81/// The macro will fail to compile if:
82/// - The directory doesn't exist
83/// - The directory is not readable
84/// - Any file content is not valid UTF-8
85///
86/// [`EmbeddedTemplates`]: standout::EmbeddedTemplates
87/// [`RenderSetup`]: standout::RenderSetup
88/// [`TemplateRegistry`]: standout::TemplateRegistry
89#[proc_macro]
90pub fn embed_templates(input: TokenStream) -> TokenStream {
91    let path_lit = parse_macro_input!(input as LitStr);
92    embed::embed_templates_impl(path_lit).into()
93}
94
95/// Embeds all stylesheet files from a directory at compile time.
96///
97/// This macro walks the specified directory, reads all files with recognized
98/// stylesheet extensions, and returns an [`EmbeddedStyles`] source that can
99/// be used with [`RenderSetup`] or converted to a [`StylesheetRegistry`].
100///
101/// # Supported Extensions
102///
103/// Files are recognized by extension (in priority order):
104/// - `.yaml` (highest priority)
105/// - `.yml` (lowest priority)
106///
107/// When multiple files share the same base name with different extensions
108/// (e.g., `dark.yaml` and `dark.yml`), the higher-priority extension wins.
109///
110/// # Hot Reload Behavior
111///
112/// - Release builds: Uses embedded content (zero file I/O)
113/// - Debug builds: Reads from disk if source path exists (hot-reload)
114///
115/// For working examples, see `standout/tests/embed_macros.rs`.
116///
117/// # Compile-Time Errors
118///
119/// The macro will fail to compile if:
120/// - The directory doesn't exist
121/// - The directory is not readable
122/// - Any file content is not valid UTF-8
123///
124/// [`EmbeddedStyles`]: standout::EmbeddedStyles
125/// [`RenderSetup`]: standout::RenderSetup
126/// [`StylesheetRegistry`]: standout::StylesheetRegistry
127#[proc_macro]
128pub fn embed_styles(input: TokenStream) -> TokenStream {
129    let path_lit = parse_macro_input!(input as LitStr);
130    embed::embed_styles_impl(path_lit).into()
131}
132
133/// Derives dispatch configuration from a clap `Subcommand` enum.
134///
135/// This macro eliminates boilerplate command-to-handler mappings by using
136/// naming conventions with explicit overrides when needed.
137///
138/// For working examples, see `standout/tests/dispatch_derive.rs`.
139///
140/// # Convention-Based Defaults
141///
142/// - Handler: `{handlers_module}::{variant_snake_case}`
143///   - `Add` → `handlers::add`
144///   - `ListAll` → `handlers::list_all`
145/// - Template: `{variant_snake_case}.j2`
146///
147/// # Container Attributes
148///
149/// | Attribute | Required | Description |
150/// |-----------|----------|-------------|
151/// | `handlers = path` | Yes | Module containing handler functions |
152///
153/// # Variant Attributes
154///
155/// | Attribute | Description | Default |
156/// |-----------|-------------|---------|
157/// | `handler = path` | Handler function | `{handlers}::{snake_case}` |
158/// | `template = "path"` | Template file | `{snake_case}.j2` |
159/// | `pre_dispatch = fn` | Pre-dispatch hook | None |
160/// | `post_dispatch = fn` | Post-dispatch hook | None |
161/// | `post_output = fn` | Post-output hook | None |
162/// | `nested` | Treat as nested subcommand | false |
163/// | `skip` | Skip this variant | false |
164///
165/// # Generated Code
166///
167/// Generates a `dispatch_config()` method returning a closure for
168/// use with `App::builder().commands()`.
169#[proc_macro_derive(Dispatch, attributes(dispatch))]
170pub fn dispatch_derive(input: TokenStream) -> TokenStream {
171    let input = parse_macro_input!(input as DeriveInput);
172    dispatch::dispatch_derive_impl(input)
173        .unwrap_or_else(|e| e.to_compile_error())
174        .into()
175}
176
177/// Derives a `TabularSpec` from struct field annotations.
178///
179/// This macro generates an implementation of the `Tabular` trait, which provides
180/// a `tabular_spec()` method that returns a `TabularSpec` for the struct.
181///
182/// For working examples, see `standout/tests/tabular_derive.rs`.
183///
184/// # Field Attributes
185///
186/// | Attribute | Type | Description |
187/// |-----------|------|-------------|
188/// | `width` | `usize` or `"fill"` or `"Nfr"` | Column width strategy |
189/// | `min` | `usize` | Minimum width (for bounded) |
190/// | `max` | `usize` | Maximum width (for bounded) |
191/// | `align` | `"left"`, `"right"`, `"center"` | Text alignment |
192/// | `anchor` | `"left"`, `"right"` | Column position |
193/// | `overflow` | `"truncate"`, `"wrap"`, `"clip"`, `"expand"` | Overflow handling |
194/// | `truncate_at` | `"end"`, `"start"`, `"middle"` | Truncation position |
195/// | `style` | string | Style name for the column |
196/// | `style_from_value` | flag | Use cell value as style name |
197/// | `header` | string | Header title (default: field name) |
198/// | `null_repr` | string | Representation for null values |
199/// | `key` | string | Data extraction key (supports dot notation) |
200/// | `skip` | flag | Exclude this field from the spec |
201///
202/// # Container Attributes
203///
204/// | Attribute | Type | Description |
205/// |-----------|------|-------------|
206/// | `separator` | string | Column separator (default: "  ") |
207/// | `prefix` | string | Row prefix |
208/// | `suffix` | string | Row suffix |
209///
210/// # Example
211///
212/// ```ignore
213/// use standout::tabular::Tabular;
214/// use serde::Serialize;
215///
216/// #[derive(Serialize, Tabular)]
217/// #[tabular(separator = " │ ")]
218/// struct Task {
219///     #[col(width = 8, style = "muted")]
220///     id: String,
221///
222///     #[col(width = "fill", overflow = "wrap")]
223///     title: String,
224///
225///     #[col(width = 12, align = "right")]
226///     status: String,
227/// }
228///
229/// let spec = Task::tabular_spec();
230/// ```
231#[proc_macro_derive(Tabular, attributes(col, tabular))]
232pub fn tabular_derive(input: TokenStream) -> TokenStream {
233    let input = parse_macro_input!(input as DeriveInput);
234    tabular::tabular_derive_impl(input)
235        .unwrap_or_else(|e| e.to_compile_error())
236        .into()
237}
238
239/// Derives optimized row extraction for tabular formatting.
240///
241/// This macro generates an implementation of the `TabularRow` trait, which provides
242/// a `to_row()` method that converts the struct to a `Vec<String>` without JSON serialization.
243///
244/// For working examples, see `standout/tests/tabular_derive.rs`.
245///
246/// # Field Attributes
247///
248/// | Attribute | Description |
249/// |-----------|-------------|
250/// | `skip` | Exclude this field from the row |
251///
252/// # Example
253///
254/// ```ignore
255/// use standout::tabular::TabularRow;
256///
257/// #[derive(TabularRow)]
258/// struct Task {
259///     id: String,
260///     title: String,
261///
262///     #[col(skip)]
263///     internal_state: u32,
264///
265///     status: String,
266/// }
267///
268/// let task = Task {
269///     id: "TSK-001".to_string(),
270///     title: "Implement feature".to_string(),
271///     internal_state: 42,
272///     status: "pending".to_string(),
273/// };
274///
275/// let row = task.to_row();
276/// assert_eq!(row, vec!["TSK-001", "Implement feature", "pending"]);
277/// ```
278#[proc_macro_derive(TabularRow, attributes(col))]
279pub fn tabular_row_derive(input: TokenStream) -> TokenStream {
280    let input = parse_macro_input!(input as DeriveInput);
281    tabular::tabular_row_derive_impl(input)
282        .unwrap_or_else(|e| e.to_compile_error())
283        .into()
284}
285
286/// Derives the `Seekable` trait for query-enabled structs.
287///
288/// This macro generates an implementation of the `Seekable` trait from
289/// `standout-seeker`, enabling type-safe field access for query operations.
290///
291/// # Field Attributes
292///
293/// | Attribute | Description |
294/// |-----------|-------------|
295/// | `String` | String field (supports Eq, Ne, Contains, StartsWith, EndsWith, Regex) |
296/// | `Number` | Numeric field (supports Eq, Ne, Gt, Gte, Lt, Lte) |
297/// | `Timestamp` | Timestamp field (supports Eq, Ne, Before, After, Gt, Gte, Lt, Lte) |
298/// | `Enum` | Enum field (supports Eq, Ne, In) - requires `SeekerEnum` impl |
299/// | `Bool` | Boolean field (supports Eq, Ne, Is) |
300/// | `skip` | Exclude this field from queries |
301/// | `rename = "..."` | Use a custom name for queries |
302///
303/// # Generated Code
304///
305/// The macro generates:
306///
307/// 1. Field name constants (e.g., `Task::NAME`, `Task::PRIORITY`)
308/// 2. Implementation of `Seekable::seeker_field_value()`
309///
310/// # Example
311///
312/// ```ignore
313/// use standout_macros::Seekable;
314/// use standout_seeker::{Query, Seekable};
315///
316/// #[derive(Seekable)]
317/// struct Task {
318/// struct Task {
319///     #[seek(String)]
320///     name: String,
321///
322///     #[seek(Number)]
323///     priority: u8,
324///
325///     #[seek(Bool)]
326///     done: bool,
327///
328///     #[seek(skip)]
329///     internal_id: u64,
330/// }
331///
332/// let tasks = vec![
333///     Task { name: "Write docs".into(), priority: 3, done: false, internal_id: 1 },
334///     Task { name: "Fix bug".into(), priority: 5, done: true, internal_id: 2 },
335/// ];
336///
337/// let query = Query::new()
338///     .and_gte(Task::PRIORITY, 3u8)
339///     .not_eq(Task::DONE, true)
340///     .build();
341///
342/// let results = query.filter(&tasks, Task::accessor);
343/// assert_eq!(results.len(), 1);
344/// assert_eq!(results[0].name, "Write docs");
345/// ```
346///
347/// # Enum Fields
348///
349/// For enum fields, implement `SeekerEnum` on your enum type:
350///
351/// ```ignore
352/// use standout_seeker::SeekerEnum;
353///
354/// #[derive(Clone, Copy)]
355/// enum Status { Pending, Active, Done }
356///
357/// impl SeekerEnum for Status {
358///     fn seeker_discriminant(&self) -> u32 {
359///         match self {
360///             Status::Pending => 0,
361///             Status::Active => 1,
362///             Status::Done => 2,
363///         }
364///     }
365/// }
366///
367/// #[derive(Seekable)]
368/// struct Task {
369///     #[seek(Enum)]
370///     status: Status,
371/// }
372/// ```
373///
374/// # Timestamp Fields
375///
376/// For timestamp fields, implement `SeekerTimestamp` on your datetime type:
377///
378/// ```ignore
379/// use standout_seeker::{SeekerTimestamp, Timestamp};
380///
381/// struct MyDateTime(i64);
382///
383/// impl SeekerTimestamp for MyDateTime {
384///     fn seeker_timestamp(&self) -> Timestamp {
385///         Timestamp::from_millis(self.0)
386///     }
387/// }
388///
389/// #[derive(Seekable)]
390/// struct Event {
391///     #[seek(Timestamp)]
392///     created_at: MyDateTime,
393/// }
394/// ```
395#[proc_macro_derive(Seekable, attributes(seek))]
396pub fn seekable_derive(input: TokenStream) -> TokenStream {
397    let input = parse_macro_input!(input as DeriveInput);
398    seeker::seekable_derive_impl(input)
399        .unwrap_or_else(|e| e.to_compile_error())
400        .into()
401}
402
403/// Transforms a pure function into a Standout-compatible handler.
404///
405/// This macro generates a wrapper function that extracts CLI arguments from
406/// `ArgMatches` and calls your pure function. The original function is preserved
407/// for direct testing.
408///
409/// # Parameter Annotations
410///
411/// | Annotation | Type | Description |
412/// |------------|------|-------------|
413/// | `#[flag]` | `bool` | Boolean CLI flag |
414/// | `#[flag(name = "x")]` | `bool` | Flag with custom CLI name |
415/// | `#[arg]` | `T` | Required CLI argument |
416/// | `#[arg]` | `Option<T>` | Optional CLI argument |
417/// | `#[arg]` | `Vec<T>` | Multiple CLI arguments |
418/// | `#[arg(name = "x")]` | `T` | Argument with custom CLI name |
419/// | `#[ctx]` | `&CommandContext` | Access to command context |
420/// | `#[matches]` | `&ArgMatches` | Raw matches (escape hatch) |
421///
422/// # Return Type Handling
423///
424/// | Return Type | Behavior |
425/// |-------------|----------|
426/// | `Result<T, E>` | Passed through (dispatch auto-wraps in Output::Render) |
427/// | `Result<(), E>` | Wrapped in `HandlerResult<()>` with `Output::Silent` |
428///
429/// # Generated Code
430///
431/// For a function `fn foo(...)`, the macro generates `fn foo__handler(...)`.
432///
433/// # Example
434///
435/// ```rust,ignore
436/// use standout_macros::handler;
437///
438/// // Pure function - easy to test
439/// #[handler]
440/// pub fn list(#[flag] all: bool, #[arg] limit: Option<usize>) -> Result<Vec<Item>, Error> {
441///     storage::list(all, limit)
442/// }
443///
444/// // Generates:
445/// // pub fn list__handler(m: &ArgMatches) -> Result<Vec<Item>, Error> {
446/// //     let all = m.get_flag("all");
447/// //     let limit = m.get_one::<usize>("limit").cloned();
448/// //     list(all, limit)
449/// // }
450///
451/// // Use with Dispatch derive:
452/// #[derive(Subcommand, Dispatch)]
453/// #[dispatch(handlers = handlers)]
454/// enum Commands {
455///     #[dispatch(handler = list)]  // Uses list__handler
456///     List { ... },
457/// }
458/// ```
459///
460/// # Testing
461///
462/// The original function is preserved, so you can test it directly:
463///
464/// ```rust,ignore
465/// #[test]
466/// fn test_list() {
467///     let result = list(true, Some(10));
468///     assert!(result.is_ok());
469/// }
470/// ```
471#[proc_macro_attribute]
472pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
473    let attr = proc_macro2::TokenStream::from(attr);
474    let item = proc_macro2::TokenStream::from(item);
475    handler::handler_impl(attr, item)
476        .unwrap_or_else(|e| e.to_compile_error())
477        .into()
478}