cortexkit_model_batch/
identity.rs1use serde::Serialize;
13use serde_json::Value;
14use sha2::{Digest, Sha256};
15
16use crate::image::Image;
17
18#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct IdentityError(pub String);
22
23impl std::fmt::Display for IdentityError {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 write!(f, "request cannot be canonicalized: {}", self.0)
26 }
27}
28
29impl std::error::Error for IdentityError {}
30
31pub fn canonical_json<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>, IdentityError> {
33 serde_jcs::to_vec(value).map_err(|e| IdentityError(e.to_string()))
34}
35
36pub fn digest<T: Serialize + ?Sized>(value: &T) -> Result<String, IdentityError> {
38 let digest = Sha256::digest(canonical_json(value)?);
39 Ok(digest.iter().map(|byte| format!("{byte:02x}")).collect())
40}
41
42pub fn item_digest<T: Serialize + ?Sized>(
46 item: &T,
47 images: Option<&[Image]>,
48) -> Result<String, IdentityError> {
49 let mut value = serde_json::to_value(item).map_err(|e| IdentityError(e.to_string()))?;
50 if let (Some(images), Value::Object(members)) = (images, &mut value) {
51 let canonical = images
52 .iter()
53 .enumerate()
54 .map(|(j, image)| {
55 image
56 .canonical()
57 .map_err(|why| IdentityError(format!("images[{j}]: {why}")))
58 })
59 .collect::<Result<Vec<_>, _>>()?;
60 members.insert(
61 "images".into(),
62 serde_json::to_value(canonical).map_err(|e| IdentityError(e.to_string()))?,
63 );
64 }
65 digest(&value)
66}
67
68pub fn first_difference(
74 parts: &[(&str, &str, &str)],
75 recorded_items: &[String],
76 sent_items: &[String],
77) -> Option<String> {
78 if let Some((name, _, _)) = parts.iter().find(|(_, recorded, sent)| recorded != sent) {
79 return Some((*name).to_owned());
80 }
81 if recorded_items.len() != sent_items.len() {
82 return Some("items".into());
83 }
84 recorded_items
85 .iter()
86 .zip(sent_items)
87 .position(|(recorded, sent)| recorded != sent)
88 .map(|i| format!("items[{i}]"))
89}
90
91#[cfg(test)]
92mod tests {
93 use serde_json::json;
94
95 use super::*;
96 use crate::image::InlineImage;
97
98 #[test]
99 fn number_spelling_and_key_order_do_not_change_a_digest() {
100 let float: Value = serde_json::from_str(r#"{"a":1.0,"b":{"y":[2.50],"x":0}}"#).unwrap();
101 let int: Value = serde_json::from_str(r#"{"b":{"x":0,"y":[2.5]},"a":1}"#).unwrap();
102 assert_ne!(
103 serde_json::to_string(&float).unwrap(),
104 serde_json::to_string(&int).unwrap()
105 );
106 assert_eq!(digest(&float).unwrap(), digest(&int).unwrap());
107 }
108
109 #[test]
110 fn an_image_in_either_spelling_is_the_same_item() {
111 let url = vec![Image::DataUrl("data:image/png;base64,AAECAw".into())];
112 let inline = vec![Image::Inline(InlineImage::new("image/png", "AAECAw=="))];
113 let a = json!({"state": "s", "images": url});
114 let b = json!({"state": "s", "images": inline});
115 assert_ne!(digest(&a).unwrap(), digest(&b).unwrap());
116 assert_eq!(
117 item_digest(&a, Some(&url)).unwrap(),
118 item_digest(&b, Some(&inline)).unwrap()
119 );
120 let other = vec![Image::Inline(InlineImage::new("image/jpeg", "AAECAw=="))];
121 let c = json!({"state": "s", "images": other});
122 assert_ne!(
123 item_digest(&a, Some(&url)).unwrap(),
124 item_digest(&c, Some(&other)).unwrap()
125 );
126 }
127
128 #[test]
129 fn the_first_differing_part_is_named_in_order() {
130 let items = |list: &[&str]| list.iter().map(|s| s.to_string()).collect::<Vec<_>>();
131 let parts = [
132 ("model", "m", "m"),
133 ("fields", "f", "g"),
134 ("options", "o", "p"),
135 ];
136 assert_eq!(
137 first_difference(&parts, &items(&["a"]), &items(&["b"])),
138 Some("fields".into())
139 );
140 let same = [("model", "m", "m")];
141 assert_eq!(
142 first_difference(&same, &items(&["a", "b"]), &items(&["a", "c"])),
143 Some("items[1]".into())
144 );
145 assert_eq!(
146 first_difference(&same, &items(&["a"]), &items(&["a", "b"])),
147 Some("items".into())
148 );
149 assert_eq!(
150 first_difference(&same, &items(&["a"]), &items(&["a"])),
151 None
152 );
153 }
154}