Skip to main content

ocpi_kit/transport/
patch.rs

1//! PATCH: JSON Merge Patch with the two rules OCPI adds on top.
2//!
3//! > *A PATCH request must only specify the object's identifier (if needed to identify this
4//! > object) and the fields to be updated. Any fields (both required or optional) that are left
5//! > out remain unchanged.*
6//!
7//! That is [RFC 7396 JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7396), plus:
8//!
9//! 1. `last_updated` is required in every PATCH — a patch without it is
10//!    [`StatusCode::INVALID_PARAMETERS`](super::StatusCode::INVALID_PARAMETERS), the spec's own
11//!    example of what 2001 means.
12//! 2. The result must still be a valid object, so a patch that nulls a required field is refused
13//!    rather than applied.
14//!
15//! Spec: 2.3.0 §transport_and_format_patch, §status_codes_2xxx_client_errors
16
17use core::fmt;
18
19use serde::{Deserialize, Serialize};
20use serde_json::{Map, Value};
21
22use crate::types::{DateTime, Validate, Violations};
23
24use super::envelope::OcpiError;
25use super::status::StatusCode;
26
27/// A partial update to an OCPI object.
28///
29/// Held as a JSON object rather than as a per-field `Option` struct, because that is what a merge
30/// patch is: the difference between "absent" and "present and null" is the whole semantics, and
31/// a struct of `Option`s cannot express it.
32///
33/// ```
34/// use ocpi_kit::transport::Patch;
35/// use ocpi_kit::v2_3_0::locations::Evse;
36///
37/// let patch: Patch<Evse> = serde_json::from_str(
38///     r#"{"status":"CHARGING","last_updated":"2019-06-24T12:39:09Z"}"#,
39/// ).unwrap();
40/// assert!(patch.last_updated().is_some());
41/// assert!(patch.touches("status"));
42/// ```
43///
44/// Spec: 2.3.0 §transport_and_format_patch
45#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
46#[serde(transparent, bound = "")]
47pub struct Patch<T> {
48    body: Value,
49    #[serde(skip)]
50    _target: core::marker::PhantomData<fn() -> T>,
51}
52
53impl<T> Patch<T> {
54    /// Wraps a JSON value as a patch.
55    #[must_use]
56    pub fn from_value(body: Value) -> Self {
57        Self { body, _target: core::marker::PhantomData }
58    }
59
60    /// Builds a patch by serialising `value`, keeping only the fields it writes.
61    ///
62    /// # Errors
63    ///
64    /// Propagates the `serde_json` error if `value` cannot be serialised.
65    pub fn from_partial<P: Serialize>(value: &P) -> Result<Self, serde_json::Error> {
66        Ok(Self::from_value(serde_json::to_value(value)?))
67    }
68
69    /// The patch as a JSON value.
70    #[must_use]
71    pub const fn as_value(&self) -> &Value {
72        &self.body
73    }
74
75    /// Consumes the patch and yields the JSON value.
76    #[must_use]
77    pub fn into_value(self) -> Value {
78        self.body
79    }
80
81    /// Whether the patch writes `field` at the top level, including writing it to `null`.
82    #[must_use]
83    pub fn touches(&self, field: &str) -> bool {
84        self.body.as_object().is_some_and(|o| o.contains_key(field))
85    }
86
87    /// The `last_updated` the patch carries, if any.
88    #[must_use]
89    pub fn last_updated(&self) -> Option<DateTime> {
90        self.body.get("last_updated")?.as_str()?.parse().ok()
91    }
92
93    /// Re-types this patch to the object it is meant to be applied to.
94    ///
95    /// A merge patch is untyped on the wire, so a server extractor produces a
96    /// `Patch<serde_json::Value>`; this names the type it will be applied to, which is what
97    /// [`Patch::apply`] needs in order to check the result.
98    #[must_use]
99    pub fn retype<U>(self) -> Patch<U> {
100        Patch::from_value(self.body)
101    }
102
103    /// The fields the patch writes, at the top level.
104    #[must_use]
105    pub fn fields(&self) -> Vec<&str> {
106        self.body.as_object().map(|o| o.keys().map(String::as_str).collect()).unwrap_or_default()
107    }
108}
109
110impl<T> Patch<T>
111where
112    T: Serialize + serde::de::DeserializeOwned + Validate,
113{
114    /// Applies this patch to `target`, returning the updated object.
115    ///
116    /// The whole operation is checked before anything is returned:
117    ///
118    /// * a patch without `last_updated` is refused with `2001`, as the spec's own example says;
119    /// * the merged object must still deserialise into `T`, so a patch that removes a required
120    ///   field is refused rather than producing a half-object;
121    /// * the merged object must still satisfy [`Validate`], so a patch cannot turn a conformant
122    ///   object into a non-conformant one.
123    ///
124    /// # Errors
125    ///
126    /// Returns [`OcpiError::Decode`] with a `2001`-shaped message when any of those fail.
127    pub fn apply(&self, target: &T) -> Result<T, OcpiError> {
128        if self.last_updated().is_none() {
129            return Err(OcpiError::Decode {
130                path: "/last_updated".to_owned(),
131                message: format!("a PATCH must carry `last_updated` ({})", StatusCode::INVALID_PARAMETERS),
132            });
133        }
134
135        let mut merged = serde_json::to_value(target)
136            .map_err(|e| OcpiError::Decode { path: "/".to_owned(), message: e.to_string() })?;
137        merge(&mut merged, &self.body);
138
139        let updated: T = serde_json::from_value(merged).map_err(|e| OcpiError::Decode {
140            path: "/".to_owned(),
141            message: format!(
142                "the patched object is no longer a valid object: {e}; \
143                 a PATCH may not remove a required field"
144            ),
145        })?;
146
147        updated.validate().map_err(|violations: Violations| OcpiError::Decode {
148            path: violations.as_slice().first().map_or("/", |v| v.pointer.as_str()).to_owned(),
149            message: format!("the patched object no longer conforms: {violations}"),
150        })?;
151
152        Ok(updated)
153    }
154}
155
156impl<T> fmt::Display for Patch<T> {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        write!(f, "{}", self.body)
159    }
160}
161
162/// Applies an RFC 7396 JSON Merge Patch in place.
163///
164/// > *If the patch is anything other than an object, the result will always be to replace the
165/// > entire target with the entire patch. Also, it is not possible to patch part of a target that
166/// > is not an object … null values in the merge patch are given special meaning to indicate the
167/// > removal of existing values in the target.*
168///
169/// This is exported because a hub needs it to apply a patch it is forwarding without knowing the
170/// object's type.
171pub fn merge(target: &mut Value, patch: &Value) {
172    let Some(patch_object) = patch.as_object() else {
173        *target = patch.clone();
174        return;
175    };
176    if !target.is_object() {
177        *target = Value::Object(Map::new());
178    }
179    let target_object = target.as_object_mut().expect("just replaced with an object");
180    for (key, value) in patch_object {
181        if value.is_null() {
182            target_object.remove(key);
183        } else {
184            merge(target_object.entry(key.clone()).or_insert(Value::Null), value);
185        }
186    }
187}
188
189/// What a client should do after a PATCH that the peer refused.
190///
191/// > *In case a PATCH request fails, the client is expected to call the GET method to check the
192/// > state of the object in the other party's system. If the object doesn't exist, the client
193/// > should do a PUT.*
194///
195/// Spec: 2.3.0 §transport_and_format_patch
196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub enum PatchFallback {
198    /// GET the object to see what the peer has.
199    GetThenReconcile,
200    /// The object does not exist at the peer; PUT the whole object.
201    PutWholeObject,
202}
203
204/// Decides the fallback for a failed PATCH from the error the peer returned.
205#[must_use]
206pub fn patch_fallback(error: &OcpiError) -> PatchFallback {
207    match error {
208        OcpiError::NotFound(_) => PatchFallback::PutWholeObject,
209        _ => PatchFallback::GetThenReconcile,
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use serde_json::json;
217
218    #[test]
219    fn merge_follows_rfc_7396() {
220        // The RFC's own example table.
221        let cases = [
222            (json!({"a": "b"}), json!({"a": "c"}), json!({"a": "c"})),
223            (json!({"a": "b"}), json!({"b": "c"}), json!({"a": "b", "b": "c"})),
224            (json!({"a": "b"}), json!({"a": null}), json!({})),
225            (json!({"a": "b", "b": "c"}), json!({"a": null}), json!({"b": "c"})),
226            (json!({"a": [{"b": "c"}]}), json!({"a": [1]}), json!({"a": [1]})),
227            (json!({"a": {"b": "c"}}), json!({"a": {"b": "d"}}), json!({"a": {"b": "d"}})),
228            (json!({"a": [{"b": "c"}]}), json!({"a": "replaced"}), json!({"a": "replaced"})),
229        ];
230        for (mut target, patch, expected) in cases {
231            merge(&mut target, &patch);
232            assert_eq!(target, expected);
233        }
234    }
235
236    #[test]
237    fn merging_a_non_object_patch_replaces_the_target() {
238        let mut target = json!({"a": 1});
239        merge(&mut target, &json!("scalar"));
240        assert_eq!(target, json!("scalar"));
241    }
242
243    #[test]
244    fn the_fallback_matches_the_spec_advice() {
245        assert_eq!(
246            patch_fallback(&OcpiError::NotFound("no such EVSE".into())),
247            PatchFallback::PutWholeObject
248        );
249        assert_eq!(patch_fallback(&OcpiError::Transport("timeout".into())), PatchFallback::GetThenReconcile);
250    }
251
252    #[test]
253    fn a_patch_reports_the_fields_it_writes_including_nulls() {
254        let patch: Patch<()> = Patch::from_value(json!({"status": "CHARGING", "name": null}));
255        assert!(patch.touches("status") && patch.touches("name"));
256        assert!(!patch.touches("id"));
257        let mut fields = patch.fields();
258        fields.sort_unstable();
259        assert_eq!(fields, vec!["name", "status"]);
260    }
261}