appcore_filemaker/
error.rs1use std::fmt;
14
15use thiserror::Error;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum ErrorCode {
20 SchemaSyntax,
22 SchemaVersion,
24 SchemaField,
26 DataType,
28 DataCycle,
30 AssetSandbox,
32 AssetInvalid,
34 FontMissing,
36 GeometryInvalid,
38 LayoutInvalid,
40 LayoutNonConvergent,
42 PatchInvalid,
44 PatchLocked,
46 ExportUnsupported,
48 ExportWrite,
50 LimitExceeded,
52 Cancelled,
54 Validation,
56}
57
58impl ErrorCode {
59 #[must_use]
61 pub const fn as_str(self) -> &'static str {
62 match self {
63 Self::SchemaSyntax => "FM-SCHEMA-SYNTAX",
64 Self::SchemaVersion => "FM-SCHEMA-VERSION",
65 Self::SchemaField => "FM-SCHEMA-FIELD",
66 Self::DataType => "FM-DATA-TYPE",
67 Self::DataCycle => "FM-DATA-CYCLE",
68 Self::AssetSandbox => "FM-ASSET-SANDBOX",
69 Self::AssetInvalid => "FM-ASSET-INVALID",
70 Self::FontMissing => "FM-FONT-MISSING",
71 Self::GeometryInvalid => "FM-GEOM-INVALID",
72 Self::LayoutInvalid => "FM-LAYOUT-INVALID",
73 Self::LayoutNonConvergent => "FM-LAYOUT-NON-CONVERGENT",
74 Self::PatchInvalid => "FM-PATCH-INVALID",
75 Self::PatchLocked => "FM-PATCH-LOCKED",
76 Self::ExportUnsupported => "FM-EXPORT-UNSUPPORTED",
77 Self::ExportWrite => "FM-EXPORT-WRITE",
78 Self::LimitExceeded => "FM-LIMIT-EXCEEDED",
79 Self::Cancelled => "FM-CANCELLED",
80 Self::Validation => "FM-VALIDATION",
81 }
82 }
83}
84
85#[derive(Debug, Error)]
87#[error("{code}: {message}")]
88pub struct FileMakerError {
89 code: CodeDisplay,
90 message: String,
91 source_path: Option<String>,
92}
93
94impl FileMakerError {
95 #[must_use]
98 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
99 let message = bounded_diagnostic(message.into(), 1_024);
100 Self {
101 code: CodeDisplay(code),
102 message,
103 source_path: None,
104 }
105 }
106
107 #[must_use]
109 pub fn at(mut self, source_path: impl Into<String>) -> Self {
110 let source_path = bounded_diagnostic(source_path.into(), 512);
111 self.source_path = Some(source_path);
112 self
113 }
114
115 #[must_use]
117 pub const fn code(&self) -> ErrorCode {
118 self.code.0
119 }
120
121 #[must_use]
123 pub fn message(&self) -> &str {
124 &self.message
125 }
126
127 #[must_use]
129 pub fn source_path(&self) -> Option<&str> {
130 self.source_path.as_deref()
131 }
132}
133
134#[derive(Debug)]
135struct CodeDisplay(ErrorCode);
136
137impl fmt::Display for CodeDisplay {
138 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
139 formatter.write_str(self.0.as_str())
140 }
141}
142
143pub type Result<T> = std::result::Result<T, FileMakerError>;
145
146pub(crate) fn bounded_diagnostic(mut text: String, limit: usize) -> String {
148 let mut end = text.len().min(limit);
149 while !text.is_char_boundary(end) {
150 end -= 1;
151 }
152 if text.capacity() > limit {
153 return text[..end].to_owned();
154 }
155 text.truncate(end);
156 text
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn diagnostic_reports_share_utf8_bounds_and_release_oversized_buffers() {
165 let mut text = String::with_capacity(1024 * 1024);
166 text.push_str("short");
167 let bounded = bounded_diagnostic(text, 512);
168 assert_eq!(bounded, "short");
169 assert!(bounded.capacity() <= 512);
170 let text = "日".repeat(200);
171 let mut report = crate::ValidationReport::default();
172 report.push(
173 crate::ValidationSeverity::Warning,
174 crate::ValidationCode::Overflow,
175 None,
176 None,
177 text.clone(),
178 1,
179 );
180 assert_eq!(report.issues[0].message, "日".repeat(170));
181 let mut losses = crate::ExportLossReport::default();
182 losses.push(crate::ExportLossKind::UnsupportedElement, None, text);
183 assert_eq!(losses.losses[0].message, "日".repeat(170));
184 }
185
186 #[test]
187 fn unicode_diagnostics_are_bounded_without_splitting_characters() {
188 for symbol in ["a", "é", "日", "🦀"] {
189 for prefix in 0..4 {
190 let text = format!("{}{}", "x".repeat(prefix), symbol.repeat(1100));
191 let error =
192 FileMakerError::new(ErrorCode::SchemaField, text.clone()).at(text.clone());
193 assert!(error.message().len() <= 1024);
194 assert!(text.starts_with(error.message()));
195 assert!(error.source_path().unwrap().len() <= 512);
196 assert!(text.starts_with(error.source_path().unwrap()));
197 }
198 }
199 }
200}