use std::fmt;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CollisionError {
keys: Vec<String>,
}
impl CollisionError {
pub(crate) fn new<I, S>(keys: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let mut keys: Vec<String> = keys.into_iter().map(Into::into).collect();
keys.sort();
keys.dedup();
Self { keys }
}
#[must_use]
pub fn keys(&self) -> &[String] {
&self.keys
}
}
impl fmt::Display for CollisionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"vendor-extras key(s) collide with typed wire-format field(s): {}",
self.keys.join(", ")
)
}
}
impl std::error::Error for CollisionError {}
pub(crate) fn check(
extra: &serde_json::Map<String, serde_json::Value>,
reserved: &[&str],
) -> Result<(), CollisionError> {
let collisions: Vec<&str> = reserved
.iter()
.copied()
.filter(|name| extra.contains_key(*name))
.collect();
if collisions.is_empty() {
Ok(())
} else {
Err(CollisionError::new(collisions))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collision_error_display_lists_keys_alphabetically() {
let err = CollisionError::new(["uid", "id", "version"]);
assert_eq!(err.keys(), &["id", "uid", "version"]);
let msg = err.to_string();
assert!(msg.contains("id, uid, version"), "got: {msg}");
}
#[test]
fn collision_error_dedups_duplicate_input_keys() {
let err = CollisionError::new(["id", "id", "uid"]);
assert_eq!(err.keys(), &["id", "uid"]);
}
#[test]
fn check_returns_ok_on_clean_extras() {
let mut extra = serde_json::Map::new();
extra.insert("acmeCorpFoo".into(), serde_json::json!("bar"));
assert!(check(&extra, &["id", "uid"]).is_ok());
}
#[test]
fn check_returns_ok_on_empty_extras() {
let extra = serde_json::Map::new();
assert!(check(&extra, &["id", "uid"]).is_ok());
}
#[test]
fn check_returns_err_on_single_collision() {
let mut extra = serde_json::Map::new();
extra.insert("uid".into(), serde_json::json!("attacker"));
let err = check(&extra, &["id", "uid"]).expect_err("collision must error");
assert_eq!(err.keys(), &["uid"]);
}
#[test]
fn check_returns_err_on_multiple_collisions() {
let mut extra = serde_json::Map::new();
extra.insert("uid".into(), serde_json::json!("a"));
extra.insert("id".into(), serde_json::json!("b"));
extra.insert("acmeCorpFoo".into(), serde_json::json!("c"));
let err = check(&extra, &["id", "uid", "version"]).expect_err("collisions must error");
assert_eq!(err.keys(), &["id", "uid"]);
}
}