1use reqwest::StatusCode;
2use scarb_metadata::{Metadata, PackageId};
3use std::fmt::{self, Formatter};
4use thiserror::Error;
5use url::Url;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ErrorCode {
10 E001,
12 E002,
14 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
28fn find_closest_match(target: &str, candidates: &[String]) -> Option<String> {
30 if candidates.is_empty() {
31 return None;
32 }
33
34 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 if best_distance <= target.len() / 2 + 1 {
48 best_match
49 } else {
50 None
51 }
52}
53
54fn 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, matrix[i + 1][j] + 1, ),
83 matrix[i][j] + cost, );
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 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 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}