4 releases
Uses new Rust 2024
| 0.1.4 | Nov 10, 2025 |
|---|---|
| 0.1.3 | Nov 10, 2025 |
| 0.1.2 | Oct 20, 2025 |
| 0.1.1 | Oct 12, 2025 |
| 0.1.0 |
|
#1007 in Procedural macros
25 downloads per month
22KB
389 lines
dissolve-derive
A Rust derive macro that generates a dissolve(self) method for structs, converting them into structs with all fields made public, with support for field skipping and renaming.
Dual-licensed under Apache 2.0 or MIT.
For more details, see docs.rs/dissolve-derive.
lib.rs:
Dissolve Derive
A procedural macro for safely taking ownership of inner fields from a struct without exposing those fields publicly.
Motivation
The dissolve-derive proc macro solves a specific problem: when you have a struct with
private fields and need to transfer ownership of those fields to another part of your code,
you often face two undesirable choices:
- Make fields public: This exposes your internal state and allows arbitrary mutation, breaking encapsulation.
- Write accessor methods: This requires boilerplate code and may involve cloning data, which is inefficient for large structures.
The Dissolve derive macro provides a dissolve(self) method that consumes the struct and
returns its fields in a type-safe manner. This approach:
- Preserves encapsulation: Fields remain private in the original struct
- Enables efficient ownership transfer: No cloning required, fields are moved
- Prevents misuse: The dissolved struct is a different type, preventing it from being used where the original struct is expected
- Provides flexibility: Control which fields are exposed and rename them if needed
- Allows custom visibility: Configure the visibility of the
dissolvemethod itself
Use Cases
1. API Boundaries
When building a library, you want to keep internal structure private but allow consumers to extract owned data when they're done with the struct's instance:
use dissolve_derive::Dissolve;
#[derive(Dissolve)]
pub struct Connection {
// Private: users can't modify the socket directly
socket: std::net::TcpStream,
// Private: internal state
buffer: Vec<u8>,
// Skip: purely internal, never exposed
#[dissolved(skip)]
statistics: ConnectionStats,
}
// Users can dissolve the connection to reclaim the socket
// without having public access to it during normal operation
let ConnectionDissolved { socket, buffer } = conn.dissolve();
2. Builder Pattern Finalization
Use dissolve to finalize a builder and extract components with controlled visibility:
use dissolve_derive::Dissolve;
#[derive(Dissolve)]
#[dissolve(visibility = "pub(crate)")]
pub struct ConfigBuilder {
database_url: String,
max_connections: u32,
#[dissolved(skip)]
validated: bool,
}
impl ConfigBuilder {
pub fn build(mut self) -> Config {
self.validated = true;
// Only accessible within the crate due to pub(crate)
let ConfigBuilderDissolved { database_url, max_connections } = self.dissolve();
Config { database_url, max_connections }
}
}
3. State Machine Transitions
Safely transition between states by dissolving one state struct and constructing the next:
use std::time::Instant;
use dissolve_derive::Dissolve;
#[derive(Dissolve)]
struct PendingRequest {
request_id: u64,
payload: Vec<u8>,
#[dissolved(skip)]
timestamp: Instant,
}
#[derive(Dissolve)]
struct ProcessedRequest {
request_id: u64,
response: Vec<u8>,
}
impl PendingRequest {
fn process(self) -> ProcessedRequest {
let PendingRequestDissolved { request_id, payload } = self.dissolve();
let response = process_payload(payload);
ProcessedRequest { request_id, response }
}
}
4. Zero-Cost Abstraction Unwrapping
When wrapping types for compile-time guarantees, use dissolve for efficient unwrapping:
use dissolve_derive::Dissolve;
#[derive(Dissolve)]
pub struct Validated<T> {
inner: T,
#[dissolved(skip)]
validation_token: ValidationToken,
}
impl<T> Validated<T> {
pub fn into_inner(self) -> T {
self.dissolve().inner
}
}
Attributes
Container Attributes (on structs)
#[dissolve(visibility = "...")]- Set the visibility of both thedissolvemethod and the generated dissolved struct- Supported values:
"pub","pub(crate)","pub(super)","pub(self)", or empty string for private - Default:
"pub"if not specified - Note: The dissolved struct (
{StructName}Dissolved) will have the same visibility as thedissolvemethod
- Supported values:
Field Attributes
#[dissolved(skip)]- Skip this field in the dissolved output#[dissolved(rename = "new_name")]- Rename this field in the dissolved struct (named structs only)
Examples
Basic Usage
use dissolve_derive::Dissolve;
#[derive(Dissolve)]
struct User {
name: String,
email: String,
}
let user = User {
name: "alice".to_string(),
email: "alice@example.com".to_string(),
};
let UserDissolved { name, email } = user.dissolve();
assert_eq!(name, "alice");
With Custom Visibility
use dissolve_derive::Dissolve;
#[derive(Dissolve)]
#[dissolve(visibility = "pub(crate)")]
pub struct InternalData {
value: i32,
}
// Both the dissolve method and InternalDataDissolved struct
// are only accessible within the same crate
let InternalDataDissolved { value } = data.dissolve();
Skipping Fields
use dissolve_derive::Dissolve;
#[derive(Dissolve)]
struct Credentials {
username: String,
#[dissolved(skip)]
password: String, // Never exposed, even through dissolve
}
Renaming Fields
use dissolve_derive::Dissolve;
#[derive(Dissolve)]
struct ApiResponse {
#[dissolved(rename = "user_id")]
id: u64,
#[dissolved(rename = "user_name")]
name: String,
}
Tuple Structs
use dissolve_derive::Dissolve;
#[derive(Dissolve)]
struct Coordinate(f64, f64, #[dissolved(skip)] String);
let coord = Coordinate(1.0, 2.0, "label".to_string());
let (x, y) = coord.dissolve();
Dependencies
~150–560KB
~13K SLoC