sublime_node_tools 0.0.4

Node.js bindings for Sublime Workspace CLI Tools via napi-rs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
//! Error handling module for Node.js bindings.
//!
//! # What
//!
//! This module provides error types and conversion utilities for the NAPI layer.
//! It defines the `ErrorInfo` structure that represents errors in a Node.js-friendly
//! format with error codes following Node.js conventions (e.g., EVALIDATION, EGIT, ENOENT).
//!
//! # How
//!
//! The module provides:
//! - `ErrorInfo`: A NAPI-compatible struct with error code, message, context, and kind
//! - `ErrorCode`: An enumeration of all possible error codes
//! - Conversion traits from `CliError` to `ErrorInfo`
//!
//! Error codes follow Node.js conventions:
//! - `ECONFIG`: Configuration errors
//! - `EVALIDATION`: Parameter validation errors
//! - `EEXEC`: Execution errors
//! - `EGIT`: Git-related errors
//! - `EPKG`: Package-related errors
//! - `ENOENT`: File/path not found
//! - `EIO`: I/O errors
//! - `ENETWORK`: Network errors
//! - `EUSER`: User-caused errors
//! - `ETIMEOUT`: Timeout errors
//!
//! # Why
//!
//! Node.js developers expect error codes in a specific format. This module bridges
//! the gap between Rust's error handling and JavaScript's error conventions, providing
//! a consistent and familiar error interface for Node.js consumers.
//!
//! # Examples
//!
//! ```typescript
//! import { status } from '@websublime/workspace-tools';
//!
//! const result = await status({ root: '/invalid/path' });
//! if (!result.success) {
//!   console.error(`Error [${result.error.code}]: ${result.error.message}`);
//!   // Output: Error [ENOENT]: Path not found: /invalid/path
//! }
//! ```

use sublime_cli_tools::error::CliError;

/// Node.js-style error codes for categorizing errors.
///
/// These codes follow Node.js conventions and provide a familiar interface
/// for JavaScript/TypeScript developers to handle errors programmatically.
///
/// # Examples
///
/// ```rust
/// use sublime_node_tools::error::ErrorCode;
///
/// let code = ErrorCode::Validation;
/// assert_eq!(code.as_str(), "EVALIDATION");
/// ```
// Allow dead code for placeholder types that will be used in future stories
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ErrorCode {
    /// Configuration-related errors (invalid config, config not found).
    Config,
    /// Parameter validation errors (invalid arguments, invalid state).
    Validation,
    /// Execution errors (command failed, operation failed).
    Execution,
    /// Git-related errors (repository not found, git operation failed).
    Git,
    /// Package-related errors (package not found, invalid package.json).
    Package,
    /// File or path not found errors.
    NotFound,
    /// I/O errors (permission denied, disk full).
    Io,
    /// Network errors (registry unreachable, download failed).
    Network,
    /// User-caused errors (invalid input, cancelled operation).
    User,
    /// Timeout errors (operation timed out).
    Timeout,
}

impl ErrorCode {
    /// Returns the string representation of the error code.
    ///
    /// These strings follow Node.js error code conventions (e.g., ENOENT, EIO).
    ///
    /// # Returns
    ///
    /// A static string slice containing the error code.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use sublime_node_tools::error::ErrorCode;
    ///
    /// assert_eq!(ErrorCode::NotFound.as_str(), "ENOENT");
    /// assert_eq!(ErrorCode::Io.as_str(), "EIO");
    /// assert_eq!(ErrorCode::Validation.as_str(), "EVALIDATION");
    /// ```
    #[must_use]
    pub(crate) const fn as_str(self) -> &'static str {
        match self {
            Self::Config => "ECONFIG",
            Self::Validation => "EVALIDATION",
            Self::Execution => "EEXEC",
            Self::Git => "EGIT",
            Self::Package => "EPKG",
            Self::NotFound => "ENOENT",
            Self::Io => "EIO",
            Self::Network => "ENETWORK",
            Self::User => "EUSER",
            Self::Timeout => "ETIMEOUT",
        }
    }
}

impl std::fmt::Display for ErrorCode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

/// Error information structure for Node.js bindings.
///
/// This structure is exposed to JavaScript/TypeScript via napi-rs and provides
/// detailed error information in a format familiar to Node.js developers.
///
/// # Fields
///
/// - `code`: Node.js-style error code (e.g., "EVALIDATION", "EGIT")
/// - `message`: Human-readable error message
/// - `context`: Optional additional context (field name, path, etc.)
/// - `kind`: Error category from the CLI layer
///
/// # Examples
///
/// ```typescript
/// // In JavaScript/TypeScript:
/// if (!result.success) {
///   const { code, message, context, kind } = result.error;
///   console.error(`[${code}] ${message}`);
///   if (context) {
///     console.error(`Context: ${context}`);
///   }
/// }
/// ```
// TODO: will be implemented on story 2.1
// The #[napi(object)] attribute will be added when implementing the full ErrorInfo
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub(crate) struct ErrorInfo {
    /// Node.js-style error code (e.g., "EVALIDATION", "EGIT").
    ///
    /// These codes follow Node.js conventions and can be used for
    /// programmatic error handling in JavaScript/TypeScript.
    pub(crate) code: String,

    /// Human-readable error message.
    ///
    /// This message is suitable for displaying to end users and
    /// provides a clear description of what went wrong.
    pub(crate) message: String,

    /// Optional additional context for the error.
    ///
    /// This may contain the field name that caused a validation error,
    /// the path that was not found, or other relevant context information.
    pub(crate) context: Option<String>,

    /// Error category from the CLI layer.
    ///
    /// This corresponds to the `CliError` variant name (e.g., "Configuration",
    /// "Validation", "Git") and can be used for logging and debugging.
    pub(crate) kind: String,
}

#[allow(dead_code)]
impl ErrorInfo {
    /// Creates a new `ErrorInfo` instance.
    ///
    /// # Arguments
    ///
    /// * `code` - The Node.js-style error code
    /// * `message` - Human-readable error message
    /// * `context` - Optional additional context
    /// * `kind` - Error category from CLI
    ///
    /// # Returns
    ///
    /// A new `ErrorInfo` instance.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use sublime_node_tools::error::ErrorInfo;
    ///
    /// let error = ErrorInfo::new(
    ///     "EVALIDATION",
    ///     "Invalid package name",
    ///     Some("packages[0]"),
    ///     "Validation",
    /// );
    /// ```
    #[must_use]
    pub(crate) fn new(
        code: impl Into<String>,
        message: impl Into<String>,
        context: Option<impl Into<String>>,
        kind: impl Into<String>,
    ) -> Self {
        Self {
            code: code.into(),
            message: message.into(),
            context: context.map(Into::into),
            kind: kind.into(),
        }
    }

    /// Creates an `ErrorInfo` for a validation error.
    ///
    /// # Arguments
    ///
    /// * `message` - The validation error message
    /// * `field` - Optional field name that caused the validation error
    ///
    /// # Returns
    ///
    /// A new `ErrorInfo` with code "EVALIDATION".
    ///
    /// # Examples
    ///
    /// ```rust
    /// use sublime_node_tools::error::ErrorInfo;
    ///
    /// let error = ErrorInfo::validation("Package name cannot be empty", Some("packages"));
    /// assert_eq!(error.code, "EVALIDATION");
    /// ```
    #[must_use]
    pub(crate) fn validation(message: impl Into<String>, field: Option<impl Into<String>>) -> Self {
        Self::new(ErrorCode::Validation.as_str(), message, field, "Validation")
    }

    /// Creates an `ErrorInfo` for a configuration error.
    ///
    /// # Arguments
    ///
    /// * `message` - The configuration error message
    ///
    /// # Returns
    ///
    /// A new `ErrorInfo` with code "ECONFIG".
    #[must_use]
    pub(crate) fn configuration(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::Config.as_str(), message, None::<String>, "Configuration")
    }

    /// Creates an `ErrorInfo` for a not found error.
    ///
    /// # Arguments
    ///
    /// * `message` - The not found error message
    /// * `path` - Optional path that was not found
    ///
    /// # Returns
    ///
    /// A new `ErrorInfo` with code "ENOENT".
    #[must_use]
    pub(crate) fn not_found(message: impl Into<String>, path: Option<impl Into<String>>) -> Self {
        Self::new(ErrorCode::NotFound.as_str(), message, path, "Io")
    }

    /// Creates an `ErrorInfo` for a git error.
    ///
    /// # Arguments
    ///
    /// * `message` - The git error message
    ///
    /// # Returns
    ///
    /// A new `ErrorInfo` with code "EGIT".
    #[must_use]
    pub(crate) fn git(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::Git.as_str(), message, None::<String>, "Git")
    }

    /// Creates an `ErrorInfo` for an execution error.
    ///
    /// # Arguments
    ///
    /// * `message` - The execution error message
    ///
    /// # Returns
    ///
    /// A new `ErrorInfo` with code "EEXEC".
    #[must_use]
    pub(crate) fn execution(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::Execution.as_str(), message, None::<String>, "Execution")
    }

    /// Creates an `ErrorInfo` for a package error.
    ///
    /// # Arguments
    ///
    /// * `message` - The package error message
    ///
    /// # Returns
    ///
    /// A new `ErrorInfo` with code "EPKG".
    #[must_use]
    pub(crate) fn package(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::Package.as_str(), message, None::<String>, "Package")
    }

    /// Creates an `ErrorInfo` for an I/O error.
    ///
    /// # Arguments
    ///
    /// * `message` - The I/O error message
    /// * `path` - Optional path related to the I/O error
    ///
    /// # Returns
    ///
    /// A new `ErrorInfo` with code "EIO".
    #[must_use]
    pub(crate) fn io(message: impl Into<String>, path: Option<impl Into<String>>) -> Self {
        Self::new(ErrorCode::Io.as_str(), message, path, "Io")
    }

    /// Creates an `ErrorInfo` for a network error.
    ///
    /// # Arguments
    ///
    /// * `message` - The network error message
    ///
    /// # Returns
    ///
    /// A new `ErrorInfo` with code "ENETWORK".
    #[must_use]
    pub(crate) fn network(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::Network.as_str(), message, None::<String>, "Network")
    }

    /// Creates an `ErrorInfo` for a user error.
    ///
    /// # Arguments
    ///
    /// * `message` - The user error message
    ///
    /// # Returns
    ///
    /// A new `ErrorInfo` with code "EUSER".
    #[must_use]
    pub(crate) fn user(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::User.as_str(), message, None::<String>, "User")
    }

    /// Creates an `ErrorInfo` for a timeout error.
    ///
    /// # Arguments
    ///
    /// * `message` - The timeout error message
    ///
    /// # Returns
    ///
    /// A new `ErrorInfo` with code "ETIMEOUT".
    #[must_use]
    pub(crate) fn timeout(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::Timeout.as_str(), message, None::<String>, "Timeout")
    }
}

impl From<&CliError> for ErrorInfo {
    /// Converts a `CliError` reference to an `ErrorInfo`.
    ///
    /// This conversion maps CLI error variants to appropriate Node.js-style
    /// error codes while preserving the original error message and kind.
    ///
    /// # Mapping
    ///
    /// | CliError Variant | ErrorCode |
    /// |------------------|-----------|
    /// | Configuration    | ECONFIG   |
    /// | Validation       | EVALIDATION |
    /// | Execution        | EEXEC     |
    /// | Git              | EGIT      |
    /// | Package          | EPKG      |
    /// | Io               | EIO       |
    /// | Network          | ENETWORK  |
    /// | User             | EUSER     |
    fn from(error: &CliError) -> Self {
        let code = match error {
            CliError::Configuration(_) => ErrorCode::Config,
            CliError::Validation(_) => ErrorCode::Validation,
            CliError::Execution(_) => ErrorCode::Execution,
            CliError::Git(_) => ErrorCode::Git,
            CliError::Package(_) => ErrorCode::Package,
            CliError::Io(_) => ErrorCode::Io,
            CliError::Network(_) => ErrorCode::Network,
            CliError::User(_) => ErrorCode::User,
        };

        Self::new(code.as_str(), error.to_string(), None::<String>, error.kind())
    }
}

impl From<CliError> for ErrorInfo {
    /// Converts a `CliError` to an `ErrorInfo`.
    ///
    /// This is a convenience implementation that delegates to the reference
    /// conversion implementation.
    fn from(error: CliError) -> Self {
        Self::from(&error)
    }
}