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}