use std::borrow::Cow;
use std::collections::BTreeMap;
use std::mem::take;
use crate::{Adapter, Mutation, MutationError, MutationKind, Mutations, Path, PathSegment};
#[derive(Default)]
enum BatchMutationKind<A: Adapter> {
#[default]
None,
#[cfg(feature = "delete")]
Delete,
Replace(A::Value),
#[cfg(any(feature = "append", feature = "truncate"))]
TruncateAppend {
truncate_len: usize,
append_len: usize,
append_value: Option<A::Value>,
},
}
enum BatchChildren<A: Adapter> {
String(BTreeMap<Cow<'static, str>, BatchTree<A>>),
Positive(BTreeMap<usize, BatchTree<A>>),
Negative(BTreeMap<usize, BatchTree<A>>),
}
type MappedIntoIter<A, K> = std::iter::Map<
std::collections::btree_map::IntoIter<K, BatchTree<A>>,
fn((K, BatchTree<A>)) -> (PathSegment, BatchTree<A>),
>;
enum BatchChildrenIntoIter<A: Adapter> {
String(MappedIntoIter<A, Cow<'static, str>>),
Positive(MappedIntoIter<A, usize>),
Negative(MappedIntoIter<A, usize>),
}
impl<A: Adapter> Iterator for BatchChildrenIntoIter<A> {
type Item = (PathSegment, BatchTree<A>);
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::String(iter) => iter.next(),
Self::Positive(iter) => iter.next(),
Self::Negative(iter) => iter.next(),
}
}
}
impl<A: Adapter> IntoIterator for BatchChildren<A> {
type Item = (PathSegment, BatchTree<A>);
type IntoIter = BatchChildrenIntoIter<A>;
fn into_iter(self) -> Self::IntoIter {
match self {
Self::String(map) => {
BatchChildrenIntoIter::String(map.into_iter().map(|(k, v)| (PathSegment::String(k), v)))
}
Self::Positive(map) => {
BatchChildrenIntoIter::Positive(map.into_iter().map(|(k, v)| (PathSegment::Positive(k), v)))
}
Self::Negative(map) => {
BatchChildrenIntoIter::Negative(map.into_iter().map(|(k, v)| (PathSegment::Negative(k), v)))
}
}
}
}
pub struct BatchTree<A: Adapter> {
kind: BatchMutationKind<A>,
children: Option<BatchChildren<A>>,
}
impl<A: Adapter> Default for BatchTree<A> {
fn default() -> Self {
Self {
kind: Default::default(),
children: Default::default(),
}
}
}
impl<A: Adapter> BatchTree<A> {
pub fn new() -> Self {
Default::default()
}
pub fn load(&mut self, mutation: Mutation<A::Value>) -> Result<(), MutationError> {
self.load_with_stack(mutation, &mut Default::default())
}
fn load_with_stack(
&mut self,
mut mutation: Mutation<A::Value>,
path_stack: &mut Path<false>,
) -> Result<(), MutationError> {
if let BatchMutationKind::Replace(value) = &mut self.kind {
A::mutate(value, mutation, path_stack)?;
return Ok(());
}
let mut batch = self;
while let Some(mut segment) = mutation.path.pop() {
#[cfg(any(feature = "append", feature = "truncate"))]
if let PathSegment::Negative(index) = &mut segment
&& let BatchMutationKind::TruncateAppend {
truncate_len,
append_len,
append_value,
} = &mut batch.kind
{
if *index <= *append_len {
mutation.path.push(segment);
A::mutate(append_value.as_mut().unwrap(), mutation, path_stack)?;
return Ok(());
} else {
*index -= *append_len;
}
*index += *truncate_len;
}
path_stack.push(segment.clone());
let children: &mut BatchChildren<A> = batch.children.get_or_insert_with(|| match &segment {
PathSegment::String(_) => BatchChildren::String(BTreeMap::new()),
PathSegment::Positive(_) => BatchChildren::Positive(BTreeMap::new()),
PathSegment::Negative(_) => BatchChildren::Negative(BTreeMap::new()),
});
batch = match (segment, children) {
(PathSegment::String(key), BatchChildren::String(children)) => children.entry(key).or_default(),
(PathSegment::Positive(key), BatchChildren::Positive(children)) => children.entry(key).or_default(),
(PathSegment::Negative(key), BatchChildren::Negative(children)) => children.entry(key).or_default(),
_ => return Err(MutationError::IndexError { path: take(path_stack) }),
};
if let BatchMutationKind::Replace(value) = &mut batch.kind {
A::mutate(value, mutation, path_stack)?;
return Ok(());
}
}
match mutation.kind {
MutationKind::Replace(value) => {
batch.kind = BatchMutationKind::Replace(value);
batch.children.take();
}
#[cfg(feature = "delete")]
MutationKind::Delete => {
batch.kind = BatchMutationKind::Delete;
batch.children.take();
}
MutationKind::Batch(mutations) => {
let len = path_stack.len();
for mutation in mutations {
batch.load_with_stack(mutation, path_stack)?;
path_stack.truncate(len);
}
}
#[cfg(feature = "append")]
MutationKind::Append(value) => match &mut batch.kind {
BatchMutationKind::Replace(_) => unreachable!(),
#[cfg(feature = "delete")]
BatchMutationKind::Delete => return Err(MutationError::OperationError { path: take(path_stack) }),
BatchMutationKind::None => {
let Some(append_len) = A::len(&value) else {
return Err(MutationError::OperationError { path: take(path_stack) });
};
if append_len == 0 {
return Ok(());
}
batch.kind = BatchMutationKind::TruncateAppend {
truncate_len: 0,
append_len,
append_value: Some(value),
};
}
BatchMutationKind::TruncateAppend {
truncate_len: _,
append_len,
append_value,
} => {
if let Some(append_value) = append_value {
let Some(len) = A::append(append_value, value) else {
return Err(MutationError::OperationError { path: take(path_stack) });
};
*append_len += len;
} else {
let Some(len) = A::len(&value) else {
return Err(MutationError::OperationError { path: take(path_stack) });
};
*append_len = len;
*append_value = Some(value);
}
}
},
#[cfg(feature = "truncate")]
MutationKind::Truncate(len) => match &mut batch.kind {
BatchMutationKind::Replace(_) => unreachable!(),
#[cfg(feature = "delete")]
BatchMutationKind::Delete => return Err(MutationError::OperationError { path: take(path_stack) }),
BatchMutationKind::None => {
if len == 0 {
return Ok(());
}
batch.kind = BatchMutationKind::TruncateAppend {
truncate_len: len,
append_len: 0,
append_value: None,
};
if let Some(children) = &mut batch.children {
let BatchChildren::Negative(children) = children else {
return Err(MutationError::IndexError { path: take(path_stack) });
};
children.retain(|index, _| *index > len);
}
}
BatchMutationKind::TruncateAppend {
truncate_len,
append_len,
append_value,
} => {
let remaining = if let Some(append_value) = append_value {
let Some(remaining) = A::truncate(append_value, len) else {
return Err(MutationError::OperationError { path: take(path_stack) });
};
*append_len -= len - remaining;
remaining
} else {
len
};
*truncate_len += remaining;
if *append_len == 0 && *truncate_len == 0 {
batch.kind = BatchMutationKind::None;
} else if remaining > 0
&& let Some(children) = &mut batch.children
{
let BatchChildren::Negative(children) = children else {
return Err(MutationError::IndexError { path: take(path_stack) });
};
children.retain(|index, _| *index > *truncate_len);
}
}
},
}
Ok(())
}
pub fn dump(&mut self) -> Mutations<A::Value> {
let mut mutations = Mutations::new();
if let Some(children) = take(&mut self.children) {
for (segment, mut batch) in children {
mutations.insert(segment, batch.dump());
}
}
match take(&mut self.kind) {
BatchMutationKind::None => {}
#[cfg(feature = "delete")]
BatchMutationKind::Delete => mutations.extend(MutationKind::Delete),
BatchMutationKind::Replace(value) => mutations.extend(MutationKind::Replace(value)),
#[cfg(any(feature = "append", feature = "truncate"))]
BatchMutationKind::TruncateAppend {
truncate_len,
append_len,
append_value,
} => {
#[cfg(feature = "truncate")]
if truncate_len > 0 {
mutations.extend(MutationKind::Truncate(truncate_len));
}
#[cfg(feature = "append")]
if append_len > 0
&& let Some(value) = append_value
{
mutations.extend(MutationKind::Append(value));
}
}
}
mutations
}
}
#[cfg(test)]
mod test {
use morphix_test_utils::*;
use serde_json::json;
use super::*;
use crate::adapter::Json;
#[test]
fn empty_batch() {
let mut batch = BatchTree::<Json>::new();
assert_eq!(batch.dump().into_inner(), None);
}
#[test]
fn replace() {
let mut batch = BatchTree::<Json>::new();
batch.load(replace!(foo.bar, json!(1))).unwrap();
assert_eq!(batch.dump().into_inner(), Some(replace!(foo.bar, json!(1))));
}
#[test]
fn replace_after_replace() {
let mut batch = BatchTree::<Json>::new();
batch.load(replace!(foo.bar, json!(1))).unwrap();
batch.load(replace!(foo.bar, json!(2))).unwrap();
assert_eq!(batch.dump().into_inner(), Some(replace!(foo.bar, json!(2))));
}
#[test]
fn append_after_replace() {
let mut batch = BatchTree::<Json>::new();
batch.load(replace!(foo.bar, json!({"qux": "1"}))).unwrap();
batch.load(append!(foo.bar.qux, json!("2"))).unwrap();
assert_eq!(batch.dump().into_inner(), Some(replace!(foo.bar, json!({"qux": "12"}))),);
}
#[test]
fn replace_after_append() {
let mut batch = BatchTree::<Json>::new();
batch.load(append!(foo.bar.qux, json!("2"))).unwrap();
batch.load(replace!(foo.bar, json!({"qux": "1"}))).unwrap();
assert_eq!(batch.dump().into_inner(), Some(replace!(foo.bar, json!({"qux": "1"}))),);
}
#[test]
fn merge_append() {
let mut batch = BatchTree::<Json>::new();
batch
.load(batch!(foo, append!(bar, json!("1")), append!(bar, json!("2"))))
.unwrap();
assert_eq!(batch.dump().into_inner(), Some(append!(foo.bar, json!("12"))),);
}
#[test]
fn basic_batch() {
let mut batch = BatchTree::<Json>::new();
batch.load(append!(bar, json!("2"))).unwrap();
batch.load(append!(qux, json!("1"))).unwrap();
assert_eq!(
batch.dump().into_inner(),
Some(batch!(_, append!(bar, json!("2")), append!(qux, json!("1")))),
);
}
#[test]
fn nested_batch() {
let mut batch = BatchTree::<Json>::new();
batch.load(append!(foo.bar, json!("2"))).unwrap();
batch.load(append!(foo.qux, json!("1"))).unwrap();
assert_eq!(
batch.dump().into_inner(),
Some(batch!(foo, append!(bar, json!("2")), append!(qux, json!("1")))),
);
}
#[test]
fn append_with_neg_index_1() {
let mut batch = BatchTree::<Json>::new();
batch.load(append!(-1, json!("c"))).unwrap();
batch.load(append!(_, json!(["a", "b"]))).unwrap();
batch.load(append!(-1, json!("d"))).unwrap();
batch.load(append!(-3, json!("e"))).unwrap();
assert_eq!(
batch.dump().into_inner(),
Some(batch!(_, append!(-1, json!("ce")), append!(_, json!(["a", "bd"])),)),
);
}
#[test]
fn append_with_neg_index_2() {
let mut batch = BatchTree::<Json>::new();
batch.load(append!(-1, json!("c"))).unwrap();
batch.load(append!(_, json!(["a", "b"]))).unwrap();
batch.load(append!(-2, json!("d"))).unwrap();
batch.load(append!(_, json!(["e", "f"]))).unwrap();
batch.load(append!(-3, json!("g"))).unwrap();
assert_eq!(
batch.dump().into_inner(),
Some(batch!(
_,
append!(-1, json!("c")),
append!(_, json!(["ad", "bg", "e", "f"])),
)),
);
}
#[test]
fn merge_truncate() {
let mut batch = BatchTree::<Json>::new();
batch.load(batch!(foo, truncate!(bar, 1), truncate!(bar, 2))).unwrap();
assert_eq!(batch.dump().into_inner(), Some(truncate!(foo.bar, 3)),);
}
#[test]
fn truncate_after_append_1() {
let mut batch = BatchTree::<Json>::new();
batch.load(append!(foo.bar.qux, json!("42"))).unwrap();
batch.load(truncate!(foo.bar.qux, 1)).unwrap();
assert_eq!(batch.dump().into_inner(), Some(append!(foo.bar.qux, json!("4"))),);
}
#[test]
fn truncate_after_append_2() {
let mut batch = BatchTree::<Json>::new();
batch.load(append!(foo.bar.qux, json!("42"))).unwrap();
batch.load(truncate!(foo.bar.qux, 2)).unwrap();
assert_eq!(batch.dump().into_inner(), None);
}
#[test]
fn truncate_after_append_3() {
let mut batch = BatchTree::<Json>::new();
batch.load(append!(foo.bar.qux, json!("42"))).unwrap();
batch.load(truncate!(foo.bar.qux, 3)).unwrap();
assert_eq!(batch.dump().into_inner(), Some(truncate!(foo.bar.qux, 1)),);
}
#[test]
fn append_after_truncate_1() {
let mut batch = BatchTree::<Json>::new();
batch.load(truncate!(foo.bar.qux, 3)).unwrap();
batch.load(append!(foo.bar.qux, json!("Hello, World!"))).unwrap();
batch.load(truncate!(foo.bar.qux, 1)).unwrap();
assert_eq!(
batch.dump().into_inner(),
Some(batch!(foo.bar.qux, truncate!(_, 3), append!(_, json!("Hello, World")),)),
);
}
#[test]
fn append_after_truncate_2() {
let mut batch = BatchTree::<Json>::new();
batch.load(truncate!(foo.bar.qux, 3)).unwrap();
batch.load(append!(foo.bar.qux, json!("42"))).unwrap();
batch.load(truncate!(foo.bar.qux, 3)).unwrap();
assert_eq!(batch.dump().into_inner(), Some(truncate!(foo.bar.qux, 4)),);
}
#[test]
fn truncate_with_neg_index_1() {
let mut batch = BatchTree::<Json>::new();
batch.load(truncate!(-1, 1)).unwrap();
batch.load(truncate!(_, 2)).unwrap();
batch.load(truncate!(-1, 3)).unwrap();
assert_eq!(
batch.dump().into_inner(),
Some(batch!(_, truncate!(-3, 3), truncate!(_, 2))),
);
}
#[test]
fn truncate_with_neg_index_2() {
let mut batch = BatchTree::<Json>::new();
batch.load(truncate!(_, 2)).unwrap();
batch.load(truncate!(-2, 3)).unwrap();
batch.load(truncate!(_, 1)).unwrap();
batch.load(append!(-1, json!("Hello, world!"))).unwrap();
batch.load(append!(_, json!(["114", "514"]))).unwrap();
batch.load(truncate!(-3, 8)).unwrap();
assert_eq!(
batch.dump().into_inner(),
Some(batch!(
_,
batch!(-4, truncate!(_, 3), append!(_, json!("Hello"))),
truncate!(_, 3),
append!(_, json!(["114", "514"])),
)),
);
}
#[test]
fn delete_after_delete() {
let mut batch = BatchTree::<Json>::new();
batch.load(delete!(foo)).unwrap();
batch.load(delete!(foo)).unwrap();
assert_eq!(batch.dump().into_inner(), Some(delete!(foo)));
}
#[test]
fn delete_after_truncate() {
let mut batch = BatchTree::<Json>::new();
batch.load(truncate!(foo.bar.qux, 3)).unwrap();
batch.load(delete!(foo.bar)).unwrap();
assert_eq!(batch.dump().into_inner(), Some(delete!(foo.bar)));
}
#[test]
fn replace_after_delete() {
let mut batch = BatchTree::<Json>::new();
batch.load(delete!(foo)).unwrap();
batch.load(replace!(foo, json!({}))).unwrap();
assert_eq!(batch.dump().into_inner(), Some(replace!(foo, json!({}))));
}
#[test]
fn append_after_delete() {
let mut batch = BatchTree::<Json>::new();
batch.load(delete!(foo)).unwrap();
batch.load(append!(foo, json!("test"))).unwrap_err();
}
}