Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,6 @@ jobs:
cd no-std-examples
nix develop .#no-std -c cargo run --features=box --bin no-std-box
nix develop .#no-std -c cargo run --features=option --bin no-std-option

- name: Test with catalyst
nix develop .#ci -c check-catalyst
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ Cargo.lock
.direnv/
struct-patch/examples
no-std-examples/target
complex-example/target
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ members = [
"lib",
"derive",
]
exclude = [ "no-std-examples" ]
exclude = [
"no-std-examples",
"complex-example",
]

[workspace.package]
authors = ["Antonio Yang <yanganto@gmail.com>"]
Expand Down
55 changes: 53 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
[![MIT licensed][mit-badge]][mit-url]
[![Docs][doc-badge]][doc-url]

A lib help you patch Rust instance, and easy to partial update configures.
A lib help you modify the config struct, you can
- patch an instance, and easy to partial update with `Patch` derive macro
- fill up an instance with `Filler` derive macro
- extend with extra fields with `Substrate` and `Catalyst` derive macros

## Introduction
This crate provides the `Patch`, `Filler` traits and accompanying derive macro.
Expand All @@ -12,8 +15,11 @@ If the any field in the instance is none then it will try to fill the field with
Currently, `Filler` only support `Option` and `Vec` fields, and also you can check this [template](https://github.com/yanganto/ConfigTemplate)
if you already work on a big project with a lot of configs.
This crate support `no_std`, please check [no-std-examples](./no-std-examples).
When extending a config, the base struct should be expose in the build script with `Substrate` trait, then a catalyst struct can bind and produce complex struct,
please check [complex-example](./complex-example) and the [Quick Example: case 3](#case-3---extend-a-struct-from-a-crate).

## Quick Example
#### Case 1 - Patch on a Config
Deriving `Patch` on a struct will generate 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.
```rust
Expand Down Expand Up @@ -65,6 +71,7 @@ fn patch_json() {
}
```

#### Case 2 - Fill up on a Config
Deriving `Filler` on a struct will generate a struct similar to the original one with the field with `Option`. Unlike `Patch`, the `Filler` only work on the empty fields of instance.

```rust
Expand Down Expand Up @@ -99,13 +106,57 @@ 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 will help you expose the field information, and you can easy to expose in build.rs of other crate.
Deriving `Catalyst` on can read the field information of Substrate and generate a new Complex struct.
All the fields in substrate and catalyst need be public, and the fields in complex are also public.
The overall behavior likes chemical catalysts, a catalyst **bind** on a substrate to form a complex struct, which has all fields from substrate and catalyst.
Also, a complex can **decouple** and return a catalyst and substrate, please check [complex-example](./complex-example/catalyst/src/lib.rs).

```rust
/// In $dependency_crate/src/lib.rs
use struct_patch::Substrate;
#[derive(Substrate)]
pub struct Base {
pub field_bool: bool,
pub field_string: String,
}

/// In $main_crate/src/build.rs
use struct_patch::Substrate;

fn main() {
$dependency_crate::Base::expose();
}

/// In $main_crate/src/lib.rs
use struct_patch::Catalyst;

#[derive(Catalyst)]
#[catalyst(bind = substrate::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>,
//}
```

## Documentation and Examples
Also, you can modify the patch structure by defining `#[patch(...)]` or `#[filler(...)]` attributes on the original struct or fields.
Also, you can modify the patch structure by defining `#[patch(...)]`, `#[filler(...)]` or `#[complex(...)]`, `#[catalyst(...)]` attributes on the original struct or fields.

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.
- `#[catalyst(bind = "...")]`: decide the base structure. (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.
Expand Down
20 changes: 20 additions & 0 deletions complex-example/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[workspace]
resolver = "2"
members = [
"substrate",
"catalyst",
]
[workspace.dependencies]
struct-patch = { path = "../lib", features = ["catalyst"] }

[workspace.package]
authors = ["Antonio Yang <yanganto@gmail.com>"]
version = "0.11.0"
edition = "2021"
categories = ["development-tools"]
keywords = ["struct", "patch", "macro", "derive", "overlay"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/yanganto/struct-patch/"
description = "A library that helps you implement partial updates for your structs."
rust-version = "1.61.0"
20 changes: 20 additions & 0 deletions complex-example/catalyst/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "catalyst"
authors.workspace = true
version.workspace = true
edition.workspace = true
categories.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
description.workspace = true
rust-version.workspace = true

[dependencies]
struct-patch.workspace = true
substrate = { path = "../substrate" }

[build-dependencies]
struct-patch.workspace = true
substrate = { path = "../substrate" }
5 changes: 5 additions & 0 deletions complex-example/catalyst/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use struct_patch::Substrate;

fn main() {
substrate::Base::expose();
}
52 changes: 52 additions & 0 deletions complex-example/catalyst/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use struct_patch::Catalyst;
use substrate::Base;

#[derive(Default, Catalyst)]
#[catalyst(bind = Base)]
#[allow(dead_code)]
struct Amyloid {
pub extra_bool: bool,
pub extra_string: String,
pub extra_option: Option<usize>,
}

#[derive(Catalyst)]
#[catalyst(bind = Base)]
#[complex(name = "SmallCpx")]
#[allow(dead_code)]
#[complex(attribute(derive(Default)))]
struct SmallAmyloid {
pub extra_bool: bool,
}

#[allow(dead_code)]
impl SmallCpx {
/// A reaction to change the substrate
pub fn reaction(&mut self) {
self.field_bool = true;
}
}

#[cfg(test)]
mod tests {
use super::*;
use struct_patch::Complex;

#[test]
fn complex_works() {
let mut small_complex = SmallCpx::default();
assert_eq!(small_complex.field_bool, false);
assert_eq!(small_complex.field_string, String::new());
assert_eq!(small_complex.field_option, None);
assert_eq!(small_complex.extra_bool, false);

small_complex.reaction();

let (_cat, substrate) = small_complex.decouple();
assert!(substrate.has_bool());

let amyloid = Amyloid::default();
let complex = amyloid.bind(substrate);
assert_eq!(complex.field_bool, true);
}
}
19 changes: 19 additions & 0 deletions complex-example/substrate/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "substrate"
authors.workspace = true
version.workspace = true
edition.workspace = true
categories.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
description.workspace = true
rust-version.workspace = true

[dependencies]
struct-patch.workspace = true

[dev-dependencies]
syn = { version = "2", features = ["full"] }
syn-serde = { version = "0.3.2", features = ["json"] }
30 changes: 30 additions & 0 deletions complex-example/substrate/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#![allow(unused)]
use struct_patch::Substrate;

#[derive(Default, Substrate)]
pub struct Base {
pub field_bool: bool,
pub field_string: String,
pub field_option: Option<usize>,
}

impl Base {
pub fn has_bool(&self) -> bool {
self.field_bool
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn expose_works() {
assert_eq!(
Base::expose_content(),
r#"{"named":[{"vis":"pub","ident":"field_bool","colon_token":true,"ty":{"path":{"segments":[{"ident":"bool"}]}}},{"vis":"pub","ident":"field_string","colon_token":true,"ty":{"path":{"segments":[{"ident":"String"}]}}},{"vis":"pub","ident":"field_option","colon_token":true,"ty":{"path":{"segments":[{"ident":"Option","arguments":{"angle_bracketed":{"args":[{"type":{"path":{"segments":[{"ident":"usize"}]}}}]}}}]}}}]}"#
);

let _fields: syn::Fields = syn_serde::json::from_str(&Base::expose_content()).unwrap();
}
}
2 changes: 2 additions & 0 deletions derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ proc-macro = true
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["parsing"] }
syn-serde = { version = "0.3.2", features = ["json"], optional = true }

[features]
status = []
op = []
merge = []
nesting = []
catalyst = [ "syn-serde" ]

[dev-dependencies]
pretty_assertions_sorted = "1.2.3"
Loading