A Rust library for automatically deriving patch types and implementing efficient updates from patches for target types.
This project provides:
- A
Patchabletrait for declaring patch types. - A
Patchtrait for applying partial updates. - A
TryPatchtrait as a fallible version ofPatch. - Derive macros that generate companion patch types (
Patchable) and infallible patch logic (Patch).
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 type to produce its companion patch type instance.
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 macros handle the heavy lifting:
-
Patch Type Definition: For a given a struct definition,
#[derive(Patchable)]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.
When
#[derive(Patchable)]is used, aFrom<Struct>forStructPatchwill be generated, too. -
Correct Patch Behavior: The macro generates
Patchimplementations and correctpatchmethods based on the rules in item 1. -
Deserializable Patches: Patches can be decoded for storage or transport.
- Automatic Patch Type Generation: Derives a companion
Patchstruct 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)], andPhantomDatato keep patches lean. - Serde Integration: Generated patch types automatically implement
serde::DeserializeandClone - Generic Support: Full support for generic types with automatic trait bound inference
- Zero Runtime Overhead: All code generation happens at compile time
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.
MSRV: Rust 1.85 (edition 2024).
Add this to your Cargo.toml:
[dependencies]
patchable = "0.5.0" # You can use the latest versionuse patchable::{Patch, Patchable};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Patchable, Patch)]
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: UserPatch = serde_json::from_str(&state_json).unwrap();
let mut default = User::default();
// Apply the patch
default.patch(patch);
assert_eq!(default, user);
}Fields can be excluded from patching using serde attributes:
use patchable::{Patch, Patchable};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, Patchable, Patch)]
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.
The macros fully support generic types:
use patchable::{Patch, Patchable};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, Patchable, Patch)]
struct Container<Closure> {
#[serde(skip)]
computation_logic: Closure, // Not a part of state
metadata: String,
}
#[derive(Clone, Debug, Serialize, Patchable, Patch)]
struct Wrapper<T, Closure> {
data: T,
#[patchable]
inner: Container<Closure>,
}The macros automatically:
- Preserves only the generic parameters used in non-skipped fields
- Adds appropriate trait bounds (
Clone,Patchable,Patch) based on field usage - Generates correctly parameterized patch types
The TryPatch trait allows for fallible updates, which is useful when patch application requires validation:
use patchable::{TryPatch, Patchable};
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 Patchable for Config {
type Patch = ConfigPatch;
}
impl TryPatch for Config {
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(())
}
}- Only structs are supported (enums and unions are not).
- Lifetime parameters are not supported.
#[patchable]currently only supports simple generic types (not complex types likeVec<T>).- Generated patch types derive
CloneandDeserializebut notSerialize(by design).
When you derive Patchable on a struct:
-
Patch Type Generation: A companion struct named
{StructName}Patchis 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)]orPhantomDataare excluded
- Fields marked with
-
Trait Implementation: The
Patchabletrait is implemented:pub trait Patchable { type Patch: Clone; }
When you derive Patch on a struct:
-
Patch Method: The
patchmethod updates the struct:- Regular fields are directly assigned from the patch
#[patchable]fields are recursively patched via their ownpatchmethod
-
Trait Implementation: The
Patchtrait is implemented:pub trait Patch: Patchable { fn patch(&mut self, patch: Self::Patch); }
Generates the companion {StructName}Patch type and implements Patchable 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
Derives the Patch trait implementation 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
- The target type must implement
Patchable(derive it or implement manually)
Marks a field for recursive patching.
Requirements:
- The types of fields with
#[patchable]must implementPatch - Currently only supports simple generic types (not complex types like
Vec<T>)
pub trait Patchable {
type Patch: Clone;
}Patch: The associated patch type (automatically generated as{StructName}Patchif#[derive(Patchable)]is applied)
pub trait Patch: Patchable {
fn patch(&mut self, patch: Self::Patch);
}patch: Method to apply a patch to the current instance
A fallible variant of Patch for cases where applying a patch might fail.
pub trait TryPatch: Patchable {
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 aResult. A blanket implementation exists for all types that implementPatch(whereErrorisstd::convert::Infallible).
Contributions are welcome! Please see CONTRIBUTING.md for details on how to get started.
This project is licensed under the MIT License and Apache-2.0 License.
- serde - Serialization framework that integrates seamlessly with Patchable
See CHANGELOG.md for release notes and version history.