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
12 changes: 10 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,18 @@ updates:
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/no-std-examples"
directory: "/examples/no-std-examples"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/complex-example"
directory: "/examples/complex-examples"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/examples/patch-examples"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/examples/filler-examples"
schedule:
interval: "weekly"
3 changes: 3 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ jobs:
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}

- name: Nix flake check
run: nix flake check

- name: fmt
run: nix develop .#ci -c cargo fmt

Expand Down
76 changes: 20 additions & 56 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,69 +17,33 @@ jobs:
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}

- name: Test no default features
- name: Build devshells
run: |
nix develop .#ci -c cargo run --quiet --no-default-features --example instance
nix develop .#ci -c cargo run --quiet --no-default-features --example filler
nix develop .#ci -c cargo run --quiet --no-default-features --example diff
nix develop .#ci -c cargo run --quiet --no-default-features --example json
nix develop .#ci -c cargo run --quiet --no-default-features --example rename-patch-struct
nix develop .#ci -c cargo run --quiet --no-default-features --example patch-attr
nix develop .#ci -c cargo run --quiet --no-default-features --example time
nix develop .#ci -c cargo run --quiet --no-default-features --example clap
nix develop .#ci -c cargo run --quiet --no-default-features --features=nesting --example nesting
nix develop .#ci -c cargo run --quiet --no-default-features --features=option --example option
nix develop .#ci -c cargo run --quiet --no-default-features --example=log
nix develop .#ci -c cargo run --quiet --no-default-features --example apply-by
nix develop .#ci -c cargo run --quiet --no-default-features --features=box --example box
nix develop .#ci -c cargo test --quiet --no-default-features
nix develop .#ci -c echo Build ci devshell
nix develop .#no-std -c echo Build no-std devshell

- name: Test with std features
- name: Test lib
run: |
nix develop .#ci -c cargo run --quiet --features=std --example instance
nix develop .#ci -c cargo run --quiet --features=std --example filler
nix develop .#ci -c cargo run --quiet --features=std --example filler-op
nix develop .#ci -c cargo run --quiet --features=std --example diff
nix develop .#ci -c cargo run --quiet --features=std --example json
nix develop .#ci -c cargo run --quiet --features=std --example rename-patch-struct
nix develop .#ci -c cargo run --quiet --features=std --example patch-attr
nix develop .#ci -c cargo run --quiet --features=std --example option
nix develop .#ci -c cargo run --quiet --features=std --example box
nix develop .#ci -c cargo run --quiet --features=std,nesting --example nesting
nix develop .#ci -c cargo test --quiet --features=std
nix develop .#ci -c test no-default
nix develop .#ci -c test std
nix develop .#ci -c test merge
nix develop .#ci -c test default

- name: Test with merge features
- name: Test patch examples
run: |
nix develop .#ci -c cargo run --quiet --features=option,merge --example option
nix develop .#ci -c cargo run --quiet --features=merge --example op
nix develop .#ci -c cargo run --quiet --features=merge,nesting --example nesting
nix develop .#ci -c cargo test --quiet --features=merge --no-default-features
nix develop .#ci -c cargo test --quiet --features=merge
nix develop .#ci -c check-patch no-default
nix develop .#ci -c check-patch std
nix develop .#ci -c check-patch merge
nix develop .#ci -c check-patch option
nix develop .#ci -c check-patch default

- name: Test with option features
- name: Test filler examples
run: |
nix develop .#ci -c cargo run --quiet --features=none_as_default --example option
nix develop .#ci -c cargo run --quiet --features=none_as_default,nesting --example nesting
nix develop .#ci -c cargo run --quiet --features=keep_none --example option
nix develop .#ci -c cargo run --quiet --features=keep_none,nesting --example nesting

- name: Test with default features
run: |
nix develop .#ci -c cargo run --quiet --example status
nix develop .#ci -c cargo run --quiet --example op
nix develop .#ci -c cargo run --quiet --example clap
nix develop .#ci -c cargo run --quiet --features=nesting --example nesting
nix develop .#ci -c cargo run --quiet --features=nesting --example clap
nix develop .#ci -c cargo run --quiet --example=log
nix develop .#ci -c cargo run --quiet --example apply-by
nix develop .#ci -c cargo run --quiet --features=box --example box
nix develop .#ci -c cargo test --quiet
nix develop .#ci -c check-filler no-default
nix develop .#ci -c check-filler default

- name: Test in no std
run: |
cd no-std-examples
nix develop .#no-std -c cargo run --quiet --features=box --bin no-std-box
nix develop .#no-std -c cargo run --quiet --features=option --bin no-std-option
run: nix develop .#no-std -c check-no-std

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

[workspace.package]
Expand Down
30 changes: 19 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ This crate provides the `Patch`, `Filler`, `Substrate`, `Catalyst` and `Complex`
- 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 `substrate` feature, the `Substrate` derive macro exposes field information so that other crates can access it. With the `catalyst` feature (which implies `substrate`), `Catalyst` and `Complex` derive macros help you extend a struct with extra fields from another crate.

This crate supports `no_std` — check the [no-std-examples](./no-std-examples).
This crate supports `no_std` — check the [no-std-examples](./examples/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](https://github.com/yanganto/ConfigTemplate).
- 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](./complex-example) and [Quick Example: case 3](#case-3---extend-a-struct-from-a-crate).
- 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-examples](./examples/complex-examples) and [Quick Example: case 3](#case-3---extend-a-struct-from-a-crate).

## Quick Example

#### Case 1 - Patch on a Config
#### Case 1 (Patch) - 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.
See also Case Studies:
Expand Down Expand Up @@ -82,7 +82,7 @@ fn patch_json() {
}
```

#### Case 2 - Fill up on a Config
#### Case 2 (Filler) - 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.
See also Case Studies:
- [Log which fields were patched or filled](docs/logs.md)
Expand Down Expand Up @@ -119,11 +119,11 @@ assert_eq!(item.maybe_field_int, Some(7));
assert_eq!(item.list, vec![7]);
```

#### Case 3 - Extend a struct from a crate
#### Case 3 (Complex) - Extend a struct from a crate
Deriving `Substrate` on a struct exposes the field information so that other crates can access it.
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](https://en.wikipedia.org/wiki/Enzyme_catalysis): 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](./complex-example/catalyst/src/lib.rs).
A complex can also **decouple** without cloning, returning the original catalyst and substrate. Check the [complex-examples](./examples/complex-examples/catalyst/src/lib.rs).
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). There are two ways for the downstream crate to read the substrate's field layout:
Expand Down Expand Up @@ -165,7 +165,7 @@ struct Amyloid {
// }
```

**Option B: via source code with `src` attribute**: point the `Catalyst` macro directly at the substrate crate's source file using `#[catalyst(src = "crate_name:/path/to/file.rs")]`. The macro uses `cargo_metadata` to locate the package and `syn` to parse the file, so no `build.rs` or `expose()` call is required. Check the [catalyst-src example](./complex-example/catalyst-src/src/lib.rs).
**Option B: via source code with `src` attribute**: point the `Catalyst` macro directly at the substrate crate's source file using `#[catalyst(src = "crate_name:/path/to/file.rs")]`. The macro uses `cargo_metadata` to locate the package and `syn` to parse the file, so no `build.rs` or `expose()` call is required. Check the [catalyst-src example](./examples/complex-examples/catalyst-src/src/lib.rs).

```rust
/// In the substrate crate (src/lib.rs)
Expand Down Expand Up @@ -231,7 +231,9 @@ Please check the [traits documentation][doc-traits] to learn more.

## Examples

The [examples][examples] demonstrate the following scenarios:
Examples are organised into focused sub-projects under [`examples/`](./examples):

**[patch-examples](./examples/patch-examples)** — `Patch` derive macro 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`)
Expand All @@ -241,14 +243,20 @@ The [examples][examples] demonstrate the following scenarios:
- 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`)
- demonstrate `default_log` and `apply_with_log` for both `Patch` and `Filler` (`log.rs`)
- apply a heap-allocated (boxed) patch and produce a boxed diff (`box.rs`)
- demonstrate `apply_by` for custom field-level apply logic, e.g. list concatenation (`apply-by.rs`)

**[filler-examples](./examples/filler-examples)** — `Filler` derive macro scenarios:
- show filler with all possible types (`filler.rs`)
- show operators on fillers (`filler-op.rs`)

**[no-std-examples](./examples/no-std-examples)** — `no_std` usage with a bare-metal target.

**[complex-examples](./examples/complex-examples)** — `Substrate` / `Catalyst` / `Complex` derive macros for extending a struct across crates.

## Features

This crate includes the following optional features:
Expand All @@ -275,4 +283,4 @@ This crate includes the following optional features:
[doc-badge]: https://img.shields.io/badge/docs-rs-orange.svg
[doc-url]: https://docs.rs/struct-patch/
[doc-traits]: https://docs.rs/struct-patch/latest/struct_patch/traits/trait.Patch.html#container-attributes
[examples]: /lib/examples
[examples]: /examples
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ members = [
"catalyst-src",
]
[workspace.dependencies]
struct-patch = { path = "../lib" }
struct-patch = { path = "../../lib" }

[workspace.package]
authors = ["Antonio Yang <yanganto@gmail.com>"]
Expand Down
14 changes: 14 additions & 0 deletions examples/filler-examples/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "filler-examples"
authors = ["Antonio Yang <yanganto@gmail.com>"]
version = "0.14.1"
edition = "2021"
license = "MIT"

[dependencies]
struct-patch = { path = "../../lib" }

[features]
default = ["status", "op"]
status = ["struct-patch/status"]
op = ["struct-patch/op"]
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ edition = "2021"
license = "MIT"

[dependencies]
struct-patch = { path = "../lib" }
struct-patch = { path = "../../lib" }
linked_list_allocator = "0.10"
cortex-m-rt = "0.7"
cortex-m-semihosting = "0.5"
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
32 changes: 32 additions & 0 deletions examples/patch-examples/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]
name = "patch-examples"
authors = ["Antonio Yang <yanganto@gmail.com>"]
version = "0.14.1"
edition = "2021"
license = "MIT"

[dependencies]
struct-patch = { path = "../../lib" }
serde_json = "1.0"
serde = { version = "1", features = ["derive"] }
serde_with = "3.9.0"
toml = "1.1.2"
humantime-serde = "1.1.1"
clap = { version = "4.4.7", features = ["derive"] }

[features]
default = ["status", "op"]
status = ["struct-patch/status"]
op = ["struct-patch/op"]
merge = ["struct-patch/merge"]
alloc = ["struct-patch/alloc"]
std = ["box", "option"]
box = ["alloc", "struct-patch/box"]
option = ["struct-patch/option"]
nesting = ["struct-patch/nesting"]
none_as_default = ["option", "struct-patch/none_as_default"]
keep_none = ["option", "struct-patch/keep_none"]

[[example]]
name = "box"
required-features = ["box"]
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
79 changes: 25 additions & 54 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,64 +13,35 @@
pkgs = import nixpkgs {
inherit system overlays;
};
publishScript = pkgs.writeShellScriptBin "crate-publish" ''
cargo login $1
cargo publish -p struct-patch-derive || echo "publish struct-patch-derive fail"
sleep 10
cargo publish -p struct-patch
'';
checkCatalystScript = pkgs.writeShellScriptBin "check-catalyst" ''
set -ex
cd $(git rev-parse --show-toplevel 2>/dev/null)
cd complex-example
cargo test --quiet -p substrate
cargo test --quiet -p catalyst
cargo test --quiet -p catalyst-src

echo "Run catatyst test with unsafe features"
cargo test --quiet -p catalyst --features unsafe
cargo test --quiet -p catalyst-src --features unsafe
publishScript = pkgs.writeShellScriptBin "crate-publish"
(builtins.readFile ./nix/scripts/crate-publish.sh);
checkNoStdScript = pkgs.writeShellScriptBin "check-no-std"
(builtins.readFile ./nix/scripts/check-no-std.sh);
checkComplexScript = pkgs.writeShellScriptBin "check-complex"
(builtins.readFile ./nix/scripts/check-complex.sh);
checkFillerScript = pkgs.writeShellScriptBin "check-filler"
(builtins.readFile ./nix/scripts/check-filler.sh);
checkPatchScript = pkgs.writeShellScriptBin "check-patch"
(builtins.readFile ./nix/scripts/check-patch.sh);
testScript = pkgs.writeShellScriptBin "test"
(builtins.readFile ./nix/scripts/test.sh);
PROMPT = ''
_git_ps1() {
git rev-parse --is-inside-work-tree &>/dev/null || return
local branch dirty
branch=$(git symbolic-ref --short HEAD 2>/dev/null)
[[ -n $(git status --porcelain) ]] && dirty='*'
echo "<$branch$dirty>"
}
PS1='\[\e[33m\][$DEVSHELL] \w $(_git_ps1) \$\[\e[0m\] '
'';
in
with pkgs;
{
devShells = let
noStdRust = rust-bin.stable.latest.default.override {
targets = [
"thumbv7m-none-eabi"
];
extensions = [ "rust-src" "llvm-tools-preview" ];
};
in
{
default = mkShell {
buildInputs = [
rust-bin.stable.latest.minimal
openssl
pkg-config

checkCatalystScript
];
};

ci = mkShell {
buildInputs = [
rust-bin.stable.latest.default
openssl
pkg-config

publishScript

checkCatalystScript
];
};

no-std = mkShell {
buildInputs = [
noStdRust
qemu
];
};
devShells = {
default = import ./nix/shells/default.nix { inherit pkgs PROMPT; checkScripts = [ checkComplexScript checkFillerScript checkPatchScript testScript ]; };
ci = import ./nix/shells/ci.nix { inherit pkgs publishScript PROMPT; checkScripts = [ checkComplexScript checkFillerScript checkPatchScript testScript ]; };
no-std = import ./nix/shells/no-std.nix { inherit pkgs PROMPT; checkScripts = [ checkNoStdScript ]; };
};
}
);
Expand Down
Loading