| Crates.io | substruct-genesis |
| lib.rs | substruct-genesis |
| version | 0.1.2 |
| created_at | 2025-09-04 13:33:38.473048+00 |
| updated_at | 2025-09-04 13:33:38.473048+00 |
| description | Procedural macros for substruct generation |
| homepage | |
| repository | https://github.com/VerveSol/substruct_genesis |
| max_upload_size | |
| id | 1824215 |
| size | 78,761 |

The SubstructBuilder macro generates a substruct for your original struct, containing only the fields you explicitly mark for updates. The generated substruct is completely independent of the original struct, providing a clean separation of concerns for building update operations.
Built with a clean, modular architecture, the macro separates processing logic from code generation, making it maintainable, extensible, and easy to understand.
#[substruct_field] attributes are includedOption<T> for nullable updates (configurable with option attribute)Option<Option<T>> for clear update semanticsOption<serde_json::Value>The macro is built with a clean, modular architecture that separates concerns for maintainability and extensibility:
src/
├── lib.rs # Main macro entry point and orchestration
├── generator.rs # Code generation and output formatting
└── processor/ # Processing logic organized in subfolder
├── mod.rs # Module declarations and exports
├── attributes.rs # Attribute parsing utilities
└── fields.rs # Field processing and analysis
lib.rs - The main procedural macro entry point that orchestrates the entire processgenerator.rs - Handles all code generation logic, including trait derivation, struct definitions, and implementation blocksprocessor/attributes.rs - Parses and extracts information from struct-level attributes like substruct_builder and deriveprocessor/fields.rs - Processes individual fields, determines their types, and handles the complex logic for different field kinds (primitive, nested, JSON)use serde::{Deserialize, Serialize};
use substruct_genesis::SubstructBuilder;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SubstructBuilder)]
struct User {
#[substruct_field(primitive)]
name: String,
// age field has no attribute, so it's completely excluded from the substruct
age: u32,
#[substruct_field(primitive)]
active: bool,
}
// The macro generates UserSubstruct with ONLY:
// - name: Option<String>
// - active: Option<bool>
// Note: age field is completely absent
#[substruct_field(primitive)])Option<T> (default) or T (when option = false)Some(value) = set to value (when wrapped)None = no change (when wrapped)value = set to value (when not wrapped)#[derive(SubstructBuilder)]
struct User {
#[substruct_field(primitive)] // option wrapped (default)
name: String, // -> Option<String>
#[substruct_field(primitive, option = false)] // not option wrapped
id: u32, // -> u32
}
#[substruct_field(primitive)] on Option<T>)Option<Option<T>>None = no changeSome(None) = set to NoneSome(Some(value)) = set to Some(value)#[derive(SubstructBuilder)]
struct Config {
#[substruct_field(primitive)]
theme: Option<String>,
}
let update = ConfigSubstruct::new(
Some(Some("dark".to_string())), // set to Some("dark")
// or Some(None) to set to None
// or None for no change
);
#[substruct_field(json)])Option<serde_json::Value>Some(value) = set to deserialized valueNone = no change#[derive(SubstructBuilder)]
struct Settings {
#[substruct_field(json)]
preferences: UserPreferences,
}
let update = SettingsSubstruct::new(
Some(serde_json::to_value(&new_prefs).unwrap())
);
#[substruct_field(nested)])Option<TypeSubstruct>#[derive(SubstructBuilder)]
struct Profile {
#[substruct_field(nested)]
address: Address,
}
// Generates ProfileSubstruct with:
// - address: Option<AddressSubstruct>
You can specify custom names for nested types:
#[derive(SubstructBuilder)]
struct Profile {
#[substruct_field(nested, nested_type = "AddressBuilder")]
address: Address,
}
// Generates ProfileSubstruct with:
// - address: Option<AddressBuilder>
Advanced Usage in Complex Hierarchies:
#[derive(SubstructBuilder)]
#[substruct_builder(name = "AddressBuilder")]
struct Address {
#[substruct_field(primitive)]
street: String,
#[substruct_field(primitive)]
city: String,
}
#[derive(SubstructBuilder)]
struct Person {
#[substruct_field(nested, nested_type = "AddressBuilder")]
address: Address,
}
// Both Person and Company can use AddressBuilder
// for consistent naming across the hierarchy
Customize the entire substruct name:
#[derive(SubstructBuilder)]
#[substruct_builder(name = "UserBuilder")]
struct User {
#[substruct_field(primitive)]
name: String,
}
// Generates UserBuilder instead of UserSubstruct
new(...)Constructor that takes all updatable fields as parameters.
from_source(source: &T) -> SelfCreates a substruct from an existing instance (all fields set to no-change).
is_empty(&self) -> boolReturns true if no fields would be changed by this update.
field_count(&self) -> usizeReturns the number of fields that have values set (non-default fields).
let update = UserSubstruct::new(Some("John".to_string()), None, Some(true));
assert_eq!(update.field_count(), 2); // name and active are set
clear(&mut self)Resets all fields to their default values (None for Option fields, default for unwrapped fields).
let mut update = UserSubstruct::new(Some("John".to_string()), Some(true));
assert_eq!(update.field_count(), 2);
update.clear();
assert_eq!(update.field_count(), 0);
assert!(update.is_empty());
apply_to(&self, target: &mut StructName)Applies the updates to a target struct instance. Works with all field types including nested fields (recursive application).
let mut user = User::new("Alice".to_string(), false, 25);
let update = UserSubstruct::new(Some("Bob".to_string()), Some(true));
update.apply_to(&mut user);
// user.name is now "Bob", user.active is now true
// user.age remains unchanged (25)
Nested Field Support:
#[derive(SubstructBuilder)]
struct Person {
#[substruct_field(primitive)]
name: String,
#[substruct_field(nested)]
address: Address,
}
#[derive(SubstructBuilder)]
struct Address {
#[substruct_field(primitive)]
street: String,
#[substruct_field(primitive)]
city: String,
}
let mut person = Person {
name: "Alice".to_string(),
address: Address {
street: "Old Street".to_string(),
city: "Old City".to_string(),
},
};
let address_update = AddressSubstruct::new(
Some("123 New St".to_string()),
Some("New City".to_string()),
);
let update = PersonSubstruct::new(
Some("Bob".to_string()),
Some(address_update),
);
update.apply_to(&mut person);
// person.name is now "Bob"
// person.address.street is now "123 New St"
// person.address.city is now "New City"
would_change(&self, target: &StructName) -> boolChecks if applying this update would modify the target struct. Works with all field types including nested fields (recursive checking).
let user = User::new("Alice".to_string(), false, 25);
let update = UserSubstruct::new(Some("Bob".to_string()), Some(true));
assert!(update.would_change(&user)); // Would change name and active
let no_change = UserSubstruct::new(Some("Alice".to_string()), Some(false));
assert!(!no_change.would_change(&user)); // Would not change anything
Nested Field Support:
let person = Person {
name: "Alice".to_string(),
address: Address {
street: "Old Street".to_string(),
city: "Old City".to_string(),
},
};
// Update that would change the target
let address_update = AddressSubstruct::new(
Some("123 New St".to_string()),
Some("New City".to_string()),
);
let update = PersonSubstruct::new(
Some("Bob".to_string()),
Some(address_update),
);
assert!(update.would_change(&person)); // Would change both name and address
// Update that wouldn't change the target
let no_change_address = AddressSubstruct::new(
Some("Old Street".to_string()),
Some("Old City".to_string()),
);
let no_change_update = PersonSubstruct::new(
Some("Alice".to_string()),
Some(no_change_address),
);
assert!(!no_change_update.would_change(&person)); // Would not change anything
merge(self, other: Self) -> SelfCombines two substructs, with the other substruct taking precedence for conflicting fields.
let update1 = UserSubstruct::new(Some("Alice".to_string()), None);
let update2 = UserSubstruct::new(None, Some(true));
let merged = update1.merge(update2);
// merged has name: Some("Alice") and active: Some(true)
has_field(&self, field_name: &str) -> boolChecks if a specific field has a value set (non-default value).
let update = UserSubstruct::new(Some("Alice".to_string()), None);
assert!(update.has_field("name")); // name is set
assert!(!update.has_field("active")); // active is not set
assert!(!update.has_field("age")); // age field doesn't exist in substruct
into_partial(self) -> HashMap<String, String>Converts the substruct into a flexible HashMap representation with string values for easy comparison and inspection.
let update = UserSubstruct::new(Some("Alice".to_string()), Some(true));
let partial = update.into_partial();
// Check that fields are present
assert!(partial.contains_key("name"));
assert!(partial.contains_key("active"));
// Compare actual values (as string representations)
assert_eq!(partial.get("name"), Some(&"\"Alice\"".to_string()));
assert_eq!(partial.get("active"), Some(&"true".to_string()));
// Fields that aren't set are not included
assert!(!partial.contains_key("age")); // age field doesn't exist in substruct
Nested Field Support:
#[derive(SubstructBuilder)]
struct Person {
#[substruct_field(primitive)]
name: String,
#[substruct_field(nested)]
address: Address,
}
#[derive(SubstructBuilder)]
struct Address {
#[substruct_field(primitive)]
street: String,
#[substruct_field(primitive)]
city: String,
}
let address_update = AddressSubstruct::new(
Some("123 New St".to_string()),
Some("New City".to_string()),
);
let update = PersonSubstruct::new(
Some("Bob".to_string()),
Some(address_update),
);
let partial = update.into_partial();
// Top-level field
assert_eq!(partial.get("name"), Some(&"\"Bob\"".to_string()));
// Nested field (recursively converted)
assert!(partial.contains_key("address"));
let address_str = partial.get("address").unwrap();
assert!(address_str.contains("street"));
assert!(address_str.contains("city"));
Default::default()Creates a substruct where all fields indicate "no change".
From<T> and From<&T>Implementations for creating substructs from owned and borrowed instances.
use serde::{Deserialize, Serialize};
use substruct_genesis::SubstructBuilder;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SubstructBuilder)]
struct UserProfile {
#[substruct_field(primitive)]
name: String,
#[substruct_field(primitive)]
age: u32,
#[substruct_field(primitive)]
email: Option<String>,
#[substruct_field(json)]
preferences: UserPreferences,
id: String, // completely excluded from substruct
}
// Create an update
let update = UserProfileSubstruct::new(
Some("John Doe".to_string()), // change name
None, // don't change age
Some(None), // set email to None
Some(serde_json::to_value(&new_prefs).unwrap()), // change preferences
);
// The substruct is completely independent
// Now includes utility methods for update operations
use serde::{Deserialize, Serialize};
use substruct_genesis::SubstructBuilder;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, SubstructBuilder)]
struct UserSettings {
#[substruct_field(primitive)]
theme: String,
#[substruct_field(primitive)]
notifications: bool,
#[substruct_field(primitive, option = false)]
version: u32,
}
// Create a partial update
let mut update = UserSettingsSubstruct::new(
Some("dark".to_string()),
Some(true),
2,
);
// Check how many fields are being updated
assert_eq!(update.field_count(), 3);
// Check if the update is empty
assert!(!update.is_empty());
// Clear all fields to reset
update.clear();
assert_eq!(update.field_count(), 0);
assert!(update.is_empty());
// Create a minimal update
let minimal_update = UserSettingsSubstruct::new(
Some("light".to_string()),
None, // no change to notifications
0, // version is unwrapped, so 0 means "set to 0"
);
assert_eq!(minimal_update.field_count(), 2);
// Check if specific fields are set
assert!(minimal_update.has_field("theme"));
assert!(!minimal_update.has_field("notifications"));
assert!(minimal_update.has_field("version"));
// Apply updates to a target struct
let mut settings = UserSettings::new("dark".to_string(), false, 1);
minimal_update.apply_to(&mut settings);
// settings.theme is now "light", settings.version is now 0
// settings.notifications remains false (unchanged)
// Check if updates would change a target
let other_settings = UserSettings::new("light".to_string(), false, 0);
assert!(!minimal_update.would_change(&other_settings)); // No changes needed
// Merge two updates
let theme_update = UserSettingsSubstruct::new(Some("dark".to_string()), None, 0);
let notification_update = UserSettingsSubstruct::new(None, Some(true), 0);
let combined = theme_update.merge(notification_update);
// combined has theme: Some("dark") and notifications: Some(true)
// Convert to partial representation for flexible handling
let partial = combined.into_partial();
assert_eq!(partial.get("theme"), Some(&"\"dark\"".to_string()));
assert_eq!(partial.get("notifications"), Some(&"true".to_string()));
assert_eq!(partial.get("version"), Some(&"0".to_string()));
#[derive(SubstructBuilder)]
#[substruct_builder(name = "AddressBuilder")]
struct Address {
#[substruct_field(primitive)]
street: String,
#[substruct_field(primitive)]
city: String,
}
#[derive(SubstructBuilder)]
struct Profile {
#[substruct_field(nested, nested_type = "AddressBuilder")]
address: Address,
}
let address_update = AddressBuilder::new(
Some("123 New St".to_string()),
Some("New City".to_string()),
);
let profile_update = ProfileSubstruct::new(Some(address_update));
Advanced Nested Naming Example:
#[derive(SubstructBuilder)]
#[substruct_builder(name = "AddressBuilder")]
struct Address {
#[substruct_field(primitive)]
street: String,
#[substruct_field(primitive)]
city: String,
}
#[derive(SubstructBuilder)]
struct Person {
#[substruct_field(primitive)]
name: String,
#[substruct_field(nested, nested_type = "AddressBuilder")]
address: Address,
}
#[derive(SubstructBuilder)]
struct Company {
#[substruct_field(nested)]
ceo: Person,
#[substruct_field(nested, nested_type = "AddressBuilder")]
address: Address,
}
// Creates a deep hierarchy with custom naming
let company_update = CompanySubstruct::new(
Some(person_update),
Some(address_update),
);
#[derive(SubstructBuilder)]
struct Config {
#[substruct_field(primitive, option = false)]
version: u32, // Always required, not wrapped in Option
#[substruct_field(primitive)]
theme: String, // Optional, wrapped in Option<String>
}
let update = ConfigSubstruct::new(
2, // version is required
Some("dark".to_string()), // theme is optional
);
#[substruct_field] attributes are completely excluded from the generated substruct#[substruct_field] - empty structs or structs with no tagged fields will cause compilation errorsserde for serialization supportClone and PartialEqThe macro provides clear error messages for:
serde_json::Value with #[substruct_field(json)]#[substruct_field]Eq for f64, PartialEq for String)The project includes a comprehensive test suite that validates all macro functionality and edge cases.
| Test File | Tests | Status | Purpose |
|---|---|---|---|
basic_functionality.rs |
12 | ✅ All Passing | Core macro functionality, field exclusion, and utility methods |
field_types.rs |
10 | ✅ All Passing | Primitive, JSON, and nested field handling |
configuration.rs |
4 | ✅ All Passing | Attributes, wrapping, naming, and debug |
complex_scenarios.rs |
5 | ✅ All Passing | Complex nested types and edge cases |
integration.rs |
2 | ✅ All Passing | Multiple features working together |
error_handling.rs |
7 | ✅ All Passing | Macro validation and error handling |
real_world.rs |
9 | ✅ All Passing | API, database, and e-commerce patterns |
edge_cases.rs |
9 | ✅ All Passing | Boundary conditions and edge cases |
Total: 58 tests, all passing ✅
basic_functionality.rs - Core Functionality TestsTests the fundamental behavior of the macro with a simple struct containing both marked and unmarked fields.
Test Cases:
test_basic_struct_derivation: Validates substruct creation with only marked fieldstest_basic_struct_default: Tests default substruct creationtest_basic_struct_from_source: Tests creating substruct from source structtest_basic_struct_is_empty: Validates empty state detectiontest_basic_struct_from_owned: Tests owned source conversiontest_basic_struct_field_count: Tests field counting functionalitytest_basic_struct_clear: Tests field clearing functionalitytest_basic_struct_apply_to: Tests applying updates to target structtest_basic_struct_would_change: Tests change detection functionalitytest_basic_struct_merge: Tests merging two substructstest_basic_struct_has_field: Tests field presence checkingtest_basic_struct_into_partial: Tests conversion to flexible HashMap representationKey Validation:
#[substruct_field] are completely excludedname and active fields appear in the substructage field is absent from all substruct operationsfield_types.rs - Field Type HandlingComprehensive tests for all field types: primitive, JSON, and nested.
Test Cases:
apply_to(), would_change(), and into_partial() functionality for nested structsKey Validation:
Option<T> by defaultOption<serde_json::Value>into_partial() method provides flexible HashMap representation with comparable string valuesconfiguration.rs - Configuration and AttributesTests all configuration options: attributes, wrapping, naming, and debug.
Test Cases:
Key Validation:
complex_scenarios.rs - Complex Type ScenariosTests complex nested types, custom types, and edge cases with advanced naming features.
Test Cases:
AddressBuilder custom naming for nested structsKey Validation:
AddressBuilder instead of AddressSubstruct)integration.rs - Feature IntegrationTests how multiple features work together in complex scenarios.
Test Cases:
Key Validation:
error_handling.rs - Macro Validation and Error HandlingComprehensive tests for macro compilation, validation, and edge case handling.
Test Cases:
Key Validation:
real_world.rs - Real-World Use Case PatternsTests common patterns used in production applications.
Test Cases:
Key Validation:
edge_cases.rs - Boundary Conditions and Edge CasesTests extreme scenarios and boundary conditions to ensure robustness.
Test Cases:
Key Validation:
cargo test --test basic_functionality
cargo test --test field_types
cargo test --test configuration
cargo test --test complex_scenarios
cargo test --test integration
cargo test --test error_handling
cargo test --test real_world
cargo test --test edge_cases
cargo test
cargo test -- --nocapture
Test Structure: Each test file follows a consistent pattern:
new(), default(), from_source()Key Testing Principles:
The Substruct Genesis macro provides a clean, efficient way to generate independent substruct builders for your Rust data structures. With comprehensive test coverage and clear documentation, the project ensures reliability and ease of use.
Key Benefits:
The test suite serves as both validation of current functionality and documentation of expected behavior, ensuring the macro remains reliable and well-tested as it evolves. With 58 comprehensive tests covering all aspects of the macro's functionality, the project maintains high quality and reliability standards.