Skip to main content

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};