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//! # Design Philosophy
21//!
22//! These macros return [`EmbeddedSource`] types that contain:
23//!
24//! 1. Embedded content (baked into binary at compile time)
25//! 2. Source path (for debug hot-reload)
26//!
27//! This design enables:
28//!
29//! - Release builds: Use embedded content, zero file I/O
30//! - Debug builds: Hot-reload from disk if source path exists
31//!
32//! # Examples
33//!
34//! For working examples, see:
35//! - `standout/tests/embed_macros.rs` - embedding macros
36//! - `standout/tests/dispatch_derive.rs` - dispatch derive macro
37//!
38//! [`EmbeddedSource`]: standout::EmbeddedSource
39//! [`RenderSetup`]: standout::RenderSetup
40
41mod dispatch;
42mod embed;
43mod seeker;
44mod tabular;
45
46use proc_macro::TokenStream;
47use syn::{parse_macro_input, DeriveInput, LitStr};
48
49/// Embeds all template files from a directory at compile time.
50///
51/// This macro walks the specified directory, reads all files with recognized
52/// template extensions, and returns an [`EmbeddedTemplates`] source that can
53/// be used with [`RenderSetup`] or converted to a [`TemplateRegistry`].
54///
55/// # Supported Extensions
56///
57/// Files are recognized by extension (in priority order):
58/// - `.jinja` (highest priority)
59/// - `.jinja2`
60/// - `.j2`
61/// - `.txt` (lowest priority)
62///
63/// When multiple files share the same base name with different extensions
64/// (e.g., `config.jinja` and `config.txt`), the higher-priority extension wins
65/// for extensionless lookups.
66///
67/// # Hot Reload Behavior
68///
69/// - Release builds: Uses embedded content (zero file I/O)
70/// - Debug builds: Reads from disk if source path exists (hot-reload)
71///
72/// For working examples, see `standout/tests/embed_macros.rs`.
73///
74/// # Compile-Time Errors
75///
76/// The macro will fail to compile if:
77/// - The directory doesn't exist
78/// - The directory is not readable
79/// - Any file content is not valid UTF-8
80///
81/// [`EmbeddedTemplates`]: standout::EmbeddedTemplates
82/// [`RenderSetup`]: standout::RenderSetup
83/// [`TemplateRegistry`]: standout::TemplateRegistry
84#[proc_macro]
85pub fn embed_templates(input: TokenStream) -> TokenStream {
86 let path_lit = parse_macro_input!(input as LitStr);
87 embed::embed_templates_impl(path_lit).into()
88}
89
90/// Embeds all stylesheet files from a directory at compile time.
91///
92/// This macro walks the specified directory, reads all files with recognized
93/// stylesheet extensions, and returns an [`EmbeddedStyles`] source that can
94/// be used with [`RenderSetup`] or converted to a [`StylesheetRegistry`].
95///
96/// # Supported Extensions
97///
98/// Files are recognized by extension (in priority order):
99/// - `.yaml` (highest priority)
100/// - `.yml` (lowest priority)
101///
102/// When multiple files share the same base name with different extensions
103/// (e.g., `dark.yaml` and `dark.yml`), the higher-priority extension wins.
104///
105/// # Hot Reload Behavior
106///
107/// - Release builds: Uses embedded content (zero file I/O)
108/// - Debug builds: Reads from disk if source path exists (hot-reload)
109///
110/// For working examples, see `standout/tests/embed_macros.rs`.
111///
112/// # Compile-Time Errors
113///
114/// The macro will fail to compile if:
115/// - The directory doesn't exist
116/// - The directory is not readable
117/// - Any file content is not valid UTF-8
118///
119/// [`EmbeddedStyles`]: standout::EmbeddedStyles
120/// [`RenderSetup`]: standout::RenderSetup
121/// [`StylesheetRegistry`]: standout::StylesheetRegistry
122#[proc_macro]
123pub fn embed_styles(input: TokenStream) -> TokenStream {
124 let path_lit = parse_macro_input!(input as LitStr);
125 embed::embed_styles_impl(path_lit).into()
126}
127
128/// Derives dispatch configuration from a clap `Subcommand` enum.
129///
130/// This macro eliminates boilerplate command-to-handler mappings by using
131/// naming conventions with explicit overrides when needed.
132///
133/// For working examples, see `standout/tests/dispatch_derive.rs`.
134///
135/// # Convention-Based Defaults
136///
137/// - Handler: `{handlers_module}::{variant_snake_case}`
138/// - `Add` → `handlers::add`
139/// - `ListAll` → `handlers::list_all`
140/// - Template: `{variant_snake_case}.j2`
141///
142/// # Container Attributes
143///
144/// | Attribute | Required | Description |
145/// |-----------|----------|-------------|
146/// | `handlers = path` | Yes | Module containing handler functions |
147///
148/// # Variant Attributes
149///
150/// | Attribute | Description | Default |
151/// |-----------|-------------|---------|
152/// | `handler = path` | Handler function | `{handlers}::{snake_case}` |
153/// | `template = "path"` | Template file | `{snake_case}.j2` |
154/// | `pre_dispatch = fn` | Pre-dispatch hook | None |
155/// | `post_dispatch = fn` | Post-dispatch hook | None |
156/// | `post_output = fn` | Post-output hook | None |
157/// | `nested` | Treat as nested subcommand | false |
158/// | `skip` | Skip this variant | false |
159///
160/// # Generated Code
161///
162/// Generates a `dispatch_config()` method returning a closure for
163/// use with `App::builder().commands()`.
164#[proc_macro_derive(Dispatch, attributes(dispatch))]
165pub fn dispatch_derive(input: TokenStream) -> TokenStream {
166 let input = parse_macro_input!(input as DeriveInput);
167 dispatch::dispatch_derive_impl(input)
168 .unwrap_or_else(|e| e.to_compile_error())
169 .into()
170}
171
172/// Derives a `TabularSpec` from struct field annotations.
173///
174/// This macro generates an implementation of the `Tabular` trait, which provides
175/// a `tabular_spec()` method that returns a `TabularSpec` for the struct.
176///
177/// For working examples, see `standout/tests/tabular_derive.rs`.
178///
179/// # Field Attributes
180///
181/// | Attribute | Type | Description |
182/// |-----------|------|-------------|
183/// | `width` | `usize` or `"fill"` or `"Nfr"` | Column width strategy |
184/// | `min` | `usize` | Minimum width (for bounded) |
185/// | `max` | `usize` | Maximum width (for bounded) |
186/// | `align` | `"left"`, `"right"`, `"center"` | Text alignment |
187/// | `anchor` | `"left"`, `"right"` | Column position |
188/// | `overflow` | `"truncate"`, `"wrap"`, `"clip"`, `"expand"` | Overflow handling |
189/// | `truncate_at` | `"end"`, `"start"`, `"middle"` | Truncation position |
190/// | `style` | string | Style name for the column |
191/// | `style_from_value` | flag | Use cell value as style name |
192/// | `header` | string | Header title (default: field name) |
193/// | `null_repr` | string | Representation for null values |
194/// | `key` | string | Data extraction key (supports dot notation) |
195/// | `skip` | flag | Exclude this field from the spec |
196///
197/// # Container Attributes
198///
199/// | Attribute | Type | Description |
200/// |-----------|------|-------------|
201/// | `separator` | string | Column separator (default: " ") |
202/// | `prefix` | string | Row prefix |
203/// | `suffix` | string | Row suffix |
204///
205/// # Example
206///
207/// ```ignore
208/// use standout::tabular::Tabular;
209/// use serde::Serialize;
210///
211/// #[derive(Serialize, Tabular)]
212/// #[tabular(separator = " │ ")]
213/// struct Task {
214/// #[col(width = 8, style = "muted")]
215/// id: String,
216///
217/// #[col(width = "fill", overflow = "wrap")]
218/// title: String,
219///
220/// #[col(width = 12, align = "right")]
221/// status: String,
222/// }
223///
224/// let spec = Task::tabular_spec();
225/// ```
226#[proc_macro_derive(Tabular, attributes(col, tabular))]
227pub fn tabular_derive(input: TokenStream) -> TokenStream {
228 let input = parse_macro_input!(input as DeriveInput);
229 tabular::tabular_derive_impl(input)
230 .unwrap_or_else(|e| e.to_compile_error())
231 .into()
232}
233
234/// Derives optimized row extraction for tabular formatting.
235///
236/// This macro generates an implementation of the `TabularRow` trait, which provides
237/// a `to_row()` method that converts the struct to a `Vec<String>` without JSON serialization.
238///
239/// For working examples, see `standout/tests/tabular_derive.rs`.
240///
241/// # Field Attributes
242///
243/// | Attribute | Description |
244/// |-----------|-------------|
245/// | `skip` | Exclude this field from the row |
246///
247/// # Example
248///
249/// ```ignore
250/// use standout::tabular::TabularRow;
251///
252/// #[derive(TabularRow)]
253/// struct Task {
254/// id: String,
255/// title: String,
256///
257/// #[col(skip)]
258/// internal_state: u32,
259///
260/// status: String,
261/// }
262///
263/// let task = Task {
264/// id: "TSK-001".to_string(),
265/// title: "Implement feature".to_string(),
266/// internal_state: 42,
267/// status: "pending".to_string(),
268/// };
269///
270/// let row = task.to_row();
271/// assert_eq!(row, vec!["TSK-001", "Implement feature", "pending"]);
272/// ```
273#[proc_macro_derive(TabularRow, attributes(col))]
274pub fn tabular_row_derive(input: TokenStream) -> TokenStream {
275 let input = parse_macro_input!(input as DeriveInput);
276 tabular::tabular_row_derive_impl(input)
277 .unwrap_or_else(|e| e.to_compile_error())
278 .into()
279}
280
281/// Derives the `Seekable` trait for query-enabled structs.
282///
283/// This macro generates an implementation of the `Seekable` trait from
284/// `standout-seeker`, enabling type-safe field access for query operations.
285///
286/// # Field Attributes
287///
288/// | Attribute | Description |
289/// |-----------|-------------|
290/// | `String` | String field (supports Eq, Ne, Contains, StartsWith, EndsWith, Regex) |
291/// | `Number` | Numeric field (supports Eq, Ne, Gt, Gte, Lt, Lte) |
292/// | `Timestamp` | Timestamp field (supports Eq, Ne, Before, After, Gt, Gte, Lt, Lte) |
293/// | `Enum` | Enum field (supports Eq, Ne, In) - requires `SeekerEnum` impl |
294/// | `Bool` | Boolean field (supports Eq, Ne, Is) |
295/// | `skip` | Exclude this field from queries |
296/// | `rename = "..."` | Use a custom name for queries |
297///
298/// # Generated Code
299///
300/// The macro generates:
301///
302/// 1. Field name constants (e.g., `Task::NAME`, `Task::PRIORITY`)
303/// 2. Implementation of `Seekable::seeker_field_value()`
304///
305/// # Example
306///
307/// ```ignore
308/// use standout_macros::Seekable;
309/// use standout_seeker::{Query, Seekable};
310///
311/// #[derive(Seekable)]
312/// struct Task {
313/// struct Task {
314/// #[seek(String)]
315/// name: String,
316///
317/// #[seek(Number)]
318/// priority: u8,
319///
320/// #[seek(Bool)]
321/// done: bool,
322///
323/// #[seek(skip)]
324/// internal_id: u64,
325/// }
326///
327/// let tasks = vec![
328/// Task { name: "Write docs".into(), priority: 3, done: false, internal_id: 1 },
329/// Task { name: "Fix bug".into(), priority: 5, done: true, internal_id: 2 },
330/// ];
331///
332/// let query = Query::new()
333/// .and_gte(Task::PRIORITY, 3u8)
334/// .not_eq(Task::DONE, true)
335/// .build();
336///
337/// let results = query.filter(&tasks, Task::accessor);
338/// assert_eq!(results.len(), 1);
339/// assert_eq!(results[0].name, "Write docs");
340/// ```
341///
342/// # Enum Fields
343///
344/// For enum fields, implement `SeekerEnum` on your enum type:
345///
346/// ```ignore
347/// use standout_seeker::SeekerEnum;
348///
349/// #[derive(Clone, Copy)]
350/// enum Status { Pending, Active, Done }
351///
352/// impl SeekerEnum for Status {
353/// fn seeker_discriminant(&self) -> u32 {
354/// match self {
355/// Status::Pending => 0,
356/// Status::Active => 1,
357/// Status::Done => 2,
358/// }
359/// }
360/// }
361///
362/// #[derive(Seekable)]
363/// struct Task {
364/// #[seek(Enum)]
365/// status: Status,
366/// }
367/// ```
368///
369/// # Timestamp Fields
370///
371/// For timestamp fields, implement `SeekerTimestamp` on your datetime type:
372///
373/// ```ignore
374/// use standout_seeker::{SeekerTimestamp, Timestamp};
375///
376/// struct MyDateTime(i64);
377///
378/// impl SeekerTimestamp for MyDateTime {
379/// fn seeker_timestamp(&self) -> Timestamp {
380/// Timestamp::from_millis(self.0)
381/// }
382/// }
383///
384/// #[derive(Seekable)]
385/// struct Event {
386/// #[seek(Timestamp)]
387/// created_at: MyDateTime,
388/// }
389/// ```
390#[proc_macro_derive(Seekable, attributes(seek))]
391pub fn seekable_derive(input: TokenStream) -> TokenStream {
392 let input = parse_macro_input!(input as DeriveInput);
393 seeker::seekable_derive_impl(input)
394 .unwrap_or_else(|e| e.to_compile_error())
395 .into()
396}