use crate::json_path::JsonPath;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Change {
Added {
path: JsonPath,
value: Value,
},
Removed {
path: JsonPath,
value: Value,
},
Modified {
path: JsonPath,
old_value: Value,
new_value: Value,
},
}
impl Change {
pub fn path(&self) -> &JsonPath {
match self {
Change::Added { path, .. } => path,
Change::Removed { path, .. } => path,
Change::Modified { path, .. } => path,
}
}
}
impl Serialize for Change {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeMap;
match self {
Change::Added { path, value } => {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("path", &path.to_string())?;
map.serialize_entry("value", value)?;
map.end()
}
Change::Removed { path, value } => {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("path", &path.to_string())?;
map.serialize_entry("value", value)?;
map.end()
}
Change::Modified {
path,
old_value,
new_value,
} => {
let mut map = serializer.serialize_map(Some(3))?;
map.serialize_entry("path", &path.to_string())?;
map.serialize_entry("oldValue", old_value)?;
map.serialize_entry("newValue", new_value)?;
map.end()
}
}
}
}
impl<'de> Deserialize<'de> for Change {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{MapAccess, Visitor};
use std::fmt::Formatter;
struct ChangeVisitor;
impl<'de> Visitor<'de> for ChangeVisitor {
type Value = Change;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("a change object with path, value, and/or oldValue/newValue")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut path = None;
let mut value = None;
let mut old_value = None;
let mut new_value = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"path" => {
let path_str: String = map.next_value()?;
path = Some(path_str.parse::<JsonPath>().map_err(|_| {
serde::de::Error::custom(format!("invalid path: {}", path_str))
})?);
}
"value" => {
value = Some(map.next_value()?);
}
"oldValue" => {
old_value = Some(map.next_value()?);
}
"newValue" => {
new_value = Some(map.next_value()?);
}
_ => {
let _ = map.next_value::<serde::de::IgnoredAny>();
}
}
}
let path = path.ok_or_else(|| serde::de::Error::missing_field("path"))?;
match (old_value, new_value) {
(None, None) => {
let value =
value.ok_or_else(|| serde::de::Error::missing_field("value"))?;
Ok(Change::Added { path, value })
}
(Some(old), Some(new)) => Ok(Change::Modified {
path,
old_value: old,
new_value: new,
}),
(Some(old), None) => Ok(Change::Removed { path, value: old }),
(None, Some(_)) => Err(serde::de::Error::custom(
"newValue without oldValue is not allowed",
)),
}
}
}
deserializer.deserialize_map(ChangeVisitor)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Changes {
pub added: Vec<Change>,
pub removed: Vec<Change>,
pub modified: Vec<Change>,
#[serde(skip)]
pub after: Option<Value>,
}
impl Changes {
pub fn new() -> Self {
Self {
added: Vec::new(),
removed: Vec::new(),
modified: Vec::new(),
after: None,
}
}
pub fn push(&mut self, change: Change) {
match change {
Change::Added { .. } => self.added.push(change),
Change::Removed { .. } => self.removed.push(change),
Change::Modified { .. } => self.modified.push(change),
}
}
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.removed.is_empty() && self.modified.is_empty()
}
pub fn filter_ignore_patterns(&self, patterns: &[String]) -> Self {
let matcher = PatternMatcher::new(patterns);
Self {
added: self
.added
.iter()
.filter(|c| !should_ignore_change(c, &matcher))
.cloned()
.collect(),
removed: self
.removed
.iter()
.filter(|c| !should_ignore_change(c, &matcher))
.cloned()
.collect(),
modified: self
.modified
.iter()
.filter(|c| !should_ignore_change(c, &matcher))
.cloned()
.collect(),
after: self.after.clone(),
}
}
pub fn iter_filtered_changes<'a>(
&'a self,
patterns: &[String],
) -> impl Iterator<Item = &'a Change> + 'a {
let matcher = PatternMatcher::new(patterns);
let matcher_added = matcher.clone();
let matcher_removed = matcher.clone();
let matcher_modified = matcher;
self.added
.iter()
.filter(move |c| !should_ignore_change(c, &matcher_added))
.chain(
self.removed
.iter()
.filter(move |c| !should_ignore_change(c, &matcher_removed)),
)
.chain(
self.modified
.iter()
.filter(move |c| !should_ignore_change(c, &matcher_modified)),
)
}
}
#[derive(Clone)]
struct PatternMatcher {
prefixes: HashSet<String>,
}
impl PatternMatcher {
fn new(patterns: &[String]) -> Self {
let mut prefixes = HashSet::new();
for pattern_str in patterns {
let dot_notation = if pattern_str.starts_with('/') {
json_pointer_to_dot_notation(pattern_str)
} else {
pattern_str.clone()
};
prefixes.insert(dot_notation);
}
Self { prefixes }
}
fn should_ignore(&self, path: &JsonPath) -> bool {
for i in 1..=path.len() {
if let Some(prefix) = path.prefix(i) {
let prefix_str = prefix.to_string();
if self.prefixes.contains(&prefix_str) {
return true;
}
}
}
false
}
}
fn json_pointer_to_dot_notation(ptr: &str) -> String {
let mut result = String::new();
let parts: Vec<&str> = ptr.split('/').filter(|s| !s.is_empty()).collect();
for (i, part) in parts.iter().enumerate() {
if i > 0 {
result.push('.');
}
if part.chars().next().is_some_and(|c| c.is_ascii_digit()) {
result.push('[');
result.push_str(part);
result.push(']');
} else {
result.push_str(part);
}
}
result
}
fn should_ignore_change(change: &Change, matcher: &PatternMatcher) -> bool {
let path = match change {
Change::Added { path, .. } => path,
Change::Removed { path, .. } => path,
Change::Modified { path, .. } => path,
};
matcher.should_ignore(path)
}
impl Default for Changes {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_pattern_matching_with_json_pointer() {
let patterns = vec!["/user/id".to_string(), "/tags".to_string()];
let matcher = PatternMatcher::new(&patterns);
let user_id_path: JsonPath = "user.id".parse().unwrap();
assert!(matcher.should_ignore(&user_id_path));
let tags_path: JsonPath = "tags".parse().unwrap();
assert!(matcher.should_ignore(&tags_path));
let user_name_path: JsonPath = "user.name".parse().unwrap();
assert!(!matcher.should_ignore(&user_name_path));
}
#[test]
fn test_filter_ignore_patterns_with_json_path() {
let mut changes = Changes::new();
changes.push(Change::Modified {
path: "user.id".parse().unwrap(),
old_value: json!(1),
new_value: json!(2),
});
changes.push(Change::Modified {
path: "user.name".parse().unwrap(),
old_value: json!("John"),
new_value: json!("Jane"),
});
let patterns = vec!["/user/id".to_string()];
let filtered = changes.filter_ignore_patterns(&patterns);
assert_eq!(filtered.modified.len(), 1);
if let Change::Modified { path, .. } = &filtered.modified[0] {
assert_eq!(path.to_string(), "user.name");
} else {
panic!("Expected Modified change");
}
}
#[test]
fn test_iter_filtered_changes_basic() {
let mut changes = Changes::new();
changes.push(Change::Added {
path: "user.email".parse().unwrap(),
value: json!("test@example.com"),
});
changes.push(Change::Modified {
path: "user.name".parse().unwrap(),
old_value: json!("John"),
new_value: json!("Jane"),
});
changes.push(Change::Removed {
path: "user.age".parse().unwrap(),
value: json!(30),
});
let patterns = vec!["/user/name".to_string()];
let filtered: Vec<&Change> = changes.iter_filtered_changes(&patterns).collect();
assert_eq!(filtered.len(), 2);
assert!(filtered.iter().any(|c| matches!(c, Change::Added { .. })));
assert!(filtered.iter().any(|c| matches!(c, Change::Removed { .. })));
assert!(!filtered
.iter()
.any(|c| matches!(c, Change::Modified { .. })));
}
#[test]
fn test_iter_filtered_changes_matches_filter_ignore_patterns() {
let mut changes = Changes::new();
changes.push(Change::Added {
path: "user.email".parse().unwrap(),
value: json!("test@example.com"),
});
changes.push(Change::Modified {
path: "user.name".parse().unwrap(),
old_value: json!("John"),
new_value: json!("Jane"),
});
changes.push(Change::Removed {
path: "user.age".parse().unwrap(),
value: json!(30),
});
let patterns = vec!["/user/name".to_string()];
let filtered_old = changes.filter_ignore_patterns(&patterns);
let filtered_new: Vec<&Change> = changes.iter_filtered_changes(&patterns).collect();
let old_added = filtered_old.added.len();
let old_removed = filtered_old.removed.len();
let old_modified = filtered_old.modified.len();
let new_added = filtered_new
.iter()
.filter(|c| matches!(c, Change::Added { .. }))
.count();
let new_removed = filtered_new
.iter()
.filter(|c| matches!(c, Change::Removed { .. }))
.count();
let new_modified = filtered_new
.iter()
.filter(|c| matches!(c, Change::Modified { .. }))
.count();
assert_eq!(old_added, new_added);
assert_eq!(old_removed, new_removed);
assert_eq!(old_modified, new_modified);
}
#[test]
fn test_iter_filtered_changes_empty_patterns() {
let mut changes = Changes::new();
changes.push(Change::Added {
path: "user.email".parse().unwrap(),
value: json!("test@example.com"),
});
let patterns: Vec<String> = vec![];
let filtered: Vec<&Change> = changes.iter_filtered_changes(&patterns).collect();
assert_eq!(filtered.len(), 1);
}
#[test]
fn test_iter_filtered_changes_lazy_evaluation() {
let mut changes = Changes::new();
for i in 0..100 {
changes.push(Change::Modified {
path: format!("item{}", i).parse().unwrap(),
old_value: json!(i),
new_value: json!(i + 1),
});
}
let patterns: Vec<String> = (0..90).map(|i| format!("/item{}", i)).collect();
let filtered: Vec<_> = changes.iter_filtered_changes(&patterns).take(5).collect();
assert_eq!(filtered.len(), 5);
}
#[test]
fn test_iter_filtered_changes_order_preserved() {
let mut changes = Changes::new();
changes.push(Change::Added {
path: "first".parse().unwrap(),
value: json!(1),
});
changes.push(Change::Removed {
path: "second".parse().unwrap(),
value: json!(2),
});
changes.push(Change::Modified {
path: "third".parse().unwrap(),
old_value: json!(3),
new_value: json!(4),
});
let patterns: Vec<String> = vec![];
let filtered: Vec<&Change> = changes.iter_filtered_changes(&patterns).collect();
assert!(matches!(filtered[0], Change::Added { .. }));
assert!(matches!(filtered[1], Change::Removed { .. }));
assert!(matches!(filtered[2], Change::Modified { .. }));
}
}