noml 0.9.0

High-performance dynamic configuration language with format preservation, environment variables, native types, string interpolation, and TOML compatibility. Blazing-fast parsing at 25µs with zero-copy architecture.
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
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
//! # Error Handling
//!
//! Comprehensive error system for NOML parsing, validation, and manipulation.
//! Designed for clarity, debuggability, and extensibility.

use std::io;
use thiserror::Error;

/// The main result type used throughout NOML operations.
pub type Result<T> = std::result::Result<T, NomlError>;

/// Comprehensive error types for all NOML operations.
///
/// This error system is designed to provide maximum clarity about what went wrong,
/// where it happened, and how to fix it. Each variant includes enough context
/// for both developers and end users to understand and resolve issues.
#[derive(Error, Debug)]
pub enum NomlError {
    #[error("Parse error at line {line}, column {column}: {message}")]
    /// Parsing errors - when the input cannot be parsed due to syntax issues.
    Parse {
        /// Human-readable error message
        message: String,
        /// Line number where error occurred (1-indexed)
        line: usize,
        /// Column number where error occurred (1-indexed)
        column: usize,
        /// Optional source code snippet showing the error
        snippet: Option<String>,
    },

    /// Validation errors - when NOML is syntactically correct but semantically invalid
    #[error("Validation error: {message}")]
    Validation {
        /// Description of the validation failure
        message: String,
        /// Path to the invalid key/section
        path: Option<String>,
    },

    /// Key access errors - when requesting non-existent keys
    #[error("Key '{key}' not found")]
    KeyNotFound {
        /// The key that was requested
        key: String,
        /// Available keys at that level (for suggestions)
        available: Vec<String>,
    },

    /// Type conversion errors - when values cannot be converted to requested type
    #[error("Type error: cannot convert '{value}' to {expected_type}")]
    Type {
        /// The value that couldn't be converted
        value: String,
        /// The expected type
        expected_type: String,
        /// The actual type found
        actual_type: String,
    },

    /// File I/O errors - wraps std::io::Error with additional context
    #[error("File error for '{path}': {source}")]
    Io {
        /// Path to the file that caused the error
        path: String,
        /// The underlying I/O error
        #[source]
        source: io::Error,
    },

    /// Variable interpolation errors
    #[error("Interpolation error: {message}")]
    Interpolation {
        /// Description of the interpolation failure
        message: String,
        /// The variable expression that failed
        expression: String,
        /// Path in the document where this occurred
        context: Option<String>,
    },

    /// Environment variable errors
    #[error("Environment variable '{var}' is not set")]
    EnvVar {
        /// Name of the missing environment variable
        var: String,
        /// Whether a default was expected
        has_default: bool,
    },

    /// Import/include file errors
    #[error("Import error: failed to import '{path}': {reason}")]
    Import {
        /// Path that failed to import
        path: String,
        /// Reason for import failure
        reason: String,
        /// Path of the file that tried to do the import
        from: Option<String>,
    },

    /// Schema validation errors
    #[error("Schema error at '{path}': {message}")]
    Schema {
        /// Path where schema validation failed
        path: String,
        /// Description of the schema violation
        message: String,
        /// Expected schema type/format
        expected: Option<String>,
    },

    /// Circular reference errors (for imports and references)
    #[error("Circular reference detected: {chain}")]
    CircularReference {
        /// The chain of references that caused the cycle
        chain: String,
    },

    /// Internal errors - these should never happen in normal operation
    #[error("Internal error: {message}")]
    Internal {
        /// Description of the internal error
        message: String,
        /// Optional context about where this occurred
        context: Option<String>,
    },
}

impl NomlError {
    /// Create a parse error with position information
    pub fn parse(message: impl Into<String>, line: usize, column: usize) -> Self {
        Self::Parse {
            message: message.into(),
            line,
            column,
            snippet: None,
        }
    }

    /// Create a parse error with source code snippet
    pub fn parse_with_snippet(
        message: impl Into<String>,
        line: usize,
        column: usize,
        snippet: impl Into<String>,
    ) -> Self {
        Self::Parse {
            message: message.into(),
            line,
            column,
            snippet: Some(snippet.into()),
        }
    }

    /// Create a validation error
    pub fn validation(message: impl Into<String>) -> Self {
        Self::Validation {
            message: message.into(),
            path: None,
        }
    }

    /// Create a validation error with path context
    pub fn validation_at(message: impl Into<String>, path: impl Into<String>) -> Self {
        Self::Validation {
            message: message.into(),
            path: Some(path.into()),
        }
    }

    /// Create a key not found error
    pub fn key_not_found(key: impl Into<String>) -> Self {
        Self::KeyNotFound {
            key: key.into(),
            available: Vec::new(),
        }
    }

    /// Create a key not found error with suggestions
    pub fn key_not_found_with_suggestions(key: impl Into<String>, available: Vec<String>) -> Self {
        Self::KeyNotFound {
            key: key.into(),
            available,
        }
    }

    /// Create a type conversion error
    pub fn type_error(
        value: impl Into<String>,
        expected: impl Into<String>,
        actual: impl Into<String>,
    ) -> Self {
        Self::Type {
            value: value.into(),
            expected_type: expected.into(),
            actual_type: actual.into(),
        }
    }

    /// Create an I/O error with path context
    pub fn io(path: impl Into<String>, error: io::Error) -> Self {
        Self::Io {
            path: path.into(),
            source: error,
        }
    }

    /// Create an interpolation error
    pub fn interpolation(message: impl Into<String>, expression: impl Into<String>) -> Self {
        Self::Interpolation {
            message: message.into(),
            expression: expression.into(),
            context: None,
        }
    }

    /// Create an environment variable error
    pub fn env_var(var: impl Into<String>, has_default: bool) -> Self {
        Self::EnvVar {
            var: var.into(),
            has_default,
        }
    }

    /// Create an import error
    pub fn import(path: impl Into<String>, reason: impl Into<String>) -> Self {
        Self::Import {
            path: path.into(),
            reason: reason.into(),
            from: None,
        }
    }

    /// Create a schema validation error
    pub fn schema(path: impl Into<String>, message: impl Into<String>) -> Self {
        Self::Schema {
            path: path.into(),
            message: message.into(),
            expected: None,
        }
    }

    /// Create a circular reference error
    pub fn circular_reference(chain: impl Into<String>) -> Self {
        Self::CircularReference {
            chain: chain.into(),
        }
    }

    /// Create an internal error (should be used sparingly)
    pub fn internal(message: impl Into<String>) -> Self {
        Self::Internal {
            message: message.into(),
            context: None,
        }
    }

    /// Create an enhanced parse error with suggestions for common mistakes
    pub fn parse_with_suggestion(
        message: impl Into<String>,
        line: usize,
        column: usize,
        suggestion: impl Into<String>,
    ) -> Self {
        let message = message.into();
        let suggestion = suggestion.into();
        Self::Parse {
            message: format!("{message}. {suggestion}"),
            line,
            column,
            snippet: None,
        }
    }

    /// Create a parse error for unexpected tokens with suggestions
    pub fn unexpected_token(
        found: impl Into<String>,
        expected: impl Into<String>,
        line: usize,
        column: usize,
    ) -> Self {
        let found = found.into();
        let expected = expected.into();
        Self::Parse {
            message: format!("Expected {expected}, but found {found}"),
            line,
            column,
            snippet: None,
        }
    }

    /// Create a more helpful unknown function error
    pub fn unknown_function(name: impl Into<String>, line: usize, column: usize) -> Self {
        let name = name.into();
        let suggestion = match name.as_str() {
            "ENV" | "Env" => "Did you mean 'env()'?",
            "include" | "INCLUDE" => "Did you mean 'include \"filename\"'?",
            "size" | "SIZE" => "Did you mean '@size()'?",
            "duration" | "DURATION" => "Did you mean '@duration()'?",
            "date" | "DATE" => "Did you mean '@date()'?",
            _ => "Available functions: env(), @size(), @duration(), @date()",
        };

        Self::Parse {
            message: format!("Unknown function '{name}'. {suggestion}"),
            line,
            column,
            snippet: None,
        }
    }

    /// Create a more helpful unknown native type error
    pub fn unknown_native_type(type_name: impl Into<String>, line: usize, column: usize) -> Self {
        let type_name = type_name.into();
        let suggestion = match type_name.as_str() {
            "Size" | "SIZE" => "Did you mean '@size()'?",
            "Duration" | "DURATION" => "Did you mean '@duration()'?",
            "Date" | "DATE" => "Did you mean '@date()'?",
            "url" | "URL" => "Did you mean '@url()'?",
            _ => "Available native types: @size(), @duration(), @date(), @url()",
        };

        Self::Parse {
            message: format!("Unknown native type '@{type_name}'. {suggestion}"),
            line,
            column,
            snippet: None,
        }
    }

    /// Create an error for malformed key paths with suggestions
    pub fn malformed_key_path(path: impl Into<String>, line: usize, column: usize) -> Self {
        let path = path.into();
        Self::Parse {
            message: format!(
                "Malformed key path '{path}'. Keys should be identifiers separated by dots (e.g., 'server.host' or 'database.port')"
            ),
            line,
            column,
            snippet: None,
        }
    }

    /// Check if this error is recoverable
    pub fn is_recoverable(&self) -> bool {
        match self {
            // Parse errors are generally not recoverable
            NomlError::Parse { .. } => false,
            // Validation errors might be recoverable with different input
            NomlError::Validation { .. } => true,
            // Key errors are recoverable (use defaults, etc.)
            NomlError::KeyNotFound { .. } => true,
            // Type errors might be recoverable with conversion
            NomlError::Type { .. } => true,
            // I/O errors depend on the specific error
            NomlError::Io { source, .. } => match source.kind() {
                io::ErrorKind::NotFound => true,
                io::ErrorKind::PermissionDenied => false,
                _ => true,
            },
            // Interpolation errors might be recoverable
            NomlError::Interpolation { .. } => true,
            // Environment variable errors are recoverable with defaults
            NomlError::EnvVar { has_default, .. } => *has_default,
            // Import errors might be recoverable
            NomlError::Import { .. } => true,
            // Schema errors are usually recoverable
            NomlError::Schema { .. } => true,
            // Circular references are not recoverable
            NomlError::CircularReference { .. } => false,
            // Internal errors are not recoverable
            NomlError::Internal { .. } => false,
        }
    }

    /// Get the error category for metrics/logging
    pub fn category(&self) -> &'static str {
        match self {
            NomlError::Parse { .. } => "parse",
            NomlError::Validation { .. } => "validation",
            NomlError::KeyNotFound { .. } => "key_access",
            NomlError::Type { .. } => "type_conversion",
            NomlError::Io { .. } => "io",
            NomlError::Interpolation { .. } => "interpolation",
            NomlError::EnvVar { .. } => "environment",
            NomlError::Import { .. } => "import",
            NomlError::Schema { .. } => "schema",
            NomlError::CircularReference { .. } => "circular_reference",
            NomlError::Internal { .. } => "internal",
        }
    }

    /// Get a user-friendly error message with suggestions
    pub fn user_message(&self) -> String {
        match self {
            NomlError::Parse {
                message,
                line,
                column,
                snippet,
            } => {
                let mut msg = format!("Syntax error on line {line}, column {column}: {message}");
                if let Some(snippet) = snippet {
                    msg.push_str(&format!("\n\n{snippet}"));
                }
                msg.push_str("\n\nTip: Check for missing quotes, brackets, or commas.");
                msg
            }
            NomlError::KeyNotFound { key, available } => {
                let mut msg = format!("The key '{key}' doesn't exist.");
                if !available.is_empty() {
                    msg.push_str("\n\nDid you mean one of these?");
                    for suggestion in available {
                        msg.push_str(&format!("\n  - {suggestion}"));
                    }
                }
                msg
            }
            NomlError::EnvVar { var, has_default } => {
                let mut msg = format!("Environment variable '{var}' is not set.");
                if !has_default {
                    msg.push_str(&format!("\n\nTip: Set the environment variable or provide a default value: env(\"{var}\", \"default_value\")"));
                }
                msg
            }
            _ => self.to_string(),
        }
    }
}

/// Convert from std::io::Error to NomlError
impl From<io::Error> for NomlError {
    fn from(error: io::Error) -> Self {
        Self::Io {
            path: "<unknown>".to_string(),
            source: error,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn error_creation_and_display() {
        let err = NomlError::parse("Invalid syntax", 10, 5);
        assert_eq!(
            err.to_string(),
            "Parse error at line 10, column 5: Invalid syntax"
        );
    }

    #[test]
    fn error_categories() {
        let parse_err = NomlError::parse("test", 1, 1);
        assert_eq!(parse_err.category(), "parse");
        assert!(!parse_err.is_recoverable());

        let key_err = NomlError::key_not_found("test.key");
        assert_eq!(key_err.category(), "key_access");
        assert!(key_err.is_recoverable());
    }

    #[test]
    fn user_friendly_messages() {
        let err = NomlError::key_not_found_with_suggestions(
            "unknown_key",
            vec!["known_key".to_string(), "other_key".to_string()],
        );
        let msg = err.user_message();
        assert!(msg.contains("unknown_key"));
        assert!(msg.contains("known_key"));
        assert!(msg.contains("other_key"));
    }
}