patchable-macro 0.4.1

Macros 1.1 implementation of #[derive(Patchable)] for the patchable crate
Documentation

Patchable

Crates.io Documentation License: MIT License: Apache-2.0 Build Status

A Rust library for automatically deriving patch types and implementing efficient updates from patches for target types.

This project provides:

  • A Patchable trait for applying partial updates.
  • A TryPatch trait as a fallible version of Patchable.
  • A derive macro that generates a companion patch type for a given struct and implements Patchable.

This enables efficient partial updates of struct instances by applying patches, which is particularly useful for:

  • State management in event-driven systems.
  • Incremental updates in streaming applications.
  • Serialization/deserialization of state changes.

Note: patch types intentionally do not derive Serialize; patches should be created from their companion structs. The "serialization" item above refers to serializing a Patchable struct to produce its companion patch type instance.

Why Patchable?

Patchable shines when you need to persist and update state without hand-maintaining parallel "state" structs. A common example is durable execution: save only true state while skipping non-state fields (caches, handles, closures), then restore or update state incrementally.

The provided derive macro handles the heavy lifting:

  1. Patch Type Definition: For a given a struct definition, it provides fine-grained control over what becomes part of its companion patch:

    • Exclude non-state fields.
    • Include simple fields directly.
    • Include complex fields, which have their own patch types, indirectly by including their patches.
  2. Correct Patch Behavior: The macro generates Patchable implementations and correct patch methods based on the rules in item 1.

  3. Deserializable Patches: Patches can be decoded for storage or transport.

Patchable automates patch type generation and applies updates with zero runtime overhead.

Table of Contents

Features

  • Automatic Patch Type Generation: Derives a companion State struct for any struct annotated with #[derive(Patchable)]
  • Recursive Patching: Use #[patchable] attribute to mark fields that require recursive patching
  • Smart Exclusion: Respects #[serde(skip)] and #[serde(skip_serializing)], and PhantomData to keep patches lean.
  • Serde Integration: Generated patch types automatically implement serde::Deserialize and Clone
  • Generic Support: Full support for generic types with automatic trait bound inference
  • Zero Runtime Overhead: All code generation happens at compile time

Use Cases

Patchable is a good fit when you want to update state without hand-maintaining parallel structs, such as:

  • Event-sourced or durable systems where only state fields should be persisted.
  • Streaming or real-time pipelines that receive incremental updates.
  • Syncing or transporting partial state over the network.

Installation

MSRV: Rust 1.85 (edition 2024).

Add this to your Cargo.toml:

[dependencies]
patchable = "0.4.1"

Usage

Basic Example

use patchable::Patchable;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Patchable)]
struct User {
    id: u64,
    name: String,
    email: String,
}

fn main() {
    let mut user = User {
        id: 1,
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
    };

    // Serialize the current state
    let state_json = serde_json::to_string(&user).unwrap();
    
    // Deserialize into a patch
    let patch: UserState = serde_json::from_str(&state_json).unwrap();
    
    let mut default = User::default();
    // Apply the patch
    default.patch(patch); 
    
    assert_eq!(default, user);
}

Skipping Fields

Fields can be excluded from patching using serde attributes:

use patchable::Patchable;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize, Patchable)]
struct Measurement<T, F> {
    value: T,
    #[serde(skip)]
    compute_fn: F,
}

Fields marked with #[serde(skip)] or #[serde(skip_serializing)] are automatically excluded from the generated patch type.

Nested Patchable Structs

The macro fully supports generic types:

use patchable::Patchable;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize, Patchable)]
struct Container<Closure> {
    #[serde(skip)]
    computation_logic: Closure, // Not a part of state
    metadata: String,
}

#[derive(Clone, Debug, Serialize, Patchable)]
struct Wrapper<T, Closure> {
    data: T,
    #[patchable]
    inner: Container<Closure>,
}

The macro automatically:

  • Preserves only the generic parameters used in non-skipped fields
  • Adds appropriate trait bounds (Clone, Patchable) based on field usage
  • Generates correctly parameterized patch types

Fallible Patching

The TryPatch trait allows for fallible updates, which is useful when patch application requires validation:

use patchable::TryPatch;
use std::fmt;

struct Config {
    limit: u32,
}

#[derive(Clone)]
struct ConfigPatch {
    limit: u32,
}

#[derive(Debug)]
struct InvalidConfigError;

impl fmt::Display for InvalidConfigError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "limit cannot be zero")
    }
}

impl std::error::Error for InvalidConfigError {}

impl TryPatch for Config {
    type Patch = ConfigPatch;
    type Error = InvalidConfigError;

    fn try_patch(&mut self, patch: Self::Patch) -> Result<(), Self::Error> {
        if patch.limit == 0 {
            return Err(InvalidConfigError);
        }
        self.limit = patch.limit;
        Ok(())
    }
}

Limitations

  • Only structs are supported (enums and unions are not).
  • Lifetime parameters are not supported.
  • #[patchable] currently only supports simple generic types (not complex types like Vec<T>).
  • Generated patch types derive Clone and Deserialize but not Serialize (by design).

How It Works

When you derive Patchable on a struct:

  1. Patch Type Generation: A companion struct named {StructName}State is generated

    • Fields marked with #[patchable] use their own patch types (T::Patch)
    • Other fields are copied directly with their original types
    • Fields with #[serde(skip)], #[serde(skip_serializing)] or PhantomData are excluded
  2. Trait Implementation: The Patchable trait is implemented:

    pub trait Patchable {
        type Patch: Clone;
        fn patch(&mut self, patch: Self::Patch);
    }
    
  3. Patch Method: The patch method updates the struct:

    • Regular fields are directly assigned from the patch
    • #[patchable] fields are recursively patched via their own patch method

API Reference

#[derive(Patchable)]

Derives the Patchable trait for a struct.

Requirements:

  • Must be applied to a struct (not enums or unions)
  • Does not support lifetime parameters (borrowed fields)
  • Works with named, unnamed (tuple), and unit structs

#[patchable] Attribute

Marks a field for recursive patching.

Requirements:

  • The types of fields with #[patchable] must implement Patchable
  • Currently only supports simple generic types (not complex types like Vec<T>)

Patchable Trait

pub trait Patchable {
    type Patch: Clone;
    fn patch(&mut self, patch: Self::Patch);
}
  • Patch: The associated patch type (automatically generated as {StructName}State if #[derive(Patchable)] is applied)

  • patch: Method to apply a patch to the current instance

TryPatch Trait

A fallible variant of Patchable for cases where applying a patch might fail.

pub trait TryPatch {
    type Patch: Clone;
    type Error: std::error::Error + Send + Sync + 'static;
    fn try_patch(&mut self, patch: Self::Patch) -> Result<(), Self::Error>;
}
  • try_patch: Applies the patch, returning a Result. A blanket implementation exists for all types that implement Patchable (where Error is std::convert::Infallible).

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for details on how to get started.

License

This project is licensed under the MIT License and Apache-2.0 License.

Related Projects

  • serde - Serialization framework that integrates seamlessly with Patchable

Changelog

See CHANGELOG.md for release notes and version history.