Skip to main content

notedthat_write/
error.rs

1//! Error types for the shared write path.
2
3use notedthat_core::{Error as CoreError, StorageError};
4
5/// Errors returned by shared write operations.
6#[derive(Debug, thiserror::Error)]
7pub enum WriteError {
8    /// Storage-layer failure.
9    #[error(transparent)]
10    Storage(StorageError),
11    /// Upload size exceeded the configured limit.
12    #[error("payload too large: {size} bytes (limit {limit})")]
13    TooLarge {
14        /// Actual byte size.
15        size: u64,
16        /// Maximum allowed byte size.
17        limit: u64,
18    },
19    /// Path/domain validation failure.
20    #[error(transparent)]
21    Path(CoreError),
22    /// Indexer queue was full while enqueueing an upsert.
23    #[error("indexer queue full during upsert")]
24    IndexerBackpressureUpsert,
25    /// Indexer queue was full while enqueueing a tombstone.
26    #[error("indexer queue full during tombstone")]
27    IndexerBackpressureTombstone,
28    /// Object body exceeds the `NOTEDTHAT_MAX_PATCHABLE_SIZE` limit before or after splice.
29    #[error("patch payload too large: {size} bytes (limit {limit})")]
30    PatchTooLarge {
31        /// Actual byte size.
32        size: u64,
33        /// Maximum allowed byte size.
34        limit: u64,
35    },
36    /// Requested line range is beyond the end of the object at server-side splice time.
37    #[error("line range {first}..{last} out of range (total {total_lines} lines)")]
38    PatchLineOutOfRange {
39        /// Requested first line.
40        first: u64,
41        /// Requested last line.
42        last: u64,
43        /// Total line count in the object.
44        total_lines: u64,
45        /// Total byte count in the object (for X-Content-Range-Bytes).
46        total_bytes: u64,
47    },
48    /// Invalid range, mode contradiction, or missing If-Match for PATCH.
49    #[error("invalid patch request: {message}")]
50    PatchInvalidRange {
51        /// Human-readable reason.
52        message: String,
53    },
54    /// Replace operation found no occurrence of the requested old string.
55    #[error("replace: no match found for old_string")]
56    ReplaceNoMatch,
57    /// Replace operation found multiple occurrences, making single replace ambiguous.
58    #[error("replace: found {count} matches; use replace_all to replace them all")]
59    ReplaceAmbiguous {
60        /// Number of matches found in the object body.
61        count: u64,
62    },
63}
64
65impl From<StorageError> for WriteError {
66    fn from(err: StorageError) -> Self {
67        Self::Storage(err)
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::WriteError;
74
75    #[test]
76    fn patch_too_large_displays_size_limit_when_constructed() {
77        let err = WriteError::PatchTooLarge {
78            size: 200 * 1024 * 1024,
79            limit: 100 * 1024 * 1024,
80        };
81
82        assert!(err.to_string().contains("too large"));
83    }
84
85    #[test]
86    fn patch_line_out_of_range_constructs_with_line_and_byte_totals() {
87        let err = WriteError::PatchLineOutOfRange {
88            first: 999,
89            last: 1000,
90            total_lines: 20,
91            total_bytes: 100,
92        };
93
94        assert!(err.to_string().contains("out of range"));
95    }
96
97    #[test]
98    fn patch_invalid_range_displays_reason_when_constructed() {
99        let err = WriteError::PatchInvalidRange {
100            message: "test".into(),
101        };
102
103        assert!(err.to_string().contains("invalid patch"));
104    }
105
106    #[test]
107    fn replace_ambiguous_displays_match_count_when_constructed() {
108        let err = WriteError::ReplaceAmbiguous { count: 3 };
109
110        assert_eq!(
111            err.to_string(),
112            "replace: found 3 matches; use replace_all to replace them all"
113        );
114    }
115}