use core::fmt;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::types::{DateTime, Validate, Violations};
use super::envelope::OcpiError;
use super::status::StatusCode;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(transparent, bound = "")]
pub struct Patch<T> {
body: Value,
#[serde(skip)]
_target: core::marker::PhantomData<fn() -> T>,
}
impl<T> Patch<T> {
#[must_use]
pub fn from_value(body: Value) -> Self {
Self { body, _target: core::marker::PhantomData }
}
pub fn from_partial<P: Serialize>(value: &P) -> Result<Self, serde_json::Error> {
Ok(Self::from_value(serde_json::to_value(value)?))
}
#[must_use]
pub const fn as_value(&self) -> &Value {
&self.body
}
#[must_use]
pub fn into_value(self) -> Value {
self.body
}
#[must_use]
pub fn touches(&self, field: &str) -> bool {
self.body.as_object().is_some_and(|o| o.contains_key(field))
}
#[must_use]
pub fn last_updated(&self) -> Option<DateTime> {
self.body.get("last_updated")?.as_str()?.parse().ok()
}
#[must_use]
pub fn retype<U>(self) -> Patch<U> {
Patch::from_value(self.body)
}
#[must_use]
pub fn fields(&self) -> Vec<&str> {
self.body.as_object().map(|o| o.keys().map(String::as_str).collect()).unwrap_or_default()
}
}
impl<T> Patch<T>
where
T: Serialize + serde::de::DeserializeOwned + Validate,
{
pub fn apply(&self, target: &T) -> Result<T, OcpiError> {
if self.last_updated().is_none() {
return Err(OcpiError::Decode {
path: "/last_updated".to_owned(),
message: format!("a PATCH must carry `last_updated` ({})", StatusCode::INVALID_PARAMETERS),
});
}
let mut merged = serde_json::to_value(target)
.map_err(|e| OcpiError::Decode { path: "/".to_owned(), message: e.to_string() })?;
merge(&mut merged, &self.body);
let updated: T = serde_json::from_value(merged).map_err(|e| OcpiError::Decode {
path: "/".to_owned(),
message: format!(
"the patched object is no longer a valid object: {e}; \
a PATCH may not remove a required field"
),
})?;
updated.validate().map_err(|violations: Violations| OcpiError::Decode {
path: violations.as_slice().first().map_or("/", |v| v.pointer.as_str()).to_owned(),
message: format!("the patched object no longer conforms: {violations}"),
})?;
Ok(updated)
}
}
impl<T> fmt::Display for Patch<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.body)
}
}
pub fn merge(target: &mut Value, patch: &Value) {
let Some(patch_object) = patch.as_object() else {
*target = patch.clone();
return;
};
if !target.is_object() {
*target = Value::Object(Map::new());
}
let target_object = target.as_object_mut().expect("just replaced with an object");
for (key, value) in patch_object {
if value.is_null() {
target_object.remove(key);
} else {
merge(target_object.entry(key.clone()).or_insert(Value::Null), value);
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PatchFallback {
GetThenReconcile,
PutWholeObject,
}
#[must_use]
pub fn patch_fallback(error: &OcpiError) -> PatchFallback {
match error {
OcpiError::NotFound(_) => PatchFallback::PutWholeObject,
_ => PatchFallback::GetThenReconcile,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn merge_follows_rfc_7396() {
let cases = [
(json!({"a": "b"}), json!({"a": "c"}), json!({"a": "c"})),
(json!({"a": "b"}), json!({"b": "c"}), json!({"a": "b", "b": "c"})),
(json!({"a": "b"}), json!({"a": null}), json!({})),
(json!({"a": "b", "b": "c"}), json!({"a": null}), json!({"b": "c"})),
(json!({"a": [{"b": "c"}]}), json!({"a": [1]}), json!({"a": [1]})),
(json!({"a": {"b": "c"}}), json!({"a": {"b": "d"}}), json!({"a": {"b": "d"}})),
(json!({"a": [{"b": "c"}]}), json!({"a": "replaced"}), json!({"a": "replaced"})),
];
for (mut target, patch, expected) in cases {
merge(&mut target, &patch);
assert_eq!(target, expected);
}
}
#[test]
fn merging_a_non_object_patch_replaces_the_target() {
let mut target = json!({"a": 1});
merge(&mut target, &json!("scalar"));
assert_eq!(target, json!("scalar"));
}
#[test]
fn the_fallback_matches_the_spec_advice() {
assert_eq!(
patch_fallback(&OcpiError::NotFound("no such EVSE".into())),
PatchFallback::PutWholeObject
);
assert_eq!(patch_fallback(&OcpiError::Transport("timeout".into())), PatchFallback::GetThenReconcile);
}
#[test]
fn a_patch_reports_the_fields_it_writes_including_nulls() {
let patch: Patch<()> = Patch::from_value(json!({"status": "CHARGING", "name": null}));
assert!(patch.touches("status") && patch.touches("name"));
assert!(!patch.touches("id"));
let mut fields = patch.fields();
fields.sort_unstable();
assert_eq!(fields, vec!["name", "status"]);
}
}