derive_defs/lib.rs
1//! # derive-defs
2//!
3//! Library for generating derive preset macros from TOML configuration.
4//!
5//! This crate provides functionality to parse TOML configuration files
6//! and generate proc-macro code for derive attribute presets.
7//!
8//! ## How It Works
9//!
10//! `derive-defs` generates **proc-macro code** during the build process:
11//!
12//! 1. You define macro presets in `derive_defs.toml`
13//! 2. During `cargo build`, the code generator creates Rust proc-macro implementations
14//! 3. The generated code is written to `$OUT_DIR/derive_defs.rs`
15//! 4. You include this code in your proc-macro crate using `include!`
16//! 5. You use the generated macros in your library or binary code
17//!
18//! **Important**: Proc-macros can only be defined in a separate `proc-macro` type crate.
19//! Binary crates **cannot** define and use proc-macros in the same crate.
20//!
21//! ## Project Setup Guide
22//!
23//! ### Scenario 1: Library Crate with Inline Macros
24//!
25//! **Use when**: Your library generates its own macros and exports them.
26//!
27//! **Structure:**
28//! ```text
29//! my-library/
30//! ├── Cargo.toml # [lib] proc-macro = true
31//! ├── build.rs # Generates macros
32//! ├── derive_defs.toml # Macro definitions
33//! └── src/
34//! └── lib.rs # Includes generated macros
35//! ```
36//!
37//! **Cargo.toml:**
38//! ```toml
39//! [package]
40//! name = "my-library"
41//! version = "0.1.0"
42//! edition = "2024"
43//!
44//! [lib]
45//! proc-macro = true
46//!
47//! [dependencies]
48//! proc-macro2 = "1"
49//! quote = "1"
50//! syn = { version = "2", features = ["full"] }
51//!
52//! [build-dependencies]
53//! derive-defs = "0.1"
54//! ```
55//!
56//! **build.rs:**
57//! ```rust,no_run
58//! fn main() {
59//! derive_defs::generate("derive_defs.toml")
60//! .expect("Failed to generate derive defs");
61//! }
62//! ```
63//!
64//! **src/lib.rs:**
65//! ```rust,ignore
66//! // Include the generated proc-macro code
67//! include!(concat!(env!("OUT_DIR"), "/derive_defs.rs"));
68//! ```
69//!
70//! ### Scenario 2: Binary Crate with Separate Macros Crate ⭐ RECOMMENDED
71//!
72//! **Use when**: You have a binary application that wants to use derive macros.
73//!
74//! **Important**: Binary crates cannot define proc-macros that they also use.
75//! You must create a separate proc-macro crate in the workspace.
76//!
77//! **Structure:**
78//! ```text
79//! my-app/
80//! ├── Cargo.toml # [workspace] with members
81//! ├── macros/ # Proc-macro crate
82//! │ ├── Cargo.toml # [lib] proc-macro = true
83//! │ ├── build.rs # Generates macros
84//! │ ├── derive_defs.toml # Macro definitions
85//! │ └── src/
86//! │ └── lib.rs # Includes generated macros
87//! └── src/
88//! └── main.rs # Uses macros from `macros` crate
89//! ```
90//!
91//! **Root Cargo.toml:**
92//! ```toml
93//! [workspace]
94//! members = ["macros", "src"]
95//! resolver = "2"
96//! ```
97//!
98//! **macros/Cargo.toml:**
99//! ```toml
100//! [package]
101//! name = "my-app-macros"
102//! version = "0.1.0"
103//! edition = "2024"
104//!
105//! [lib]
106//! proc-macro = true
107//!
108//! [dependencies]
109//! proc-macro2 = "1"
110//! quote = "1"
111//! syn = { version = "2", features = ["full"] }
112//!
113//! [build-dependencies]
114//! derive-defs = "0.1"
115//! ```
116//!
117//! **macros/build.rs:**
118//! ```rust,no_run
119//! fn main() {
120//! derive_defs::generate("derive_defs.toml")
121//! .expect("Failed to generate derive defs");
122//! }
123//! ```
124//!
125//! **macros/src/lib.rs:**
126//! ```rust,ignore
127//! include!(concat!(env!("OUT_DIR"), "/derive_defs.rs"));
128//! ```
129//!
130//! **`macros/derive_defs.toml`:**
131//! ```toml
132//! [defs.config]
133//! traits = ["Debug", "Clone", "Default"]
134//!
135//! [defs.model]
136//! traits = ["Debug", "Clone", "PartialEq"]
137//! ```
138//!
139//! **src/Cargo.toml:**
140//! ```toml
141//! [package]
142//! name = "my-app"
143//! version = "0.1.0"
144//! edition = "2024"
145//!
146//! [[bin]]
147//! name = "my-app"
148//! path = "main.rs"
149//!
150//! [dependencies]
151//! my-app-macros = { path = "../macros" }
152//! ```
153//!
154//! **src/main.rs:**
155//! ```rust,ignore
156//! use my_app_macros::{Config, Model};
157//!
158//! #[config]
159//! struct AppConfig {
160//! host: String,
161//! port: u16,
162//! }
163//!
164//! #[model]
165//! struct User {
166//! name: String,
167//! age: u32,
168//! }
169//!
170//! fn main() {
171//! let config = AppConfig {
172//! host: "localhost".to_string(),
173//! port: 8080,
174//! };
175//! println!("{:?}", config);
176//! }
177//! ```
178//!
179//! ### Why Separate Proc-Macro Crate?
180//!
181//! Rust's proc-macro system has a fundamental limitation: **proc-macros must be
182//! defined in a separate crate** from where they are used. This is because:
183//!
184//! 1. Proc-macros are compiled before the rest of the crate
185//! 2. A binary crate cannot simultaneously define and consume macros
186//! 3. The macro expansion happens during compilation, not at runtime
187//!
188//! **Error you'll see if you try to use macros in the same binary crate:**
189//! ```text
190//! error: can't use a procedural macro from the same crate that defines it
191//! ```
192//!
193//! **Solution**: Create a separate `macros` package in your workspace.
194//!
195//! ## Using `include!` vs `include_str!`
196//!
197//! This crate uses `include!` macro for the generated code:
198//!
199//! ```rust,ignore
200//! include!(concat!(env!("OUT_DIR"), "/derive_defs.rs"));
201//! ```
202//!
203//! - **`include!`**: Includes the file as Rust code (what we use)
204//! - **`include_str!`**: Includes the file as a `&'static str` string
205//!
206//! The generated code must be included as Rust code because it contains
207//! actual function definitions that the compiler needs to parse.
208//!
209//! ## Quick Start
210//!
211//! 1. Create a `derive_defs.toml` configuration file:
212//!
213//! ```toml
214//! [defs.serialization]
215//! traits = ["Clone", "Serialize", "Deserialize"]
216//! attrs = ['#[serde(rename_all = "camelCase")]']
217//!
218//! [defs.model]
219//! traits = ["Debug", "Clone", "PartialEq"]
220//! ```
221//!
222//! 2. Add to your `build.rs`:
223//!
224//! ```rust,no_run
225//! derive_defs::generate("derive_defs.toml")
226//! .expect("Failed to generate derive defs");
227//! ```
228//!
229//! 3. Include generated code in your `lib.rs` (in the proc-macro crate):
230//!
231//! ```rust,ignore
232//! include!(concat!(env!("OUT_DIR"), "/derive_defs.rs"));
233//! ```
234//!
235//! 4. Use the generated macros in your code:
236//!
237//! ```rust,ignore
238//! use my_crate_macros::*;
239//!
240//! #[serialization]
241//! struct User {
242//! name: String,
243//! age: u32,
244//! }
245//! ```
246//!
247//! ## Features
248//!
249//! - **Declarative Configuration**: Define derive presets in TOML, not code
250//! - **Inheritance**: Bundles can extend other bundles via `extends`
251//! - **Cross-file Imports**: Split configuration across multiple files
252//! - **Runtime Modification**: Use `omit`/`add` to modify bundles at use site
253//!
254//! ## TOML Configuration
255//!
256//! ### Basic Syntax
257//!
258//! ```toml
259//! [defs.<name>]
260//! traits = ["Trait1", "Trait2"] # List of derive traits
261//! attrs = ["#[attr]"] # Additional attributes
262//! extends = "<parent>" # Inheritance (optional)
263//! ```
264//!
265//! ### Inheritance
266//!
267//! ```toml
268//! [defs.base]
269//! traits = ["Debug", "Clone"]
270//!
271//! [defs.value_object]
272//! extends = "base"
273//! traits = ["PartialEq", "Eq", "Hash"]
274//! # Result: Debug, Clone, PartialEq, Eq, Hash
275//! ```
276//!
277//! ### Cross-file Imports
278//!
279//! ```toml
280//! [includes]
281//! common = "shared/common_defs.toml"
282//!
283//! [defs.api_response]
284//! extends = "common.serialization"
285//! traits = ["Default"]
286//! ```
287//!
288//! ### Namespaced Definitions
289//!
290//! ```toml
291//! [defs.cli.config]
292//! traits = ["Debug", "Default"]
293//!
294//! [defs.cli.args]
295//! traits = ["Debug", "Clone"]
296//!
297//! [defs.web.model]
298//! traits = ["Debug", "Clone", "Serialize"]
299//! ```
300//!
301//! Generates macros: `cli_config`, `cli_args`, `web_model`.
302//!
303//! ## API Usage
304//!
305//! ### Attribute Modifiers
306//!
307//! ```rust,ignore
308//! // Basic usage
309//! #[serialization]
310//! struct User { ... }
311//!
312//! // Exclude Clone from the bundle
313//! #[serialization(omit(Clone))]
314//! struct Session { ... }
315//!
316//! // Add Default and Hash to the bundle
317//! #[serialization(add(Default, Hash))]
318//! struct Config { ... }
319//!
320//! // Exclude serde attributes
321//! #[serialization(omit_attrs(serde))]
322//! struct Internal { ... }
323//!
324//! // Combination
325//! #[serialization(omit(Clone), add(Copy))]
326//! struct Flags { ... }
327//! ```
328//!
329//! ## Common Patterns
330//!
331//! ### Configuration Structs
332//!
333//! ```toml
334//! [defs.config]
335//! traits = ["Debug", "Clone", "Default"]
336//! ```
337//!
338//! ```rust,ignore
339//! #[config]
340//! struct ServerConfig {
341//! host: String,
342//! port: u16,
343//! }
344//! ```
345//!
346//! ### Domain Models
347//!
348//! ```toml
349//! [defs.model]
350//! traits = ["Debug", "Clone", "PartialEq", "Eq"]
351//! ```
352//!
353//! ```rust,ignore
354//! #[model]
355//! struct User {
356//! id: u32,
357//! name: String,
358//! }
359//! ```
360//!
361//! ### Serialization-Ready Types
362//!
363//! ```toml
364//! [defs.serializable]
365//! traits = ["Serialize", "Deserialize"]
366//! attrs = ['#[serde(rename_all = "camelCase")]']
367//! ```
368//!
369//! ```rust,ignore
370//! #[serializable]
371//! struct ApiResponse {
372//! status_code: u16,
373//! message: String,
374//! }
375//! ```
376//!
377//! ## Error Handling
378//!
379//! The crate provides clear error messages at `build.rs` time:
380//!
381//! - Circular inheritance detection
382//! - Undefined parent references
383//! - Missing include files
384//! - Duplicate definition names
385//!
386//! ### Common Pitfalls
387//!
388//! **Error**: `can't use a procedural macro from the same crate that defines it`
389//!
390//! **Solution**: Binary crates cannot define and use proc-macros. Create a
391//! separate `macros` crate in your workspace (see Scenario 2 above).
392
393#![cfg_attr(docsrs, feature(doc_cfg))]
394#![doc(
395 html_logo_url = "https://raw.githubusercontent.com/opentc/rust-derive-defs/main/assets/logo.png"
396)]
397#![doc(
398 html_favicon_url = "https://raw.githubusercontent.com/opentc/rust-derive-defs/main/assets/favicon.ico"
399)]
400#![allow(clippy::needless_doctest_main)]
401
402use std::path::Path;
403
404pub mod codegen;
405pub mod includes;
406pub mod parser;
407pub mod resolver;
408pub mod validation;
409
410/// Errors that can occur during code generation.
411#[derive(Debug, thiserror::Error)]
412#[non_exhaustive]
413pub enum Error {
414 /// Failed to read the configuration file.
415 #[error("failed to read config file: {0}")]
416 ConfigRead(#[source] std::io::Error),
417
418 /// Failed to parse TOML.
419 #[error("failed to parse TOML: {0}")]
420 TomlParse(#[source] toml::de::Error),
421
422 /// Validation error.
423 #[error("validation error: {0}")]
424 Validation(String),
425
426 /// Failed to write generated code.
427 #[error("failed to write generated code: {0}")]
428 CodegenWrite(#[source] std::io::Error),
429
430 /// Circular dependency detected.
431 #[error("circular extends detected: {0}")]
432 CircularDependency(String),
433
434 /// Reference to undefined definition.
435 #[error("def \"{def}\" extends \"{parent}\" which is not defined")]
436 UndefinedParent {
437 /// The name of the definition with the invalid extends.
438 def: String,
439 /// The name of the undefined parent.
440 parent: String,
441 },
442
443 /// Include file not found.
444 #[error("include file \"{path}\" not found (resolved to {resolved})")]
445 IncludeNotFound {
446 /// The path as specified in the config.
447 path: String,
448 /// The resolved absolute path.
449 resolved: String,
450 },
451}
452
453/// Result type alias for this crate.
454pub type Result<T> = std::result::Result<T, Error>;
455
456/// Generate proc-macro code from a TOML configuration file.
457///
458/// This function parses the TOML configuration at `path` and generates
459/// proc-macro code in the `OUT_DIR` directory. The generated code should
460/// be included in your proc-macro crate.
461///
462/// # Errors
463///
464/// Returns an error if:
465/// - The configuration file cannot be read
466/// - The TOML is invalid
467/// - There are validation errors (circular dependencies, undefined parents, etc.)
468/// - The output file cannot be written
469///
470/// # Example
471///
472/// ```no_run
473/// derive_defs::generate("derive_defs.toml")
474/// .expect("Failed to generate derive defs");
475/// ```
476///
477/// # Panics
478///
479/// This function panics if the `OUT_DIR` environment variable is not set.
480/// This should only happen if called outside of a build script context.
481pub fn generate<P: AsRef<Path>>(path: P) -> Result<()> {
482 let path_ref = path.as_ref();
483
484 // Validate that the crate type is compatible with proc-macro generation
485 let manifest_dir = path_ref.parent().unwrap_or_else(|| Path::new("."));
486 validation::validate_crate_type_for_macros(manifest_dir)?;
487
488 let config = parser::parse_file(path_ref)?;
489 let config = includes::resolve_includes(config, path_ref)?;
490 let resolved = resolver::resolve(&config)?;
491 codegen::generate(resolved)
492}
493
494/// Generate proc-macro code from a TOML configuration file with custom output path.
495///
496/// Similar to [`generate`], but allows specifying a custom output path instead
497/// of using `OUT_DIR`.
498///
499/// # Errors
500///
501/// Same as [`generate`].
502///
503/// # Example
504///
505/// ```no_run
506/// derive_defs::generate_to("derive_defs.toml", "/tmp/output.rs")
507/// .expect("Failed to generate derive defs");
508/// ```
509pub fn generate_to<P: AsRef<Path>, O: AsRef<Path>>(path: P, output: O) -> Result<()> {
510 let path_ref = path.as_ref();
511
512 // Validate that the crate type is compatible with proc-macro generation
513 let manifest_dir = path_ref.parent().unwrap_or_else(|| Path::new("."));
514 validation::validate_crate_type_for_macros(manifest_dir)?;
515
516 let config = parser::parse_file(path_ref)?;
517 let config = includes::resolve_includes(config, path_ref)?;
518 let resolved = resolver::resolve(&config)?;
519 codegen::generate_to(resolved, output)
520}
521
522/// Generate proc-macro code from an already resolved configuration.
523///
524/// This is useful when you want to programmatically build a configuration
525/// and generate code without reading from a file.
526///
527/// # Errors
528///
529/// Returns an error if writing the output file fails.
530///
531/// # Example
532///
533/// ```no_run
534/// use derive_defs::resolver::{ResolvedConfig, ResolvedDef};
535/// use std::collections::HashMap;
536///
537/// let mut defs = HashMap::new();
538/// defs.insert("model".to_string(), ResolvedDef {
539/// name: "model".to_string(),
540/// traits: vec!["Debug".to_string(), "Clone".to_string()],
541/// attrs: vec![],
542/// });
543///
544/// let config = ResolvedConfig { defs };
545/// derive_defs::generate_from_resolved(config, "/tmp/output.rs").unwrap();
546/// ```
547pub fn generate_from_resolved<O: AsRef<Path>>(
548 resolved: resolver::ResolvedConfig,
549 output: O,
550) -> Result<()> {
551 codegen::generate_to(resolved, output)
552}
553
554/// Common imports for convenience.
555pub mod prelude {
556 pub use crate::{Error, Result, generate, generate_to};
557}
558
559// Re-export validation types for users who want to do custom validation
560pub use validation::{CrateType, detect_crate_type, validate_crate_type_for_macros};