confetti_rs/
lib.rs

1/*!
2# Confetti-rs
3
4A configuration language and parser library for Rust, with a flexible mapper for converting between configuration files and Rust structs.
5
6## Features
7
8- Simple, intuitive configuration syntax
9- A powerful parser with customizable options
10- Automatic mapping between configuration and Rust structs
11- Support for custom data types
12- Comprehensive error handling
13
14## Basic Usage
15
16Here's a simple example of how to use Confetti-rs:
17
18```rust
19use confetti_rs::{ConfMap, from_str, to_string};
20use std::error::Error;
21
22// Define a configuration structure
23#[derive(ConfMap, Debug)]
24struct ServerConfig {
25    host: String,
26    port: i32,
27    #[conf_map(name = "ssl-enabled")]
28    ssl_enabled: bool,
29    max_connections: Option<i32>,
30}
31
32fn main() -> Result<(), Box<dyn Error>> {
33    // Configuration string in Confetti syntax
34    let config_str = r#"
35    ServerConfig {
36        host "localhost";
37        port 8080;
38        ssl-enabled false;
39        max_connections 100;
40    }
41    "#;
42
43    // Parse the configuration
44    let server_config = from_str::<ServerConfig>(config_str)?;
45    println!("Loaded config: {:?}", server_config);
46
47    // Modify the configuration
48    let new_config = ServerConfig {
49        host: "0.0.0.0".to_string(),
50        port: 443,
51        ssl_enabled: true,
52        max_connections: Some(200),
53    };
54
55    // Serialize to a string
56    let serialized = to_string(&new_config)?;
57    println!("Serialized config:\n{}", serialized);
58
59    Ok(())
60}
61```
62
63## Configuration Syntax
64
65Confetti-rs uses a simple, readable syntax:
66
67```ignore
68DirectiveName {
69  nested_directive "value";
70  another_directive 123;
71
72  block_directive {
73    setting true;
74    array 1, 2, 3, 4;
75  }
76}
77```
78
79## Documentation
80
81For more examples and detailed documentation, please visit:
82- [GitHub repository](https://github.com/shkmv/confetti-rs)
83- [Comprehensive documentation on docs.rs](https://docs.rs/confetti-rs)
84*/
85
86use std::error::Error;
87use std::fmt;
88use std::ops::Range;
89
90pub mod lexer;
91pub mod mapper;
92pub mod parser;
93
94#[cfg(feature = "derive")]
95pub use confetti_derive::ConfMap;
96
97// Private module for derive macro implementation details
98#[doc(hidden)]
99pub mod __private {
100    pub fn is_option_type(type_name: &str) -> bool {
101        type_name.starts_with("core::option::Option<")
102            || type_name.starts_with("std::option::Option<")
103    }
104
105    pub fn extract_option_type(type_name: &str) -> Option<&str> {
106        if is_option_type(type_name) {
107            // Extract the inner type from Option<T>
108            let start = type_name.find('<')? + 1;
109            let end = type_name.rfind('>')?;
110            Some(&type_name[start..end])
111        } else {
112            None
113        }
114    }
115
116    pub fn strip_quotes(value: &str) -> String {
117        let mut result = value.to_string();
118        if result.starts_with('"') && result.ends_with('"') {
119            result = result[1..result.len() - 1].to_string();
120        }
121        result
122    }
123}
124
125/// Represents a configuration argument.
126#[derive(Debug, Clone)]
127pub struct ConfArgument {
128    /// The value of the argument.
129    pub value: String,
130    /// The span of the argument in the source text.
131    pub span: Range<usize>,
132    /// Whether the argument is quoted.
133    pub is_quoted: bool,
134    /// Whether the argument is a triple-quoted string.
135    pub is_triple_quoted: bool,
136    /// Whether the argument is an expression.
137    pub is_expression: bool,
138}
139
140/// Represents a configuration directive.
141#[derive(Debug, Clone)]
142pub struct ConfDirective {
143    /// The name of the directive.
144    pub name: ConfArgument,
145    /// The arguments of the directive.
146    pub arguments: Vec<ConfArgument>,
147    /// The child directives of this directive.
148    pub children: Vec<ConfDirective>,
149}
150
151/// Represents a configuration unit.
152#[derive(Debug, Clone)]
153pub struct ConfUnit {
154    /// The root directives of the configuration.
155    pub directives: Vec<ConfDirective>,
156    /// The comments in the configuration.
157    pub comments: Vec<ConfComment>,
158}
159
160/// Represents a comment in the configuration.
161#[derive(Debug, Clone)]
162pub struct ConfComment {
163    /// The content of the comment.
164    pub content: String,
165    /// The span of the comment in the source text.
166    pub span: Range<usize>,
167    /// Whether the comment is a multi-line comment.
168    pub is_multi_line: bool,
169}
170
171/// Represents an error that can occur during parsing.
172#[derive(Debug)]
173pub enum ConfError {
174    /// An error occurred during lexing.
175    LexerError {
176        /// The position in the source text where the error occurred.
177        position: usize,
178        /// A description of the error.
179        message: String,
180    },
181    /// An error occurred during parsing.
182    ParserError {
183        /// The position in the source text where the error occurred.
184        position: usize,
185        /// A description of the error.
186        message: String,
187    },
188}
189
190impl Error for ConfError {}
191
192impl fmt::Display for ConfError {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        match self {
195            ConfError::LexerError { position, message } => {
196                write!(f, "Lexer error at position {}: {}", position, message)
197            }
198            ConfError::ParserError { position, message } => {
199                write!(f, "Parser error at position {}: {}", position, message)
200            }
201        }
202    }
203}
204
205/// Options for parsing configuration.
206#[derive(Debug, Clone)]
207pub struct ConfOptions {
208    /// Whether to allow C-style comments.
209    pub allow_c_style_comments: bool,
210    /// Whether to allow expression arguments.
211    pub allow_expression_arguments: bool,
212    /// The maximum depth of nested directives.
213    pub max_depth: usize,
214    /// Whether to allow bidirectional formatting characters.
215    pub allow_bidi: bool,
216    /// Whether to require semicolons at the end of directives.
217    pub require_semicolons: bool,
218    /// Whether to allow triple-quoted strings.
219    pub allow_triple_quotes: bool,
220    /// Whether to allow line continuations with backslash.
221    pub allow_line_continuations: bool,
222}
223
224impl Default for ConfOptions {
225    fn default() -> Self {
226        Self {
227            allow_c_style_comments: false,
228            allow_expression_arguments: false,
229            max_depth: 100,
230            allow_bidi: false,
231            require_semicolons: false,
232            allow_triple_quotes: true,
233            allow_line_continuations: true,
234        }
235    }
236}
237
238/// Parses a configuration string.
239///
240/// # Arguments
241///
242/// * `input` - The configuration string to parse.
243/// * `options` - The options for parsing.
244///
245/// # Returns
246///
247/// A `Result` containing either the parsed configuration unit or an error.
248///
249/// # Examples
250///
251/// ```
252/// use confetti_rs::{parse, ConfOptions};
253///
254/// let input = "server {\n  listen 80;\n}";
255/// let options = ConfOptions::default();
256/// let result = parse(input, options);
257/// assert!(result.is_ok());
258/// ```
259pub fn parse(input: &str, options: ConfOptions) -> Result<ConfUnit, ConfError> {
260    let mut parser = parser::Parser::new(input, options)?;
261    parser.parse()
262}
263
264// Re-export key traits from mapper module
265pub use crate::mapper::{FromConf, MapperError, MapperOptions, ToConf, ValueConverter};
266
267// Create convenience wrappers for common operations
268/// Load configuration from a file into a struct
269///
270/// # Example
271///
272/// ```ignore
273/// use confetti_rs::{from_file, ConfMap};
274///
275/// #[derive(ConfMap, Debug)]
276/// struct ServerConfig {
277///     port: i32,
278///     host: String,
279///     #[conf_map(name = "max-connections")]
280///     max_connections: Option<i32>,
281/// }
282///
283/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
284/// # let _server_config = ServerConfig { port: 8080, host: "localhost".into(), max_connections: Some(100) };
285/// // Using from_file with explicit type parameters (both T and P):
286/// // let server_config = from_file::<ServerConfig, _>("config.conf")?;
287/// //
288/// // Alternatively, use the FromConf trait method directly:
289/// // let server_config = ServerConfig::from_file("config.conf")?;
290/// # Ok(())
291/// # }
292/// ```
293///
294/// You can also directly use the `from_file` method from the trait implementation:
295///
296/// ```ignore
297/// use confetti_rs::ConfMap;
298///
299/// #[derive(ConfMap, Debug)]
300/// struct ServerConfig {
301///     port: i32,
302///     host: String,
303/// }
304///
305/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
306/// let server_config = ServerConfig::from_file("config.conf")?;
307/// println!("Server running at {}:{}", server_config.host, server_config.port);
308/// # Ok(())
309/// # }
310/// ```
311pub fn from_file<T: FromConf, P: AsRef<std::path::Path>>(
312    path: P,
313) -> Result<T, mapper::MapperError> {
314    T::from_file(path)
315}
316
317/// Load configuration from a string into a struct
318///
319/// # Example
320///
321/// ```ignore
322/// use confetti_rs::{from_str, ConfMap};
323///
324/// #[derive(ConfMap, Debug)]
325/// struct ServerConfig {
326///     port: i32,
327///     host: String,
328/// }
329///
330/// let config_str = r#"
331/// ServerConfig {
332///   port 8080;
333///   host "localhost";
334/// }
335/// "#;
336///
337/// let server_config = from_str::<ServerConfig>(config_str).unwrap();
338/// assert_eq!(server_config.port, 8080);
339/// assert_eq!(server_config.host, "localhost");
340/// ```
341pub fn from_str<T: FromConf>(s: &str) -> Result<T, mapper::MapperError> {
342    T::from_str(s)
343}
344
345/// Convert a struct to a configuration string
346///
347/// # Example
348///
349/// ```ignore
350/// use confetti_rs::{to_string, ConfMap};
351///
352/// #[derive(ConfMap, Debug)]
353/// struct ServerConfig {
354///     port: i32,
355///     host: String,
356/// }
357///
358/// let server_config = ServerConfig {
359///     port: 8080,
360///     host: "localhost".into(),
361/// };
362///
363/// let config_str = to_string(&server_config).unwrap();
364/// assert!(config_str.contains("port 8080"));
365/// assert!(config_str.contains("host \"localhost\""));
366/// ```
367pub fn to_string<T: ToConf>(value: &T) -> Result<String, mapper::MapperError> {
368    value.to_string()
369}
370
371/// Save a struct to a configuration file
372///
373/// # Example
374///
375/// ```ignore
376/// use confetti_rs::{to_file, ConfMap};
377///
378/// #[derive(ConfMap, Debug)]
379/// struct ServerConfig {
380///     port: i32,
381///     host: String,
382/// }
383///
384/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
385/// let server_config = ServerConfig {
386///     port: 8080,
387///     host: "localhost".into(),
388/// };
389///
390/// // to_file(&server_config, "config.conf")?;
391/// # Ok(())
392/// # }
393/// ```
394pub fn to_file<T: ToConf, P: AsRef<std::path::Path>>(
395    value: &T,
396    path: P,
397) -> Result<(), mapper::MapperError> {
398    value.to_file(path)
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    #[test]
406    fn test_conf_error_display() {
407        let lexer_error = ConfError::LexerError {
408            position: 10,
409            message: "Invalid character".to_string(),
410        };
411        assert_eq!(
412            lexer_error.to_string(),
413            "Lexer error at position 10: Invalid character"
414        );
415
416        let parser_error = ConfError::ParserError {
417            position: 20,
418            message: "Unexpected token".to_string(),
419        };
420        assert_eq!(
421            parser_error.to_string(),
422            "Parser error at position 20: Unexpected token"
423        );
424    }
425
426    #[test]
427    fn test_parse_empty() {
428        let input = "";
429        let options = ConfOptions::default();
430        let result = parse(input, options);
431        assert!(result.is_ok());
432        assert_eq!(result.unwrap().directives.len(), 0);
433    }
434
435    #[test]
436    fn test_parse_simple_directive() {
437        let input = "server localhost;";
438        let options = ConfOptions::default();
439        let result = parse(input, options);
440        assert!(result.is_ok());
441        let conf_unit = result.unwrap();
442        assert_eq!(conf_unit.directives.len(), 1);
443        assert_eq!(conf_unit.directives[0].name.value, "server");
444        assert_eq!(conf_unit.directives[0].arguments.len(), 1);
445        assert_eq!(conf_unit.directives[0].arguments[0].value, "localhost");
446    }
447
448    #[test]
449    fn test_parse_block_directive() {
450        let input = "server {\n  listen 80;\n}";
451        let options = ConfOptions::default();
452        let result = parse(input, options);
453        assert!(result.is_ok());
454        let conf_unit = result.unwrap();
455        assert_eq!(conf_unit.directives.len(), 1);
456        assert_eq!(conf_unit.directives[0].name.value, "server");
457        assert_eq!(conf_unit.directives[0].children.len(), 1);
458        assert_eq!(conf_unit.directives[0].children[0].name.value, "listen");
459        assert_eq!(conf_unit.directives[0].children[0].arguments.len(), 1);
460        assert_eq!(conf_unit.directives[0].children[0].arguments[0].value, "80");
461    }
462
463    #[test]
464    fn test_parse_with_comments() {
465        let input = "# This is a comment\nserver {\n  # Another comment\n  listen 80;\n}";
466        let options = ConfOptions::default();
467        let result = parse(input, options);
468        assert!(result.is_ok());
469        let conf_unit = result.unwrap();
470        assert_eq!(conf_unit.directives.len(), 1);
471        assert_eq!(conf_unit.comments.len(), 1);
472        assert_eq!(conf_unit.comments[0].content, "# This is a comment");
473    }
474
475    #[test]
476    fn test_parse_quoted_arguments() {
477        let input = r#"server "example.com";"#;
478        let options = ConfOptions::default();
479        let result = parse(input, options);
480        assert!(result.is_ok());
481        let conf_unit = result.unwrap();
482        assert_eq!(conf_unit.directives.len(), 1);
483        assert_eq!(conf_unit.directives[0].arguments.len(), 1);
484        assert_eq!(
485            conf_unit.directives[0].arguments[0].value,
486            "\"example.com\""
487        );
488        assert!(conf_unit.directives[0].arguments[0].is_quoted);
489    }
490
491    #[test]
492    fn test_parse_triple_quoted_arguments() {
493        let input = r#"server """
494        This is a multi-line
495        string argument
496        """;"#;
497        let options = ConfOptions::default();
498        let result = parse(input, options);
499        assert!(result.is_ok());
500        let conf_unit = result.unwrap();
501        assert_eq!(conf_unit.directives.len(), 1);
502        assert_eq!(conf_unit.directives[0].arguments.len(), 1);
503        assert!(conf_unit.directives[0].arguments[0]
504            .value
505            .contains("multi-line"));
506        assert!(conf_unit.directives[0].arguments[0].is_triple_quoted);
507    }
508
509    #[test]
510    fn test_parse_line_continuation() {
511        let input = "server \\\nexample.com;";
512        let options = ConfOptions {
513            allow_line_continuations: true,
514            ..ConfOptions::default()
515        };
516        let result = parse(input, options);
517        assert!(result.is_ok());
518        let conf_unit = result.unwrap();
519        assert_eq!(conf_unit.directives.len(), 1);
520        assert_eq!(conf_unit.directives[0].arguments.len(), 1);
521        assert_eq!(conf_unit.directives[0].arguments[0].value, "example.com");
522    }
523}