use crate::patch::{OwnedPatchOperation, move_index_out_of_bounds};
use crate::path::OwnedPath;
use crate::{
MetaMut, MoveItemError, Object, ObjectRef, ObjectRefMut, PatchOperation, PatchOperationKind,
PathSegment, TypeInfo, ValueKind,
};
#[allow(unused_imports)]
use crate::Meta;
use alloc::boxed::Box;
use alloc::vec::Vec;
use core::iter::FromIterator;
use core::{fmt, mem};
pub struct MutationRollback {
inverses: Vec<OwnedPatchOperation>,
}
impl fmt::Debug for MutationRollback {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MutationRollback")
.field("inverse_count", &self.inverses.len())
.finish()
}
}
impl MutationRollback {
pub fn previous_value(&self, index: usize) -> Option<ObjectRef<'_>> {
match self.inverses.get(index)? {
OwnedPatchOperation::Set { value, .. }
| OwnedPatchOperation::InsertKey { value, .. }
| OwnedPatchOperation::InsertItem { value, .. }
| OwnedPatchOperation::PushItem { value, .. } => Some(ObjectRef::new(value.as_ref())),
OwnedPatchOperation::RemoveKey { .. }
| OwnedPatchOperation::RemoveItem { .. }
| OwnedPatchOperation::MoveItem { .. } => None,
}
}
pub fn undo(mut self, target: &mut dyn MetaMut) -> Result<(), RollbackError> {
while let Some(inverse) = self.inverses.pop() {
let inverse_index = self.inverses.len();
apply_staged_operation(target, inverse).map_err(|error| RollbackError {
inverse_index,
error,
})?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct RollbackError {
pub inverse_index: usize,
pub error: CommitOperationError,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommitOperationError {
PathNotFound {
path: OwnedPath,
operation: PatchOperationKind,
},
ShapeMismatch {
path: OwnedPath,
expected: ValueKind,
actual: ValueKind,
},
TypeMismatch {
path: OwnedPath,
expected: TypeInfo,
actual: TypeInfo,
},
IndexOutOfBounds {
path: OwnedPath,
operation: PatchOperationKind,
index: usize,
len: usize,
},
ItemLost {
path: OwnedPath,
index: usize,
},
Unsupported {
path: OwnedPath,
operation: PatchOperationKind,
},
}
impl fmt::Display for CommitOperationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PathNotFound { operation, .. } => write!(
f,
"Staged mutation operation {operation} targets a path that does not exist"
),
Self::ShapeMismatch {
expected, actual, ..
} => {
write!(f, "Staged mutation expected {expected:?}, found {actual:?}")
}
Self::TypeMismatch {
expected, actual, ..
} => write!(
f,
"Staged mutation expected {}, found {}",
expected.type_name(),
actual.type_name()
),
Self::IndexOutOfBounds {
operation,
index,
len,
..
} => write!(
f,
"Staged mutation operation {operation} received index {index} for a sequence of length {len}"
),
Self::ItemLost { index, .. } => write!(
f,
"Staged mutation could not restore the item moved from index {index}"
),
Self::Unsupported { operation, .. } => {
write!(f, "Staged mutation operation {operation} is unsupported")
}
}
}
}
impl CommitOperationError {
pub fn path(&self) -> &OwnedPath {
match self {
Self::PathNotFound { path, .. }
| Self::ShapeMismatch { path, .. }
| Self::TypeMismatch { path, .. }
| Self::IndexOutOfBounds { path, .. }
| Self::ItemLost { path, .. }
| Self::Unsupported { path, .. } => path,
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for CommitOperationError {}
#[derive(Debug)]
pub enum CommitError {
OperationFailed {
index: usize,
error: CommitOperationError,
},
RollbackFailed {
failed_operation: Box<CommitError>,
operation_index: usize,
error: CommitOperationError,
},
}
impl fmt::Display for CommitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::OperationFailed { index, error, .. } => {
write!(f, "Staged mutation operation {index} failed: {error}")
}
Self::RollbackFailed {
operation_index,
error,
..
} => write!(
f,
"Staged mutation rollback for operation {operation_index} failed: {error}"
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for CommitError {}
#[derive(Debug, Default)]
pub struct MutationBatch {
operations: Vec<OwnedPatchOperation>,
}
impl<'a> FromIterator<PatchOperation<'a>> for MutationBatch {
fn from_iter<I: IntoIterator<Item = PatchOperation<'a>>>(operations: I) -> Self {
let mut batch = Self::new();
batch.extend(operations);
batch
}
}
impl<'a, const N: usize> From<[PatchOperation<'a>; N]> for MutationBatch {
fn from(operations: [PatchOperation<'a>; N]) -> Self {
operations.into_iter().collect()
}
}
impl MutationBatch {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.operations.is_empty()
}
pub fn len(&self) -> usize {
self.operations.len()
}
pub fn push(&mut self, operation: impl Into<OwnedPatchOperation>) -> &mut Self {
self.operations.push(operation.into());
self
}
pub fn extend<'a, I>(&mut self, operations: I) -> &mut Self
where
I: IntoIterator<Item = PatchOperation<'a>>,
{
self
.operations
.extend(operations.into_iter().map(OwnedPatchOperation::from));
self
}
#[allow(clippy::result_large_err)]
pub fn commit(self, target: &mut dyn MetaMut) -> Result<MutationRollback, CommitError> {
let mut inverses = self.operations;
for index in 0..inverses.len() {
let placeholder = OwnedPatchOperation::MoveItem {
path: OwnedPath::new(),
from: 0,
to: 0,
};
let operation = mem::replace(&mut inverses[index], placeholder);
match apply_staged_operation(target, operation) {
Ok(inverse) => inverses[index] = inverse,
Err(error) => {
inverses.truncate(index);
let failed_operation = CommitError::OperationFailed { index, error };
let rollback = MutationRollback { inverses };
return Err(match rollback.undo(target) {
Ok(()) => failed_operation,
Err(RollbackError {
inverse_index,
error,
}) => CommitError::RollbackFailed {
failed_operation: Box::new(failed_operation),
operation_index: inverse_index,
error,
},
});
}
}
}
Ok(MutationRollback { inverses })
}
}
fn resolve<'a>(
target: &'a mut dyn MetaMut,
path: &OwnedPath,
operation: PatchOperationKind,
) -> Result<ObjectRefMut<'a>, CommitOperationError> {
resolve_path(target, path).ok_or_else(|| CommitOperationError::PathNotFound {
path: path.clone(),
operation,
})
}
fn resolve_path<'a>(target: &'a mut dyn MetaMut, path: &OwnedPath) -> Option<ObjectRefMut<'a>> {
let mut current = ObjectRefMut::new(target);
for segment in path {
let inner = current.inner;
current = match segment {
PathSegment::Field(name) => inner.field_mut(name)?,
PathSegment::Item(index) => inner.item_mut(index)?,
PathSegment::Key(key) => inner.key_mut(key)?,
};
}
Some(current)
}
fn resolve_container<'a>(
target: &'a mut dyn MetaMut,
path: &OwnedPath,
operation: PatchOperationKind,
expected: ValueKind,
) -> Result<ObjectRefMut<'a>, CommitOperationError> {
let child = resolve(target, path, operation)?;
let actual = child.kind();
if actual != expected {
return Err(CommitOperationError::ShapeMismatch {
path: path.clone(),
expected,
actual,
});
}
Ok(child)
}
fn unsupported_error(path: &OwnedPath, operation: PatchOperationKind) -> CommitOperationError {
CommitOperationError::Unsupported {
path: path.clone(),
operation,
}
}
fn replacement_error(
path: &OwnedPath,
operation: PatchOperationKind,
value: &Object,
actual: TypeInfo,
) -> CommitOperationError {
let expected = value.type_info();
if expected != actual {
CommitOperationError::TypeMismatch {
path: path.clone(),
expected,
actual,
}
} else {
unsupported_error(path, operation)
}
}
fn apply_staged_operation(
target: &mut dyn MetaMut,
operation: OwnedPatchOperation,
) -> Result<OwnedPatchOperation, CommitOperationError> {
match operation {
OwnedPatchOperation::Set { path, value } => {
let operation_kind = PatchOperationKind::Set;
let child = resolve(target, &path, operation_kind)?;
match child.inner.replace(value) {
Ok(previous) => Ok(OwnedPatchOperation::Set {
path,
value: previous,
}),
Err(value) => Err(replacement_error(
&path,
operation_kind,
&value,
child.type_info(),
)),
}
}
OwnedPatchOperation::InsertKey { path, key, value } => {
let operation_kind = PatchOperationKind::InsertKey;
let map = resolve_container(target, &path, operation_kind, ValueKind::Map)?;
let Some(child) = map.inner.key_mut(&key) else {
map
.inner
.insert_key(&key, value)
.map_err(|_| unsupported_error(&path, operation_kind))?;
return Ok(OwnedPatchOperation::RemoveKey { path, key });
};
match child.inner.replace(value) {
Ok(previous) => Ok(OwnedPatchOperation::InsertKey {
path,
key,
value: previous,
}),
Err(value) => Err(replacement_error(
&path,
operation_kind,
&value,
child.type_info(),
)),
}
}
OwnedPatchOperation::RemoveKey { path, key } => {
let operation_kind = PatchOperationKind::RemoveKey;
let map = resolve_container(target, &path, operation_kind, ValueKind::Map)?;
match map.inner.remove_key(&key) {
Some(previous) => Ok(OwnedPatchOperation::InsertKey {
path,
key,
value: previous,
}),
None => Err(CommitOperationError::PathNotFound {
path,
operation: operation_kind,
}),
}
}
OwnedPatchOperation::InsertItem { path, index, value } => {
let operation_kind = PatchOperationKind::InsertItem;
let sequence = resolve_container(target, &path, operation_kind, ValueKind::Sequence)?;
sequence
.inner
.insert_item(index, value)
.map_err(|_| unsupported_error(&path, operation_kind))?;
Ok(OwnedPatchOperation::RemoveItem { path, index })
}
OwnedPatchOperation::PushItem { path, value } => {
let operation_kind = PatchOperationKind::PushItem;
let sequence = resolve_container(target, &path, operation_kind, ValueKind::Sequence)?;
let Some(index) = sequence.len() else {
return Err(unsupported_error(&path, operation_kind));
};
sequence
.inner
.push_item(value)
.map_err(|_| unsupported_error(&path, operation_kind))?;
Ok(OwnedPatchOperation::RemoveItem { path, index })
}
OwnedPatchOperation::RemoveItem { path, index } => {
let operation_kind = PatchOperationKind::RemoveItem;
let sequence = resolve_container(target, &path, operation_kind, ValueKind::Sequence)?;
match sequence.inner.remove_item(index) {
Some(previous) => Ok(OwnedPatchOperation::InsertItem {
path,
index,
value: previous,
}),
None => Err(unsupported_error(&path, operation_kind)),
}
}
OwnedPatchOperation::MoveItem { path, from, to } => {
let operation_kind = PatchOperationKind::MoveItem;
let sequence = resolve_container(target, &path, operation_kind, ValueKind::Sequence)?;
let len = sequence.inner.len();
if let Some((index, len)) =
len.and_then(|len| move_index_out_of_bounds(from, to, len).map(|index| (index, len)))
{
return Err(CommitOperationError::IndexOutOfBounds {
path,
operation: operation_kind,
index,
len,
});
}
sequence
.inner
.move_item(from, to)
.map_err(|error| match error {
MoveItemError::RestoreFailed => CommitOperationError::ItemLost {
path: path.clone(),
index: from,
},
_ => unsupported_error(&path, operation_kind),
})?;
let (from, to) = if from < to {
(to - 1, from)
} else {
(to, from + 1)
};
Ok(OwnedPatchOperation::MoveItem { path, from, to })
}
}
}