Expand description
§derive-defs
Library for generating derive preset macros from TOML configuration.
This crate provides functionality to parse TOML configuration files and generate proc-macro code for derive attribute presets.
§How It Works
derive-defs generates proc-macro code during the build process:
- You define macro presets in
derive_defs.toml - During
cargo build, the code generator creates Rust proc-macro implementations - The generated code is written to
$OUT_DIR/derive_defs.rs - You include this code in your proc-macro crate using
include! - You use the generated macros in your library or binary code
Important: Proc-macros can only be defined in a separate proc-macro type crate.
Binary crates cannot define and use proc-macros in the same crate.
§Project Setup Guide
§Scenario 1: Library Crate with Inline Macros
Use when: Your library generates its own macros and exports them.
Structure:
my-library/
├── Cargo.toml # [lib] proc-macro = true
├── build.rs # Generates macros
├── derive_defs.toml # Macro definitions
└── src/
└── lib.rs # Includes generated macrosCargo.toml:
[package]
name = "my-library"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
[build-dependencies]
derive-defs = "0.1"build.rs:
fn main() {
derive_defs::generate("derive_defs.toml")
.expect("Failed to generate derive defs");
}src/lib.rs:
// Include the generated proc-macro code
include!(concat!(env!("OUT_DIR"), "/derive_defs.rs"));§Scenario 2: Binary Crate with Separate Macros Crate ⭐ RECOMMENDED
Use when: You have a binary application that wants to use derive macros.
Important: Binary crates cannot define proc-macros that they also use. You must create a separate proc-macro crate in the workspace.
Structure:
my-app/
├── Cargo.toml # [workspace] with members
├── macros/ # Proc-macro crate
│ ├── Cargo.toml # [lib] proc-macro = true
│ ├── build.rs # Generates macros
│ ├── derive_defs.toml # Macro definitions
│ └── src/
│ └── lib.rs # Includes generated macros
└── src/
└── main.rs # Uses macros from `macros` crateRoot Cargo.toml:
[workspace]
members = ["macros", "src"]
resolver = "2"macros/Cargo.toml:
[package]
name = "my-app-macros"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
[build-dependencies]
derive-defs = "0.1"macros/build.rs:
fn main() {
derive_defs::generate("derive_defs.toml")
.expect("Failed to generate derive defs");
}macros/src/lib.rs:
include!(concat!(env!("OUT_DIR"), "/derive_defs.rs"));macros/derive_defs.toml:
[defs.config]
traits = ["Debug", "Clone", "Default"]
[defs.model]
traits = ["Debug", "Clone", "PartialEq"]src/Cargo.toml:
[package]
name = "my-app"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "my-app"
path = "main.rs"
[dependencies]
my-app-macros = { path = "../macros" }src/main.rs:
use my_app_macros::{Config, Model};
#[config]
struct AppConfig {
host: String,
port: u16,
}
#[model]
struct User {
name: String,
age: u32,
}
fn main() {
let config = AppConfig {
host: "localhost".to_string(),
port: 8080,
};
println!("{:?}", config);
}§Why Separate Proc-Macro Crate?
Rust’s proc-macro system has a fundamental limitation: proc-macros must be defined in a separate crate from where they are used. This is because:
- Proc-macros are compiled before the rest of the crate
- A binary crate cannot simultaneously define and consume macros
- The macro expansion happens during compilation, not at runtime
Error you’ll see if you try to use macros in the same binary crate:
error: can't use a procedural macro from the same crate that defines itSolution: Create a separate macros package in your workspace.
§Using include! vs include_str!
This crate uses include! macro for the generated code:
include!(concat!(env!("OUT_DIR"), "/derive_defs.rs"));include!: Includes the file as Rust code (what we use)include_str!: Includes the file as a&'static strstring
The generated code must be included as Rust code because it contains actual function definitions that the compiler needs to parse.
§Quick Start
- Create a
derive_defs.tomlconfiguration file:
[defs.serialization]
traits = ["Clone", "Serialize", "Deserialize"]
attrs = ['#[serde(rename_all = "camelCase")]']
[defs.model]
traits = ["Debug", "Clone", "PartialEq"]- Add to your
build.rs:
derive_defs::generate("derive_defs.toml")
.expect("Failed to generate derive defs");- Include generated code in your
lib.rs(in the proc-macro crate):
include!(concat!(env!("OUT_DIR"), "/derive_defs.rs"));- Use the generated macros in your code:
use my_crate_macros::*;
#[serialization]
struct User {
name: String,
age: u32,
}§Features
- Declarative Configuration: Define derive presets in TOML, not code
- Inheritance: Bundles can extend other bundles via
extends - Cross-file Imports: Split configuration across multiple files
- Runtime Modification: Use
omit/addto modify bundles at use site
§TOML Configuration
§Basic Syntax
[defs.<name>]
traits = ["Trait1", "Trait2"] # List of derive traits
attrs = ["#[attr]"] # Additional attributes
extends = "<parent>" # Inheritance (optional)§Inheritance
[defs.base]
traits = ["Debug", "Clone"]
[defs.value_object]
extends = "base"
traits = ["PartialEq", "Eq", "Hash"]
# Result: Debug, Clone, PartialEq, Eq, Hash§Cross-file Imports
[includes]
common = "shared/common_defs.toml"
[defs.api_response]
extends = "common.serialization"
traits = ["Default"]§Namespaced Definitions
[defs.cli.config]
traits = ["Debug", "Default"]
[defs.cli.args]
traits = ["Debug", "Clone"]
[defs.web.model]
traits = ["Debug", "Clone", "Serialize"]Generates macros: cli_config, cli_args, web_model.
§API Usage
§Attribute Modifiers
// Basic usage
#[serialization]
struct User { ... }
// Exclude Clone from the bundle
#[serialization(omit(Clone))]
struct Session { ... }
// Add Default and Hash to the bundle
#[serialization(add(Default, Hash))]
struct Config { ... }
// Exclude serde attributes
#[serialization(omit_attrs(serde))]
struct Internal { ... }
// Combination
#[serialization(omit(Clone), add(Copy))]
struct Flags { ... }§Common Patterns
§Configuration Structs
[defs.config]
traits = ["Debug", "Clone", "Default"]#[config]
struct ServerConfig {
host: String,
port: u16,
}§Domain Models
[defs.model]
traits = ["Debug", "Clone", "PartialEq", "Eq"]#[model]
struct User {
id: u32,
name: String,
}§Serialization-Ready Types
[defs.serializable]
traits = ["Serialize", "Deserialize"]
attrs = ['#[serde(rename_all = "camelCase")]']#[serializable]
struct ApiResponse {
status_code: u16,
message: String,
}§Error Handling
The crate provides clear error messages at build.rs time:
- Circular inheritance detection
- Undefined parent references
- Missing include files
- Duplicate definition names
§Common Pitfalls
Error: can't use a procedural macro from the same crate that defines it
Solution: Binary crates cannot define and use proc-macros. Create a
separate macros crate in your workspace (see Scenario 2 above).
Re-exports§
pub use validation::CrateType;pub use validation::detect_crate_type;pub use validation::validate_crate_type_for_macros;
Modules§
- codegen
- Proc-macro code generation.
- includes
- Cross-file include resolution.
- parser
- TOML configuration parser.
- prelude
- Common imports for convenience.
- resolver
- Definition resolver with inheritance support.
- validation
- Validation utilities for derive-defs configuration.
Enums§
- Error
- Errors that can occur during code generation.
Functions§
- generate
- Generate proc-macro code from a TOML configuration file.
- generate_
from_ resolved - Generate proc-macro code from an already resolved configuration.
- generate_
to - Generate proc-macro code from a TOML configuration file with custom output path.
Type Aliases§
- Result
- Result type alias for this crate.