Skip to main content

icydb_core/patch/merge/
error.rs

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