Skip to content

Automatically derive patch types for Rust structs to enable efficient partial updates with zero runtime overhead

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

ShapelessCat/patchable

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Patchable

CI Crates.io Documentation License: MIT License: Apache-2.0

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

This project provides:

  • A Patchable trait for declaring patch types.
  • A Patch trait for applying partial updates.
  • A TryPatch trait as a fallible version of Patch.
  • 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.

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 macros handle the heavy lifting:

  1. 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, a From<Struct> for StructPatch will be generated, too.

  2. Correct Patch Behavior: The macro generates Patch implementations and correct patch methods based on the rules in item 1.

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

Table of Contents

Features

  • Automatic Patch Type Generation: Derives a companion Patch 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.5.0" # You can use the latest version

Usage

Basic Example

use 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);
}

Skipping Fields

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.

Nested Patchable Structs

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

Fallible Patching

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(())
    }
}

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}Patch 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;
    }

When you derive Patch on a struct:

  1. 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
  2. Trait Implementation: The Patch trait is implemented:

    pub trait Patch: Patchable {
        fn patch(&mut self, patch: Self::Patch);
    }

API Reference

#[derive(Patchable)]

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

#[derive(Patch)]

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)

#[patchable] Attribute

Marks a field for recursive patching.

Requirements:

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

Patchable Trait

pub trait Patchable {
    type Patch: Clone;
}
  • Patch: The associated patch type (automatically generated as {StructName}Patch if #[derive(Patchable)] is applied)

Patch Trait

pub trait Patch: Patchable {
    fn patch(&mut self, patch: Self::Patch);
}
  • patch: Method to apply a patch to the current instance

TryPatch Trait

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 a Result. A blanket implementation exists for all types that implement Patch (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.

About

Automatically derive patch types for Rust structs to enable efficient partial updates with zero runtime overhead

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Contributing

Stars

Watchers

Forks

Packages

No packages published

Languages