use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MergeOp(pub(crate) Op);
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Op {
ArrayEntry {
path: Vec<String>,
value: Value,
},
ObjectMember {
path: Vec<String>,
key: String,
value: Value,
},
OwnedEntries {
path: Vec<String>,
field: String,
owned: EntryMatch,
entries: Value,
},
GroupEntries {
path: Vec<String>,
entries: String,
field: String,
owned: EntryMatch,
groups: Value,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EntryMatch {
Prefix(String),
Contains(String),
Any(Vec<EntryMatch>),
}
impl EntryMatch {
pub fn matches(&self, text: &str) -> bool {
match self {
EntryMatch::Prefix(p) => text.starts_with(p.as_str()),
EntryMatch::Contains(c) => text.contains(c.as_str()),
EntryMatch::Any(all) => all.iter().any(|m| m.matches(text)),
}
}
pub fn any_of<I: IntoIterator<Item = EntryMatch>>(matches: I) -> Self {
let mut all: Vec<EntryMatch> = Vec::new();
for m in matches {
if !all.contains(&m) {
all.push(m);
}
}
if all.len() == 1 {
all.pop().expect("one")
} else {
EntryMatch::Any(all)
}
}
}
fn segments<P: IntoIterator<Item = S>, S: Into<String>>(path: P) -> Vec<String> {
path.into_iter().map(Into::into).collect()
}
impl MergeOp {
pub fn array_entry<P: IntoIterator<Item = S>, S: Into<String>>(
path: P,
value: impl Into<Value>,
) -> Self {
MergeOp(Op::ArrayEntry {
path: segments(path),
value: value.into(),
})
}
pub fn object_member<P: IntoIterator<Item = S>, S: Into<String>>(
path: P,
key: impl Into<String>,
value: impl Into<Value>,
) -> Self {
MergeOp(Op::ObjectMember {
path: segments(path),
key: key.into(),
value: value.into(),
})
}
pub fn owned_entries<P: IntoIterator<Item = S>, S: Into<String>>(
path: P,
field: impl Into<String>,
owned: EntryMatch,
entries: Vec<Value>,
) -> Self {
MergeOp(Op::OwnedEntries {
path: segments(path),
field: field.into(),
owned,
entries: Value::Array(entries),
})
}
pub fn group_entries<P: IntoIterator<Item = S>, S: Into<String>>(
path: P,
entries: impl Into<String>,
field: impl Into<String>,
owned: EntryMatch,
groups: Vec<Value>,
) -> Self {
MergeOp(Op::GroupEntries {
path: segments(path),
entries: entries.into(),
field: field.into(),
owned,
groups: Value::Array(groups),
})
}
pub fn value(&self) -> &Value {
match &self.0 {
Op::ArrayEntry { value, .. } | Op::ObjectMember { value, .. } => value,
Op::OwnedEntries { entries, .. } => entries,
Op::GroupEntries { groups, .. } => groups,
}
}
pub(crate) fn path(&self) -> &[String] {
match &self.0 {
Op::ArrayEntry { path, .. }
| Op::ObjectMember { path, .. }
| Op::OwnedEntries { path, .. }
| Op::GroupEntries { path, .. } => path,
}
}
pub(crate) fn expects_nothing(&self) -> bool {
match &self.0 {
Op::OwnedEntries { entries: v, .. } | Op::GroupEntries { groups: v, .. } => {
v.as_array().is_some_and(Vec::is_empty)
}
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entry_matches() {
assert!(EntryMatch::Prefix("a".into()).matches("ab"));
assert!(!EntryMatch::Prefix("b".into()).matches("ab"));
assert!(EntryMatch::Contains("b".into()).matches("abc"));
let any = EntryMatch::any_of([
EntryMatch::Prefix("x".into()),
EntryMatch::Prefix("y".into()),
EntryMatch::Prefix("x".into()),
]);
assert_eq!(
any,
EntryMatch::Any(vec![
EntryMatch::Prefix("x".into()),
EntryMatch::Prefix("y".into())
])
);
assert!(any.matches("y1") && !any.matches("z"));
assert_eq!(
EntryMatch::any_of([EntryMatch::Prefix("x".into())]),
EntryMatch::Prefix("x".into())
);
}
}