#macro-derive #struct-fields #rename #visibility #optional-fields #proc-macro #field-attributes #dissolved

macro dissolve-derive

derive macro for creating dissolved structs with public fields and optional field renaming

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 Aug 16, 2025

#1007 in Procedural macros

25 downloads per month

MIT/Apache

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:

  1. Make fields public: This exposes your internal state and allows arbitrary mutation, breaking encapsulation.
  2. 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 dissolve method 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 the dissolve method 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 the dissolve method

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