rskit_config/strict/merge/
identity.rs1use serde_json::Value;
10
11pub trait MergeIdentity: Send + Sync {
17 fn label(&self) -> &str;
19
20 fn identity_of(&self, element: &Value) -> Option<String>;
23}
24
25#[derive(Debug, Clone)]
27pub struct IdentityKey(String);
28
29impl IdentityKey {
30 pub fn new(field: impl Into<String>) -> Self {
32 Self(field.into())
33 }
34}
35
36impl MergeIdentity for IdentityKey {
37 fn label(&self) -> &str {
38 &self.0
39 }
40
41 fn identity_of(&self, element: &Value) -> Option<String> {
42 scalar_token(element.get(&self.0)?)
43 }
44}
45
46#[derive(Debug, Clone)]
55pub struct CompositeKey {
56 fields: Vec<Vec<String>>,
57 label: String,
58}
59
60impl CompositeKey {
61 pub fn new<I, S>(fields: I) -> Self
65 where
66 I: IntoIterator<Item = S>,
67 S: Into<String>,
68 {
69 let raw: Vec<String> = fields.into_iter().map(Into::into).collect();
70 let label = raw.join("+");
71 let fields = raw
72 .iter()
73 .map(|field| field.split('.').map(ToOwned::to_owned).collect())
74 .collect();
75 Self { fields, label }
76 }
77}
78
79impl MergeIdentity for CompositeKey {
80 fn label(&self) -> &str {
81 &self.label
82 }
83
84 fn identity_of(&self, element: &Value) -> Option<String> {
85 if self.fields.is_empty() {
90 return None;
91 }
92 let mut token = String::new();
93 for path in &self.fields {
94 let part = scalar_token(lookup(element, path)?)?;
95 token.push_str(part.len().to_string().as_str());
101 token.push(':');
102 token.push_str(&part);
103 }
104 Some(token)
105 }
106}
107
108fn lookup<'a>(value: &'a Value, path: &[String]) -> Option<&'a Value> {
110 let mut current = value;
111 for segment in path {
112 current = current.get(segment)?;
113 }
114 Some(current)
115}
116
117fn scalar_token(value: &Value) -> Option<String> {
119 match value {
120 Value::String(text) => Some(text.clone()),
121 Value::Number(number) => Some(number.to_string()),
122 Value::Bool(flag) => Some(flag.to_string()),
123 _ => None,
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use serde_json::json;
131
132 #[test]
133 fn identity_key_reads_named_field() {
134 let key = IdentityKey::new("name");
135 assert_eq!(
136 key.identity_of(&json!({ "name": "a" })),
137 Some("a".to_string())
138 );
139 assert_eq!(key.identity_of(&json!({ "other": "a" })), None);
140 assert_eq!(key.label(), "name");
141 }
142
143 #[test]
144 fn composite_key_joins_nested_fields() {
145 let key = CompositeKey::new(["from.ecosystem", "from.module", "to.ecosystem", "to.module"]);
146 let element = json!({
147 "from": { "ecosystem": "go", "module": "api" },
148 "to": { "ecosystem": "rust", "module": "shared" },
149 });
150
151 let token = key.identity_of(&element).expect("complete identity");
152 assert!(token.contains("go"));
153 assert!(token.contains("rust"));
154 assert_eq!(
155 key.label(),
156 "from.ecosystem+from.module+to.ecosystem+to.module"
157 );
158 }
159
160 #[test]
161 fn composite_key_is_none_when_a_field_is_missing() {
162 let key = CompositeKey::new(["from.ecosystem", "to.ecosystem"]);
163 let element = json!({ "from": { "ecosystem": "go" } });
164
165 assert_eq!(key.identity_of(&element), None);
166 }
167
168 #[test]
169 fn composite_key_with_no_fields_has_no_identity() {
170 let key = CompositeKey::new(Vec::<String>::new());
173
174 assert_eq!(key.identity_of(&json!({ "name": "a" })), None);
175 assert_eq!(key.identity_of(&json!({ "name": "b" })), None);
176 }
177
178 #[test]
179 fn composite_key_separates_distinct_tuples() {
180 let key = CompositeKey::new(["a", "b"]);
181 let first = key.identity_of(&json!({ "a": "x", "b": "yz" }));
182 let second = key.identity_of(&json!({ "a": "xy", "b": "z" }));
183
184 assert!(first.is_some());
185 assert_ne!(first, second);
186 }
187
188 #[test]
189 fn composite_key_is_injective_for_separator_like_values() {
190 let key = CompositeKey::new(["a", "b"]);
191
192 let with_sep = key.identity_of(&json!({ "a": "x\u{1f}y", "b": "z" }));
196 let split = key.identity_of(&json!({ "a": "x", "b": "y\u{1f}z" }));
197 assert!(with_sep.is_some());
198 assert_ne!(with_sep, split);
199
200 let looks_encoded = key.identity_of(&json!({ "a": "1:x", "b": "y" }));
203 let genuinely_split = key.identity_of(&json!({ "a": "1", "b": "x1:y" }));
204 assert_ne!(looks_encoded, genuinely_split);
205 }
206}