use serde_json::Value;
pub trait MergeIdentity: Send + Sync {
fn label(&self) -> &str;
fn identity_of(&self, element: &Value) -> Option<String>;
}
#[derive(Debug, Clone)]
pub struct IdentityKey(String);
impl IdentityKey {
pub fn new(field: impl Into<String>) -> Self {
Self(field.into())
}
}
impl MergeIdentity for IdentityKey {
fn label(&self) -> &str {
&self.0
}
fn identity_of(&self, element: &Value) -> Option<String> {
scalar_token(element.get(&self.0)?)
}
}
#[derive(Debug, Clone)]
pub struct CompositeKey {
fields: Vec<Vec<String>>,
label: String,
}
impl CompositeKey {
pub fn new<I, S>(fields: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let raw: Vec<String> = fields.into_iter().map(Into::into).collect();
let label = raw.join("+");
let fields = raw
.iter()
.map(|field| field.split('.').map(ToOwned::to_owned).collect())
.collect();
Self { fields, label }
}
}
impl MergeIdentity for CompositeKey {
fn label(&self) -> &str {
&self.label
}
fn identity_of(&self, element: &Value) -> Option<String> {
if self.fields.is_empty() {
return None;
}
let mut token = String::new();
for path in &self.fields {
let part = scalar_token(lookup(element, path)?)?;
token.push_str(part.len().to_string().as_str());
token.push(':');
token.push_str(&part);
}
Some(token)
}
}
fn lookup<'a>(value: &'a Value, path: &[String]) -> Option<&'a Value> {
let mut current = value;
for segment in path {
current = current.get(segment)?;
}
Some(current)
}
fn scalar_token(value: &Value) -> Option<String> {
match value {
Value::String(text) => Some(text.clone()),
Value::Number(number) => Some(number.to_string()),
Value::Bool(flag) => Some(flag.to_string()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn identity_key_reads_named_field() {
let key = IdentityKey::new("name");
assert_eq!(
key.identity_of(&json!({ "name": "a" })),
Some("a".to_string())
);
assert_eq!(key.identity_of(&json!({ "other": "a" })), None);
assert_eq!(key.label(), "name");
}
#[test]
fn composite_key_joins_nested_fields() {
let key = CompositeKey::new(["from.ecosystem", "from.module", "to.ecosystem", "to.module"]);
let element = json!({
"from": { "ecosystem": "go", "module": "api" },
"to": { "ecosystem": "rust", "module": "shared" },
});
let token = key.identity_of(&element).expect("complete identity");
assert!(token.contains("go"));
assert!(token.contains("rust"));
assert_eq!(
key.label(),
"from.ecosystem+from.module+to.ecosystem+to.module"
);
}
#[test]
fn composite_key_is_none_when_a_field_is_missing() {
let key = CompositeKey::new(["from.ecosystem", "to.ecosystem"]);
let element = json!({ "from": { "ecosystem": "go" } });
assert_eq!(key.identity_of(&element), None);
}
#[test]
fn composite_key_with_no_fields_has_no_identity() {
let key = CompositeKey::new(Vec::<String>::new());
assert_eq!(key.identity_of(&json!({ "name": "a" })), None);
assert_eq!(key.identity_of(&json!({ "name": "b" })), None);
}
#[test]
fn composite_key_separates_distinct_tuples() {
let key = CompositeKey::new(["a", "b"]);
let first = key.identity_of(&json!({ "a": "x", "b": "yz" }));
let second = key.identity_of(&json!({ "a": "xy", "b": "z" }));
assert!(first.is_some());
assert_ne!(first, second);
}
#[test]
fn composite_key_is_injective_for_separator_like_values() {
let key = CompositeKey::new(["a", "b"]);
let with_sep = key.identity_of(&json!({ "a": "x\u{1f}y", "b": "z" }));
let split = key.identity_of(&json!({ "a": "x", "b": "y\u{1f}z" }));
assert!(with_sep.is_some());
assert_ne!(with_sep, split);
let looks_encoded = key.identity_of(&json!({ "a": "1:x", "b": "y" }));
let genuinely_split = key.identity_of(&json!({ "a": "1", "b": "x1:y" }));
assert_ne!(looks_encoded, genuinely_split);
}
}