verifier/
errors.rs

1use reqwest::StatusCode;
2use scarb_metadata::{Metadata, PackageId};
3use std::fmt::{self, Formatter};
4use thiserror::Error;
5use url::Url;
6
7/// Error codes for programmatic handling
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ErrorCode {
10    /// Package not found in workspace
11    E001,
12    /// HTTP request failed
13    E002,
14    /// Contract not found in manifest
15    E003,
16}
17
18impl ErrorCode {
19    pub const fn as_str(self) -> &'static str {
20        match self {
21            Self::E001 => "E001",
22            Self::E002 => "E002",
23            Self::E003 => "E003",
24        }
25    }
26}
27
28/// Helper function for fuzzy string matching to suggest alternatives
29fn find_closest_match(target: &str, candidates: &[String]) -> Option<String> {
30    if candidates.is_empty() {
31        return None;
32    }
33
34    // Simple fuzzy matching: find the candidate with minimum edit distance
35    let mut best_match = None;
36    let mut best_distance = usize::MAX;
37
38    for candidate in candidates {
39        let distance = edit_distance(target, candidate);
40        if distance < best_distance {
41            best_distance = distance;
42            best_match = Some(candidate.clone());
43        }
44    }
45
46    // Only suggest if the distance is reasonable (less than half the target length)
47    if best_distance <= target.len() / 2 + 1 {
48        best_match
49    } else {
50        None
51    }
52}
53
54/// Simple edit distance calculation (Levenshtein distance)
55fn edit_distance(s1: &str, s2: &str) -> usize {
56    let len1 = s1.len();
57    let len2 = s2.len();
58
59    if len1 == 0 {
60        return len2;
61    }
62    if len2 == 0 {
63        return len1;
64    }
65
66    let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
67
68    for (i, row) in matrix.iter_mut().enumerate().take(len1 + 1) {
69        row[0] = i;
70    }
71    for j in 0..=len2 {
72        matrix[0][j] = j;
73    }
74
75    for (i, c1) in s1.chars().enumerate() {
76        for (j, c2) in s2.chars().enumerate() {
77            let cost = if c1 == c2 { 0 } else { 1 };
78            matrix[i + 1][j + 1] = std::cmp::min(
79                std::cmp::min(
80                    matrix[i][j + 1] + 1, // deletion
81                    matrix[i + 1][j] + 1, // insertion
82                ),
83                matrix[i][j] + cost, // substitution
84            );
85        }
86    }
87
88    matrix[len1][len2]
89}
90
91#[derive(Debug, Error)]
92pub struct MissingPackage {
93    pub package_id: PackageId,
94    pub available: Vec<PackageId>,
95}
96
97impl MissingPackage {
98    #[must_use]
99    pub fn new(package_id: &PackageId, metadata: &Metadata) -> Self {
100        Self {
101            package_id: package_id.clone(),
102            available: metadata.workspace.members.clone(),
103        }
104    }
105
106    pub const fn error_code(&self) -> ErrorCode {
107        ErrorCode::E001
108    }
109}
110
111impl fmt::Display for MissingPackage {
112    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
113        writeln!(
114            formatter,
115            "[{}] Package '{}' not found in workspace.",
116            self.error_code().as_str(),
117            self.package_id
118        )?;
119
120        if self.available.is_empty() {
121            writeln!(formatter, "\nNo packages are available in this workspace.")?;
122            writeln!(formatter, "\nSuggestions:")?;
123            writeln!(formatter, "  • Check if you're in the correct directory")?;
124            writeln!(formatter, "  • Verify that Scarb.toml exists and is valid")?;
125            writeln!(
126                formatter,
127                "  • Run 'scarb metadata' to check workspace structure"
128            )?;
129        } else {
130            writeln!(formatter, "\nAvailable packages in this workspace:")?;
131            for package in &self.available {
132                writeln!(formatter, "  • {package}")?;
133            }
134
135            // Find closest match for suggestion
136            let package_names: Vec<String> = self.available.iter().map(|p| p.to_string()).collect();
137            if let Some(suggestion) =
138                find_closest_match(&self.package_id.to_string(), &package_names)
139            {
140                writeln!(formatter, "\nDid you mean '{suggestion}'?")?;
141            }
142
143            writeln!(formatter, "\nSuggestions:")?;
144            writeln!(formatter, "  • Use --package <name> to specify a package")?;
145            writeln!(formatter, "  • Check spelling of the package name")?;
146            writeln!(formatter, "  • Run 'scarb metadata' to list all packages")?;
147        }
148
149        Ok(())
150    }
151}
152
153#[derive(Debug, Error)]
154pub struct RequestFailure {
155    pub url: Url,
156    pub status: StatusCode,
157    pub msg: String,
158}
159
160impl RequestFailure {
161    pub fn new(url: Url, status: StatusCode, msg: impl Into<String>) -> Self {
162        Self {
163            url,
164            status,
165            msg: msg.into(),
166        }
167    }
168
169    pub const fn error_code(&self) -> ErrorCode {
170        ErrorCode::E002
171    }
172}
173
174impl fmt::Display for RequestFailure {
175    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
176        writeln!(
177            formatter,
178            "[{}] HTTP request failed: {} returned status {}",
179            self.error_code().as_str(),
180            self.url,
181            self.status
182        )?;
183
184        if !self.msg.is_empty() {
185            writeln!(formatter, "\nServer response: {}", self.msg)?;
186        }
187
188        writeln!(formatter, "\nSuggestions:")?;
189        match self.status.as_u16() {
190            400 => {
191                writeln!(
192                    formatter,
193                    "  • Check that all required parameters are provided"
194                )?;
195                writeln!(formatter, "  • Verify the request format is correct")?;
196            }
197            401 => {
198                writeln!(formatter, "  • Check your authentication credentials")?;
199                writeln!(formatter, "  • Verify API key is valid and not expired")?;
200            }
201            403 => {
202                writeln!(
203                    formatter,
204                    "  • Check that you have permission for this operation"
205                )?;
206                writeln!(
207                    formatter,
208                    "  • Verify your account has the required access level"
209                )?;
210            }
211            404 => {
212                writeln!(formatter, "  • Check that the URL is correct: {}", self.url)?;
213                writeln!(formatter, "  • Verify the resource exists")?;
214                writeln!(formatter, "  • Check if the service is running")?;
215            }
216            413 => {
217                writeln!(
218                    formatter,
219                    "  • The request payload is too large (maximum 10MB)"
220                )?;
221                writeln!(
222                    formatter,
223                    "  • Consider reducing the size of your project files"
224                )?;
225                writeln!(formatter, "  • Remove unnecessary files or large assets")?;
226                writeln!(
227                    formatter,
228                    "  • Try without --test-files or --lock-file flags"
229                )?;
230                writeln!(
231                    formatter,
232                    "  • Check for large binary files or dependencies"
233                )?;
234            }
235            429 => {
236                writeln!(formatter, "  • Wait a moment before retrying")?;
237                writeln!(formatter, "  • Consider reducing request frequency")?;
238            }
239            500..=599 => {
240                writeln!(formatter, "  • The server is experiencing issues")?;
241                writeln!(formatter, "  • Try again in a few minutes")?;
242                writeln!(formatter, "  • Check service status if available")?;
243            }
244            _ => {
245                writeln!(formatter, "  • Check your internet connection")?;
246                writeln!(formatter, "  • Verify the server URL is correct")?;
247                writeln!(formatter, "  • Try again in a few moments")?;
248            }
249        }
250
251        Ok(())
252    }
253}
254
255#[derive(Debug, Error)]
256pub struct MissingContract {
257    pub name: String,
258    pub available: Vec<String>,
259}
260
261impl MissingContract {
262    #[must_use]
263    pub const fn new(name: String, available: Vec<String>) -> Self {
264        Self { name, available }
265    }
266
267    pub const fn error_code(&self) -> ErrorCode {
268        ErrorCode::E003
269    }
270}
271
272impl fmt::Display for MissingContract {
273    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
274        writeln!(
275            formatter,
276            "[{}] Contract '{}' not found in manifest file.",
277            self.error_code().as_str(),
278            self.name
279        )?;
280
281        if self.available.is_empty() {
282            writeln!(
283                formatter,
284                "\nNo contracts are defined in the manifest file."
285            )?;
286            writeln!(formatter, "\nSuggestions:")?;
287            writeln!(
288                formatter,
289                "  • Add a [tool.voyager] section to your Scarb.toml"
290            )?;
291            writeln!(formatter, "  • Define your contracts in the manifest file")?;
292            writeln!(
293                formatter,
294                "  • Check the documentation for contract configuration"
295            )?;
296        } else {
297            writeln!(formatter, "\nAvailable contracts:")?;
298            for contract in &self.available {
299                writeln!(formatter, "  • {contract}")?;
300            }
301
302            // Provide fuzzy match suggestion
303            if let Some(suggestion) = find_closest_match(&self.name, &self.available) {
304                writeln!(formatter, "\nDid you mean '{suggestion}'?")?;
305            }
306
307            writeln!(formatter, "\nSuggestions:")?;
308            writeln!(
309                formatter,
310                "  • Use --contract-name <name> to specify a contract"
311            )?;
312            writeln!(formatter, "  • Check spelling of the contract name")?;
313            writeln!(
314                formatter,
315                "  • Verify the contract is defined in [tool.voyager] section"
316            )?;
317        }
318
319        Ok(())
320    }
321}