use std::error::Error;
use std::fmt;
use std::ops::Range;
pub mod lexer;
pub mod mapper;
pub mod parser;
#[cfg(feature = "derive")]
pub use confetti_derive::ConfMap;
#[doc(hidden)]
pub mod __private {
pub fn is_option_type(type_name: &str) -> bool {
type_name.starts_with("core::option::Option<")
|| type_name.starts_with("std::option::Option<")
}
pub fn extract_option_type(type_name: &str) -> Option<&str> {
if is_option_type(type_name) {
let start = type_name.find('<')? + 1;
let end = type_name.rfind('>')?;
Some(&type_name[start..end])
} else {
None
}
}
pub fn strip_quotes(value: &str) -> String {
let mut result = value.to_string();
if result.starts_with('"') && result.ends_with('"') {
result = result[1..result.len() - 1].to_string();
}
result
}
}
#[derive(Debug, Clone)]
pub struct ConfArgument {
pub value: String,
pub span: Range<usize>,
pub is_quoted: bool,
pub is_triple_quoted: bool,
pub is_expression: bool,
}
#[derive(Debug, Clone)]
pub struct ConfDirective {
pub name: ConfArgument,
pub arguments: Vec<ConfArgument>,
pub children: Vec<ConfDirective>,
}
#[derive(Debug, Clone)]
pub struct ConfUnit {
pub directives: Vec<ConfDirective>,
pub comments: Vec<ConfComment>,
}
#[derive(Debug, Clone)]
pub struct ConfComment {
pub content: String,
pub span: Range<usize>,
pub is_multi_line: bool,
}
#[derive(Debug)]
pub enum ConfError {
LexerError {
position: usize,
message: String,
},
ParserError {
position: usize,
message: String,
},
}
impl Error for ConfError {}
impl fmt::Display for ConfError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfError::LexerError { position, message } => {
write!(f, "Lexer error at position {}: {}", position, message)
}
ConfError::ParserError { position, message } => {
write!(f, "Parser error at position {}: {}", position, message)
}
}
}
}
#[derive(Debug, Clone)]
pub struct ConfOptions {
pub allow_c_style_comments: bool,
pub allow_expression_arguments: bool,
pub max_depth: usize,
pub allow_bidi: bool,
pub require_semicolons: bool,
pub allow_triple_quotes: bool,
pub allow_line_continuations: bool,
}
impl Default for ConfOptions {
fn default() -> Self {
Self {
allow_c_style_comments: false,
allow_expression_arguments: false,
max_depth: 100,
allow_bidi: false,
require_semicolons: false,
allow_triple_quotes: true,
allow_line_continuations: true,
}
}
}
pub fn parse(input: &str, options: ConfOptions) -> Result<ConfUnit, ConfError> {
let mut parser = parser::Parser::new(input, options)?;
parser.parse()
}
pub use crate::mapper::{FromConf, MapperError, MapperOptions, ToConf, ValueConverter};
pub fn from_file<T: FromConf, P: AsRef<std::path::Path>>(
path: P,
) -> Result<T, mapper::MapperError> {
T::from_file(path)
}
pub fn from_str<T: FromConf>(s: &str) -> Result<T, mapper::MapperError> {
T::from_str(s)
}
pub fn to_string<T: ToConf>(value: &T) -> Result<String, mapper::MapperError> {
value.to_string()
}
pub fn to_file<T: ToConf, P: AsRef<std::path::Path>>(
value: &T,
path: P,
) -> Result<(), mapper::MapperError> {
value.to_file(path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_conf_error_display() {
let lexer_error = ConfError::LexerError {
position: 10,
message: "Invalid character".to_string(),
};
assert_eq!(
lexer_error.to_string(),
"Lexer error at position 10: Invalid character"
);
let parser_error = ConfError::ParserError {
position: 20,
message: "Unexpected token".to_string(),
};
assert_eq!(
parser_error.to_string(),
"Parser error at position 20: Unexpected token"
);
}
#[test]
fn test_parse_empty() {
let input = "";
let options = ConfOptions::default();
let result = parse(input, options);
assert!(result.is_ok());
assert_eq!(result.unwrap().directives.len(), 0);
}
#[test]
fn test_parse_simple_directive() {
let input = "server localhost;";
let options = ConfOptions::default();
let result = parse(input, options);
assert!(result.is_ok());
let conf_unit = result.unwrap();
assert_eq!(conf_unit.directives.len(), 1);
assert_eq!(conf_unit.directives[0].name.value, "server");
assert_eq!(conf_unit.directives[0].arguments.len(), 1);
assert_eq!(conf_unit.directives[0].arguments[0].value, "localhost");
}
#[test]
fn test_parse_block_directive() {
let input = "server {\n listen 80;\n}";
let options = ConfOptions::default();
let result = parse(input, options);
assert!(result.is_ok());
let conf_unit = result.unwrap();
assert_eq!(conf_unit.directives.len(), 1);
assert_eq!(conf_unit.directives[0].name.value, "server");
assert_eq!(conf_unit.directives[0].children.len(), 1);
assert_eq!(conf_unit.directives[0].children[0].name.value, "listen");
assert_eq!(conf_unit.directives[0].children[0].arguments.len(), 1);
assert_eq!(conf_unit.directives[0].children[0].arguments[0].value, "80");
}
#[test]
fn test_parse_with_comments() {
let input = "# This is a comment\nserver {\n # Another comment\n listen 80;\n}";
let options = ConfOptions::default();
let result = parse(input, options);
assert!(result.is_ok());
let conf_unit = result.unwrap();
assert_eq!(conf_unit.directives.len(), 1);
assert_eq!(conf_unit.comments.len(), 1);
assert_eq!(conf_unit.comments[0].content, "# This is a comment");
}
#[test]
fn test_parse_quoted_arguments() {
let input = r#"server "example.com";"#;
let options = ConfOptions::default();
let result = parse(input, options);
assert!(result.is_ok());
let conf_unit = result.unwrap();
assert_eq!(conf_unit.directives.len(), 1);
assert_eq!(conf_unit.directives[0].arguments.len(), 1);
assert_eq!(
conf_unit.directives[0].arguments[0].value,
"\"example.com\""
);
assert!(conf_unit.directives[0].arguments[0].is_quoted);
}
#[test]
fn test_parse_triple_quoted_arguments() {
let input = r#"server """
This is a multi-line
string argument
""";"#;
let options = ConfOptions::default();
let result = parse(input, options);
assert!(result.is_ok());
let conf_unit = result.unwrap();
assert_eq!(conf_unit.directives.len(), 1);
assert_eq!(conf_unit.directives[0].arguments.len(), 1);
assert!(conf_unit.directives[0].arguments[0]
.value
.contains("multi-line"));
assert!(conf_unit.directives[0].arguments[0].is_triple_quoted);
}
#[test]
fn test_parse_line_continuation() {
let input = "server \\\nexample.com;";
let options = ConfOptions {
allow_line_continuations: true,
..ConfOptions::default()
};
let result = parse(input, options);
assert!(result.is_ok());
let conf_unit = result.unwrap();
assert_eq!(conf_unit.directives.len(), 1);
assert_eq!(conf_unit.directives[0].arguments.len(), 1);
assert_eq!(conf_unit.directives[0].arguments[0].value, "example.com");
}
}