use std::fmt::Display;
use derive_more::{Display as DeriveDisplay, Error as DeriveError};
use serde::ser::{Error as SerError, Serialize, SerializeMap, Serializer};
#[derive(Debug)]
pub struct Detailed<'a, T> {
value: &'a T,
}
impl<T> Clone for Detailed<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Detailed<'_, T> {}
impl<'a, T> Detailed<'a, T> {
#[must_use]
pub fn new(value: &'a T) -> Self {
Self { value }
}
}
impl<T> Serialize for Detailed<'_, T>
where
T: Serialize + Display,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let base = serde_json::to_value(self.value).map_err(SerError::custom)?;
let serde_json::Value::Object(fields) = base else {
return Err(SerError::custom(
"Detailed can only wrap values that serialize as a map",
));
};
let mut map = serializer.serialize_map(Some(fields.len().saturating_add(1)))?;
for (key, field_value) in &fields {
map.serialize_entry(key, field_value)?;
}
map.serialize_entry("formatted", &self.value.to_string())?;
map.end()
}
}
#[derive(Debug, DeriveDisplay, DeriveError)]
#[non_exhaustive]
pub enum DetailedError {
#[display("failed to serialize cutter value: {_0}")]
Serialize(serde_json::Error),
}
pub fn detailed_json<T>(value: &T) -> Result<serde_json::Value, DetailedError>
where
T: Serialize + Display,
{
serde_json::to_value(Detailed::new(value)).map_err(DetailedError::Serialize)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::catalog::models::cutter::{CutterLoc, CutterSanborn};
use std::error::Error as _;
#[test]
fn detailed_json_loc_has_parts_and_formatted() {
let cutter = CutterLoc::new('C', 378);
let object = detailed_json(&cutter).unwrap();
assert_eq!(object["alpha_prefix"], "C");
assert_eq!(object["numeric"], 378);
assert_eq!(object["formatted"], "C378");
assert_eq!(object.as_object().unwrap().len(), 3);
}
#[test]
fn detailed_json_sanborn_has_all_parts_and_formatted() {
let cutter = CutterSanborn::new("D".to_owned(), 548, Some("o".to_owned()), "Dickens");
let object = detailed_json(&cutter).unwrap();
assert_eq!(object["alpha_prefix"], "D");
assert_eq!(object["numeric"], 548);
assert_eq!(object["alpha_suffix"], "o");
assert_eq!(object["entry"], "Dickens");
assert_eq!(object["formatted"], "D548o");
assert_eq!(object.as_object().unwrap().len(), 5);
}
#[test]
fn sanborn_without_work_mark_keeps_null_suffix() {
let cutter = CutterSanborn::new("B".to_owned(), 732, None, "Borg");
let object = detailed_json(&cutter).unwrap();
assert_eq!(object["alpha_prefix"], "B");
assert_eq!(object["alpha_suffix"], serde_json::Value::Null);
assert_eq!(object["entry"], "Borg");
assert_eq!(object["formatted"], "B732");
}
#[test]
fn detailed_wrapper_matches_detailed_json() {
let cutter = CutterSanborn::new("D".to_owned(), 548, Some("o".to_owned()), "Dickens");
let via_wrapper = serde_json::to_value(Detailed::new(&cutter)).unwrap();
let via_helper = detailed_json(&cutter).unwrap();
assert_eq!(via_wrapper, via_helper);
}
#[test]
fn numeric_stays_an_integer_not_a_float() {
let object = detailed_json(&CutterLoc::new('C', 378)).unwrap();
assert!(object["numeric"].is_u64(), "numeric must remain an integer");
assert!(
!object["numeric"].is_f64(),
"numeric must not become a float"
);
}
#[test]
fn detailed_json_rejects_non_object_values() {
let error = detailed_json(&42_u32).unwrap_err();
assert!(matches!(error, DetailedError::Serialize(_)));
assert!(!error.to_string().is_empty());
assert!(error.source().is_some());
}
#[test]
fn formatted_is_the_final_key() {
let object = detailed_json(&CutterLoc::new('C', 378)).unwrap();
assert!(object.get("formatted").is_some());
}
#[test]
fn detailed_is_copy_even_when_wrapped_value_is_not() {
fn assert_copy<T: Copy>(_: &T) {}
let cutter = CutterSanborn::new("D".to_owned(), 548, Some("o".to_owned()), "Dickens");
let detailed = Detailed::new(&cutter);
let copied = detailed;
assert_copy(&detailed);
assert_eq!(
serde_json::to_value(detailed).unwrap(),
serde_json::to_value(copied).unwrap()
);
}
}