use serde::Serialize;
use serde_json::Value;
use sha2::{Digest, Sha256};
use crate::image::Image;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IdentityError(pub String);
impl std::fmt::Display for IdentityError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "request cannot be canonicalized: {}", self.0)
}
}
impl std::error::Error for IdentityError {}
pub fn canonical_json<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>, IdentityError> {
serde_jcs::to_vec(value).map_err(|e| IdentityError(e.to_string()))
}
pub fn digest<T: Serialize + ?Sized>(value: &T) -> Result<String, IdentityError> {
let digest = Sha256::digest(canonical_json(value)?);
Ok(digest.iter().map(|byte| format!("{byte:02x}")).collect())
}
pub fn item_digest<T: Serialize + ?Sized>(
item: &T,
images: Option<&[Image]>,
) -> Result<String, IdentityError> {
let mut value = serde_json::to_value(item).map_err(|e| IdentityError(e.to_string()))?;
if let (Some(images), Value::Object(members)) = (images, &mut value) {
let canonical = images
.iter()
.enumerate()
.map(|(j, image)| {
image
.canonical()
.map_err(|why| IdentityError(format!("images[{j}]: {why}")))
})
.collect::<Result<Vec<_>, _>>()?;
members.insert(
"images".into(),
serde_json::to_value(canonical).map_err(|e| IdentityError(e.to_string()))?,
);
}
digest(&value)
}
pub fn first_difference(
parts: &[(&str, &str, &str)],
recorded_items: &[String],
sent_items: &[String],
) -> Option<String> {
if let Some((name, _, _)) = parts.iter().find(|(_, recorded, sent)| recorded != sent) {
return Some((*name).to_owned());
}
if recorded_items.len() != sent_items.len() {
return Some("items".into());
}
recorded_items
.iter()
.zip(sent_items)
.position(|(recorded, sent)| recorded != sent)
.map(|i| format!("items[{i}]"))
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::image::InlineImage;
#[test]
fn number_spelling_and_key_order_do_not_change_a_digest() {
let float: Value = serde_json::from_str(r#"{"a":1.0,"b":{"y":[2.50],"x":0}}"#).unwrap();
let int: Value = serde_json::from_str(r#"{"b":{"x":0,"y":[2.5]},"a":1}"#).unwrap();
assert_ne!(
serde_json::to_string(&float).unwrap(),
serde_json::to_string(&int).unwrap()
);
assert_eq!(digest(&float).unwrap(), digest(&int).unwrap());
}
#[test]
fn an_image_in_either_spelling_is_the_same_item() {
let url = vec![Image::DataUrl("data:image/png;base64,AAECAw".into())];
let inline = vec![Image::Inline(InlineImage::new("image/png", "AAECAw=="))];
let a = json!({"state": "s", "images": url});
let b = json!({"state": "s", "images": inline});
assert_ne!(digest(&a).unwrap(), digest(&b).unwrap());
assert_eq!(
item_digest(&a, Some(&url)).unwrap(),
item_digest(&b, Some(&inline)).unwrap()
);
let other = vec![Image::Inline(InlineImage::new("image/jpeg", "AAECAw=="))];
let c = json!({"state": "s", "images": other});
assert_ne!(
item_digest(&a, Some(&url)).unwrap(),
item_digest(&c, Some(&other)).unwrap()
);
}
#[test]
fn the_first_differing_part_is_named_in_order() {
let items = |list: &[&str]| list.iter().map(|s| s.to_string()).collect::<Vec<_>>();
let parts = [
("model", "m", "m"),
("fields", "f", "g"),
("options", "o", "p"),
];
assert_eq!(
first_difference(&parts, &items(&["a"]), &items(&["b"])),
Some("fields".into())
);
let same = [("model", "m", "m")];
assert_eq!(
first_difference(&same, &items(&["a", "b"]), &items(&["a", "c"])),
Some("items[1]".into())
);
assert_eq!(
first_difference(&same, &items(&["a"]), &items(&["a", "b"])),
Some("items".into())
);
assert_eq!(
first_difference(&same, &items(&["a"]), &items(&["a"])),
None
);
}
}