Skip to content

yanganto/struct-patch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

169 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Struct Patch

Crates.io MIT licensed Docs

A library to help you modify config structs. It provides:

  • Patch derive macro for partial updates
  • Filler derive macro to fill empty fields
  • Substrate and Catalyst derive macros to extend a struct with extra fields

Introduction

This crate provides the Patch, Filler, Substrate, Catalyst and Complex traits with accompanying derive macros in the following three use cases.

  • If any field in a Patch is Some, it overwrites the corresponding field when applied.
  • If any field in the instance is empty (None or an empty collection), Filler will try to fill it. It supports Option, Vec, VecDeque, LinkedList, HashMap, BTreeMap, HashSet, BTreeSet, BinaryHeap fields, as well as custom types via #[filler(extendable)] and any type via #[filler(empty_value = ...)].
  • With the catalyst feature, Substrate, Catalyst and Complex traits with accompanying derive macros help you extend a struct with extra fields from another crate.

This crate supports no_std — check the no-std-examples.

The following are more specific scenarios to help you learn the details:

  • A project with config from environments, files, and command line: you can use Patch to keep your config organized. Please check this template.
  • A project extended from another project, where only some fields of the config differ. You can use the catalyst feature to expose the base struct in a build script with Substrate, then a Catalyst struct can bind to it and produce a complex struct. Check the complex-example and Quick Example: case 3.

Quick Example

Case 1 - Patch on a Config

Deriving Patch on a struct generates a struct similar to the original one, but with all fields wrapped in an Option. An instance of such a patch struct can be applied onto the original struct, replacing values only if they are set to Some, leaving them unchanged otherwise.

use struct_patch::Patch;
use serde::{Deserialize, Serialize};

#[derive(Default, Debug, PartialEq, Patch)]
#[patch(attribute(derive(Debug, Default, Deserialize, Serialize)))]
struct Item {
    field_bool: bool,
    field_int: usize,
    field_string: String,
}

fn patch_json() {
    let mut item = Item {
        field_bool: true,
        field_int: 42,
        field_string: String::from("hello"),
    };

    let data = r#"{
        "field_int": 7
    }"#;

    let patch: ItemPatch = serde_json::from_str(data).unwrap();

    item.apply(patch);
    // You can do
    // `let new_item = item << patch;`

    // For multiple patches,
    // you can do this
    // `let new_item = item << patch_1 << patch_2;`
    // or make an aggregated one, but please make sure the patch fields do not conflict, else it will panic
    // ```
    // let overall_patch = patch_1 + patch_2 + patch_3;
    // let new_item = item << overall_patch;
    // ```

    assert_eq!(
        item,
        Item {
            field_bool: true,
            field_int: 7,
            field_string: String::from("hello")
        }
    );
}

Case 2 - Fill up on a Config

Deriving Filler on a struct generates a struct similar to the original one, keeping only the fields that can be filled (Option, collections, extendable, or empty_value fields). Unlike Patch, the Filler only works on empty fields of the instance.

use struct_patch::Filler;

#[derive(Filler)]
struct Item {
    field_int: usize,
    maybe_field_int: Option<usize>,
    list: Vec<usize>,
}
let mut item = Item {
    field_int: 0,
    maybe_field_int: None,
    list: Vec::new(),
};

let filler_1 = ItemFiller { maybe_field_int: Some(7), list: Vec::new() };
item.apply(filler_1);
assert_eq!(item.maybe_field_int, Some(7));

let filler_2 = ItemFiller { maybe_field_int: Some(100), list: Vec::new() };

// The field is not empty, so the filler has no effect.
item.apply(filler_2);
assert_eq!(item.maybe_field_int, Some(7));

let filler_3 = ItemFiller { maybe_field_int: Some(100), list: vec![7] };

item.apply(filler_3);
assert_eq!(item.maybe_field_int, Some(7));
assert_eq!(item.list, vec![7]);

Case 3 - Extend a struct from a crate

Deriving Substrate on a struct exposes the field information so that other crates can access it in a build.rs. Deriving Catalyst reads the field information of a Substrate and generates a new complex struct. In the other words, the catalyst is a struct with extra fields that the developer writes down in the downstream crate. The complex is a generated struct that combines the substrate's fields with the catalyst's extra fields. The overall behavior is like chemical catalysts: a catalyst binds onto a substrate to form a complex struct, which has all fields from both. A complex can also decouple without cloning, returning the original catalyst and substrate. Check the complex-example. With the unsafe feature, bind and decouple use ManuallyDrop + ptr::read to avoid memory moves, and __substrate_new uses MaybeUninit + ptr::write while __substrate_unpack uses ManuallyDrop + ptr::read, such that the copy will be less.

In terms of crate dependencies, the crate using Substrate is upstream (a dependency), and the crate using Catalyst is downstream (it depends on the substrate crate). The downstream crate calls Substrate::expose() in its build.rs to read the field information at compile time, then uses #[catalyst(bind = ...)] to generate the complex struct.

/// In the substrate crate (src/lib.rs)
use struct_patch::Substrate;

#[derive(Substrate)]
pub struct Base {
    pub field_bool: bool,
    pub field_string: String,
}

/// In the catalyst crate (build.rs)
use struct_patch::Substrate;

fn main() {
    substrate::Base::expose();
}

/// In the catalyst crate (src/lib.rs)
use struct_patch::Catalyst;

#[derive(Catalyst)]
#[catalyst(bind = Base)]
struct Amyloid {
    pub extra_bool: bool,
    pub extra_option: Option<usize>,
}
// Now AmyloidComplex is generated:
// struct AmyloidComplex {
//     pub field_bool: bool,
//     pub field_string: String,
//     pub extra_bool: bool,
//     pub extra_option: Option<usize>,
// }

Case 4 - Avoid double-Option for Option<Vec<_>> fields

By default, deriving Patch wraps every field in an Option, so a field typed Option<Vec<T>> becomes Option<Option<Vec<T>>> in the generated patch. When this double wrapping is undesirable, annotate the field with #[patch(skip_wrap)] to keep the original type in the patch. None in the patch then means "no change" and Some(v) replaces the field — including Some(vec![]) to explicitly clear the vector.

use struct_patch::Patch;

#[derive(Default, Patch)]
struct Item {
    #[patch(skip_wrap)]
    tags: Option<Vec<String>>,
}

// Generated struct
// struct ItemPatch {
//     tags: Option<Vec<String>>,
// }

let mut item = Item { tags: Some(vec!["a".into()]) };

// `None` leaves the field unchanged.
item.apply(ItemPatch { tags: None });
assert_eq!(item.tags, Some(vec!["a".into()]));

// `Some(vec![])` still applies and clears the list.
item.apply(ItemPatch { tags: Some(vec![]) });
assert_eq!(item.tags, Some(vec![]));

Attributes

You can customize the generated structs by defining #[patch(...)], #[filler(...)], #[complex(...)] (catalyst feature), or #[catalyst(...)] (catalyst feature) attributes on the original struct or its fields. Two attribute namespaces are provided for the catalyst feature because we need to handle the behaviors of two structs simultaneously — the catalyst itself and the product (complex). In general, the complex macro takes precedence over the catalyst macro when any conflict arises.

Struct attributes

  • #[patch(name = "...")]: change the name of the generated patch struct.
  • #[patch(attribute(...))]: add attributes to the generated patch struct.
  • #[patch(attribute(derive(...)))]: add derives to the generated patch struct.
  • #[filler(attribute(...))]: add attributes to the generated filler struct.
  • #[catalyst(bind = "...")]: specify the base (substrate) structure. (catalyst feature)
  • #[catalyst(keep_field_attribute)]: pass all field attributes from a substrate or catalyst through to the complex, unless an override is explicitly specified for that field. (catalyst feature)
  • #[catalyst(exclude_field_attributes = ["..."])]: when keep_field_attribute is used, specifies attribute names to exclude from being passed through to the complex struct fields. For example, exclude_field_attributes = ["serde"] strips all #[serde(...)] field attributes from the substrate before they reach the complex. (catalyst feature)
  • #[complex(override_field_attribute("$substrate_field_name", ...))]: override a complex field attribute, for example serde(default = "default_str"). (catalyst feature)
  • #[complex(name = "...")]: change the name of the generated complex struct. (catalyst feature)
  • #[complex(attribute(...))]: add attributes to the generated complex struct. (catalyst feature)

Field attributes

  • #[patch(skip)]: skip the field in the generated patch struct.
  • #[patch(name = "...")]: change the type of the field in the generated patch struct.
  • #[patch(attribute(...))]: add attributes to the field in the generated patch struct.
  • #[patch(attribute(derive(...)))]: add derives to the field in the generated patch struct.
  • #[patch(empty_value = ...)]: define a value as empty, so the corresponding field of the patch will not be wrapped by Option, and the patch is applied when the field differs from the empty value.
  • #[patch(skip_wrap)]: keep the field type as-is in the patch struct (no extra Option wrapping). Useful when the field is already Option<...> (for example Option<Vec<_>>) and you do not want a double-Option in the patch. With skip_wrap, None in the patch means "no change" and Some(v) sets the field to Some(v) (including Some(vec![]) to clear the vector). Cannot be combined with empty_value.
  • #[patch(nesting)]: treat the field as a nested patchable struct. The inner struct must also derive Patch. Requires the nesting feature.
  • #[patch(addable)]: allow conflicting patches to add their values together with the + operator instead of panicking. Requires the op feature.
  • #[patch(add = fn)]: like addable, but use the specified function to combine values. Requires the op feature.
  • #[filler(extendable)]: use the field as an extendable collection for the filler. The field type needs to implement Default, Extend, IntoIterator, and have an is_empty method.
  • #[filler(empty_value = ...)]: define a value as empty, so the corresponding field of the filler will be applied even when the field is not Option or extendable.
  • #[filler(addable)]: allow conflicting fillers to add/extend their values together with the + operator instead of panicking. Requires the op feature.
  • #[complex(attribute(...))]: add attributes to the field in the generated complex struct. (catalyst feature)

Please check the traits documentation to learn more.

Examples

The examples demonstrate the following scenarios:

  • diff two instances for a patch (diff.rs)
  • create a patch from a JSON string (json.rs)
  • rename the patch structure (rename-patch-struct.rs)
  • check whether a patch is empty (status.rs)
  • add attributes to a patch struct (patch-attr.rs)
  • show option field behavior (option.rs)
  • show operators on patches (op.rs)
  • show example with serde crates, e.g. humantime_serde for durations (time.rs)
  • show a patch nesting another patch (nesting.rs)
  • show filler with all possible types (filler.rs)
  • show operators on fillers (filler-op.rs)
  • show skip_wrap field behavior (instance.rs)
  • use Patch with clap for command-line config (clap.rs)

Features

This crate includes the following optional features:

  • status (default): implements the Status trait for the patch struct, which provides the is_empty method.
  • op (default): provides the << operator between an instance and a patch/filler, and the + operator for patches/fillers.
    • By default, when there is a field conflict between patches/fillers, + will add them together if #[patch(addable)], #[patch(add = fn)], or #[filler(addable)] is provided; otherwise it will panic.
  • merge (optional): implements the Merge trait for the patch struct, which provides the merge method, and << (if op is enabled) between patches.
  • alloc (optional): enables alloc support for no_std + alloc environments.
  • std (optional): enables std-dependent features (implies box and option).
  • box (optional): implements the Patch<Box<P>> trait for T where T implements Patch<P>. This lets you patch a boxed (or unboxed) struct with a boxed patch.
  • option (optional): implements the Patch<Option<P>> trait for Option<T> where T implements Patch<P>. Please take a look at the example to learn more.
    • default behavior: T needs to implement From<P>. When patching on None, it converts the patch into T via From<P>, letting you patch structs containing fields with optional values.
    • none_as_default (optional): T needs to implement Default. When patching on None, it patches on a default instance. Mutually exclusive with keep_none.
    • keep_none (optional): when patching on None, it stays None. Mutually exclusive with none_as_default.
  • nesting (optional): allows a field to use Patch derive with the #[patch(nesting)] attribute.
  • catalyst (optional): enables the Substrate, Catalyst, and Complex derive macros for extending a struct with fields from another crate.
  • unsafe (optional): uses ManuallyDrop + ptr::read / MaybeUninit + ptr::write in the generated bind, decouple, __substrate_new, and __substrate_unpack to avoid memory moves. Only meaningful with the catalyst feature.

About

A lib help you patch Rust instance, and easy to partial update configures.

Topics

Resources

License

Stars

47 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors