Skip to content

Commit 8d3a23c

Browse files
committed
Initial commit
0 parents  commit 8d3a23c

8 files changed

Lines changed: 1676 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
branches: [main, master]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
12+
jobs:
13+
fmt:
14+
name: Format
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: dtolnay/rust-toolchain@stable
19+
with:
20+
components: rustfmt
21+
- run: cargo fmt --all -- --check
22+
23+
clippy:
24+
name: Clippy
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v4
28+
- uses: dtolnay/rust-toolchain@stable
29+
with:
30+
components: clippy
31+
- run: cargo clippy --all-targets --all-features -- -D warnings
32+
33+
test:
34+
name: Test (${{ matrix.features }})
35+
runs-on: ubuntu-latest
36+
strategy:
37+
matrix:
38+
features:
39+
- ""
40+
- "--no-default-features"
41+
- "--all-features"
42+
steps:
43+
- uses: actions/checkout@v4
44+
- uses: dtolnay/rust-toolchain@stable
45+
- run: cargo test ${{ matrix.features }}
46+
47+
doc:
48+
name: Documentation
49+
runs-on: ubuntu-latest
50+
steps:
51+
- uses: actions/checkout@v4
52+
- uses: dtolnay/rust-toolchain@stable
53+
- run: cargo doc --no-deps --all-features
54+
env:
55+
RUSTDOCFLAGS: -D warnings
56+
57+
no-std:
58+
name: no_std Check
59+
runs-on: ubuntu-latest
60+
steps:
61+
- uses: actions/checkout@v4
62+
- uses: dtolnay/rust-toolchain@stable
63+
- run: cargo check --no-default-features

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

Cargo.lock

Lines changed: 107 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[package]
2+
name = "value-extra"
3+
version = "0.1.0"
4+
edition = "2024"
5+
description = "A tri-state Patch<T> type for partial update semantics — distinguishing between 'has value', 'absent', and 'explicitly null'"
6+
license = "MIT"
7+
repository = "https://github.com/itsfoxstudio/value-extra"
8+
documentation = "https://docs.rs/value-extra"
9+
keywords = ["patch", "option", "update", "serde", "api"]
10+
categories = ["data-structures", "rust-patterns"]
11+
authors = ["Fox Studio (Oskar Cieslik)"]
12+
readme = "README.md"
13+
14+
[features]
15+
default = ["std"]
16+
std = []
17+
serde = ["dep:serde"]
18+
19+
[dependencies]
20+
serde = { version = "1", default-features = false, optional = true }
21+
22+
[dev-dependencies]
23+
serde = { version = "1", features = ["derive"] }
24+
serde_json = "1"
25+
26+
[profile.release]
27+
lto = true
28+
codegen-units = 1
29+
opt-level = 3
30+
31+
[package.metadata.docs.rs]
32+
all-features = true
33+
rustdoc-args = ["--cfg", "docsrs"]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Fox Studio (Oskar Cieslik)
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# value-extra
2+
3+
[![Crates.io](https://img.shields.io/crates/v/value-extra.svg)](https://crates.io/crates/value-extra)
4+
[![Documentation](https://docs.rs/value-extra/badge.svg)](https://docs.rs/value-extra)
5+
[![License](https://img.shields.io/crates/l/value-extra.svg)](LICENSE)
6+
[![CI](https://github.com/itsfoxstudio/value-extra/actions/workflows/ci.yml/badge.svg)](https://github.com/itsfoxstudio/value-extra/actions/workflows/ci.yml)
7+
8+
A tri-state `Patch<T>` type for partial update semantics — distinguishing between "has value", "absent", and "explicitly null".
9+
10+
## Installation
11+
12+
```toml
13+
[dependencies]
14+
value-extra = "0.1"
15+
16+
# With serde support
17+
value-extra = { version = "0.1", features = ["serde"] }
18+
```
19+
20+
## Quick Start
21+
22+
```rust
23+
use value_extra::Patch;
24+
25+
fn apply_name_patch(current: &mut Option<String>, patch: Patch<String>) {
26+
match patch {
27+
Patch::Some(new) => *current = Some(new),
28+
Patch::None => *current = None, // explicitly clear
29+
Patch::Empty => {} // no change
30+
}
31+
}
32+
```
33+
34+
## The Problem
35+
36+
When handling partial updates (PATCH requests, config merging, etc.), `Option<T>` conflates two distinct states:
37+
38+
- **Field is absent** → don't touch the existing value
39+
- **Field is explicitly null** → clear/reset the value
40+
41+
This leads to bugs where missing fields accidentally clear data.
42+
43+
## The Solution
44+
45+
`Patch<T>` provides three states:
46+
47+
| JSON Input | Patch State | Meaning |
48+
|------------|-------------|---------|
49+
| `{ "name": "Alice" }` | `Patch::Some("Alice")` | Set to value |
50+
| `{ "name": null }` | `Patch::None` | Explicitly clear |
51+
| `{ }` | `Patch::Empty` | Leave unchanged |
52+
53+
## Serde Usage
54+
55+
```rust
56+
use value_extra::Patch;
57+
use serde::{Deserialize, Serialize};
58+
59+
#[derive(Deserialize, Serialize)]
60+
struct UserPatch {
61+
#[serde(default, skip_serializing_if = "Patch::is_empty")]
62+
name: Patch<String>,
63+
#[serde(default, skip_serializing_if = "Patch::is_empty")]
64+
email: Patch<String>,
65+
}
66+
67+
fn apply_user_patch(user: &mut User, patch: UserPatch) {
68+
if let Patch::Some(name) = patch.name {
69+
user.name = name;
70+
} else if patch.name.is_none() {
71+
user.name = String::new();
72+
}
73+
// ... same for email
74+
}
75+
```
76+
77+
## Option-like API
78+
79+
`Patch<T>` mirrors `Option<T>`'s API where it makes sense:
80+
81+
```rust
82+
use value_extra::Patch;
83+
84+
let patch = Patch::Some(42);
85+
86+
// Queries
87+
assert!(patch.is_some());
88+
assert!(!patch.is_empty());
89+
assert!(!patch.is_none());
90+
91+
// Transformations
92+
let doubled = patch.map(|x| x * 2);
93+
assert_eq!(doubled, Patch::Some(84));
94+
95+
// Fallbacks
96+
let value = Patch::<i32>::Empty.unwrap_or(10);
97+
assert_eq!(value, 10);
98+
```
99+
100+
## no_std Support
101+
102+
This crate is `no_std` compatible. Disable default features:
103+
104+
```toml
105+
[dependencies]
106+
value-extra = { version = "0.1", default-features = false }
107+
```
108+
109+
## License
110+
111+
MIT — see [LICENSE](LICENSE) for details.

0 commit comments

Comments
 (0)