mirror of
https://github.com/telemt/telemt.git
synced 2026-04-25 14:34:10 +03:00
feat(api): add Patch<T> enum for JSON merge-patch semantics
Introduce a three-state Patch<T> (Unchanged / Remove / Set) and a
serde helper patch_field that distinguishes an omitted JSON field
from an explicit null. Wired up next as the field type for the
removable settings on PATCH /v1/users/{user}.
This commit is contained in:
@@ -28,6 +28,7 @@ mod config_store;
|
|||||||
mod events;
|
mod events;
|
||||||
mod http_utils;
|
mod http_utils;
|
||||||
mod model;
|
mod model;
|
||||||
|
mod patch;
|
||||||
mod runtime_edge;
|
mod runtime_edge;
|
||||||
mod runtime_init;
|
mod runtime_init;
|
||||||
mod runtime_min;
|
mod runtime_min;
|
||||||
|
|||||||
79
src/api/patch.rs
Normal file
79
src/api/patch.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// Three-state field for JSON Merge Patch semantics on the `PATCH /v1/users/{user}`
|
||||||
|
/// endpoint.
|
||||||
|
///
|
||||||
|
/// `Unchanged` is produced when the JSON body omits the field entirely and tells the
|
||||||
|
/// handler to leave the corresponding configuration entry untouched. `Remove` is
|
||||||
|
/// produced when the JSON body sets the field to `null` and instructs the handler to
|
||||||
|
/// drop the entry from the corresponding access HashMap. `Set` carries an explicit
|
||||||
|
/// new value, including zero, which is preserved verbatim in the configuration.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(super) enum Patch<T> {
|
||||||
|
Unchanged,
|
||||||
|
Remove,
|
||||||
|
Set(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for Patch<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Unchanged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serde deserializer adapter for fields that follow JSON Merge Patch semantics.
|
||||||
|
///
|
||||||
|
/// Pair this with `#[serde(default, deserialize_with = "patch_field")]` on a
|
||||||
|
/// `Patch<T>` field. An omitted field falls back to `Patch::Unchanged` via
|
||||||
|
/// `Default`; an explicit JSON `null` becomes `Patch::Remove`; any other value
|
||||||
|
/// becomes `Patch::Set(v)`.
|
||||||
|
pub(super) fn patch_field<'de, D, T>(deserializer: D) -> Result<Patch<T>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
T: serde::Deserialize<'de>,
|
||||||
|
{
|
||||||
|
Option::<T>::deserialize(deserializer).map(|opt| match opt {
|
||||||
|
Some(value) => Patch::Set(value),
|
||||||
|
None => Patch::Remove,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Holder {
|
||||||
|
#[serde(default, deserialize_with = "patch_field")]
|
||||||
|
value: Patch<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(json: &str) -> Holder {
|
||||||
|
serde_json::from_str(json).expect("valid json")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn omitted_field_yields_unchanged() {
|
||||||
|
let h = parse("{}");
|
||||||
|
assert!(matches!(h.value, Patch::Unchanged));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_null_yields_remove() {
|
||||||
|
let h = parse(r#"{"value": null}"#);
|
||||||
|
assert!(matches!(h.value, Patch::Remove));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_value_yields_set() {
|
||||||
|
let h = parse(r#"{"value": 42}"#);
|
||||||
|
assert!(matches!(h.value, Patch::Set(42)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_zero_yields_set_zero() {
|
||||||
|
let h = parse(r#"{"value": 0}"#);
|
||||||
|
assert!(matches!(h.value, Patch::Set(0)));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user