Skip to main content

icydb_core/patch/merge/
error.rs

1//! Module: patch::merge::error
2//! Responsibility: module-local ownership and contracts for patch::merge::error.
3//! Does not own: cross-module orchestration outside this module.
4//! Boundary: exposes this module API while keeping implementation details internal.
5
6use thiserror::Error as ThisError;
7
8///
9/// MergePatchError
10///
11/// Structured failures for user-driven patch application.
12///
13
14#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
15pub enum MergePatchError {
16    #[error("invalid patch shape: expected {expected}, found {actual}")]
17    InvalidShape {
18        expected: &'static str,
19        actual: &'static str,
20    },
21
22    #[error("invalid patch cardinality: expected {expected}, found {actual}")]
23    CardinalityViolation { expected: usize, actual: usize },
24
25    #[error("patch merge failed at {path}: {source}")]
26    Context {
27        path: String,
28        #[source]
29        source: Box<Self>,
30    },
31}
32
33impl MergePatchError {
34    /// Prepend a field segment to the merge error path.
35    #[must_use]
36    pub fn with_field(self, field: impl AsRef<str>) -> Self {
37        self.with_path_segment(field.as_ref())
38    }
39
40    /// Prepend an index segment to the merge error path.
41    #[must_use]
42    pub fn with_index(self, index: usize) -> Self {
43        self.with_path_segment(format!("[{index}]"))
44    }
45
46    /// Return the full contextual path, if available.
47    #[must_use]
48    pub const fn path(&self) -> Option<&str> {
49        match self {
50            Self::Context { path, .. } => Some(path.as_str()),
51            _ => None,
52        }
53    }
54
55    /// Return the innermost, non-context merge error variant.
56    #[must_use]
57    pub fn leaf(&self) -> &Self {
58        match self {
59            Self::Context { source, .. } => source.leaf(),
60            _ => self,
61        }
62    }
63
64    #[must_use]
65    fn with_path_segment(self, segment: impl Into<String>) -> Self {
66        let segment = segment.into();
67        match self {
68            Self::Context { path, source } => Self::Context {
69                path: Self::join_segments(segment.as_str(), path.as_str()),
70                source,
71            },
72            source => Self::Context {
73                path: segment,
74                source: Box::new(source),
75            },
76        }
77    }
78
79    #[must_use]
80    fn join_segments(prefix: &str, suffix: &str) -> String {
81        if suffix.starts_with('[') {
82            format!("{prefix}{suffix}")
83        } else {
84            format!("{prefix}.{suffix}")
85        }
86    }
87}