ocpi_kit/transport/
patch.rs1use 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#[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 #[must_use]
56 pub fn from_value(body: Value) -> Self {
57 Self { body, _target: core::marker::PhantomData }
58 }
59
60 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 #[must_use]
71 pub const fn as_value(&self) -> &Value {
72 &self.body
73 }
74
75 #[must_use]
77 pub fn into_value(self) -> Value {
78 self.body
79 }
80
81 #[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 #[must_use]
89 pub fn last_updated(&self) -> Option<DateTime> {
90 self.body.get("last_updated")?.as_str()?.parse().ok()
91 }
92
93 #[must_use]
99 pub fn retype<U>(self) -> Patch<U> {
100 Patch::from_value(self.body)
101 }
102
103 #[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 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
162pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub enum PatchFallback {
198 GetThenReconcile,
200 PutWholeObject,
202}
203
204#[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 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}