use std::error::Error;
use std::fmt;
use std::fs;
use std::io;
use std::path::Path;
use crate::{parse, ConfDirective, ConfOptions};
#[derive(Debug)]
pub enum MapperError {
ParseError(String),
SerializeError(String),
IoError(io::Error),
ConversionError(String),
MissingField(String),
}
impl Error for MapperError {}
impl fmt::Display for MapperError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MapperError::ParseError(msg) => write!(f, "Parse error: {}", msg),
MapperError::SerializeError(msg) => write!(f, "Serialization error: {}", msg),
MapperError::IoError(err) => write!(f, "I/O error: {}", err),
MapperError::ConversionError(msg) => write!(f, "Conversion error: {}", msg),
MapperError::MissingField(name) => write!(f, "Missing required field: {}", name),
}
}
}
impl From<io::Error> for MapperError {
fn from(error: io::Error) -> Self {
MapperError::IoError(error)
}
}
impl From<crate::ConfError> for MapperError {
fn from(error: crate::ConfError) -> Self {
MapperError::ParseError(error.to_string())
}
}
pub trait FromConf: Sized {
fn from_directive(directive: &ConfDirective) -> Result<Self, MapperError>;
fn from_str(s: &str) -> Result<Self, MapperError> {
let options = MapperOptions::default().parser_options;
let conf_unit = parse(s, options)?;
if conf_unit.directives.is_empty() {
return Err(MapperError::ParseError("No directives found".into()));
}
Self::from_directive(&conf_unit.directives[0])
}
fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, MapperError> {
let content = fs::read_to_string(path)?;
Self::from_str(&content)
}
}
pub trait ToConf {
fn to_directive(&self) -> Result<ConfDirective, MapperError>;
fn to_string(&self) -> Result<String, MapperError> {
let directive = self.to_directive()?;
let mut result = String::new();
serialize_directive(&directive, &mut result, 0)?;
Ok(result)
}
fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), MapperError> {
let content = self.to_string()?;
fs::write(path, content)?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct MapperOptions {
pub parser_options: ConfOptions,
pub use_kebab_case: bool,
pub indent: String,
}
impl Default for MapperOptions {
fn default() -> Self {
Self {
parser_options: ConfOptions::default(),
use_kebab_case: false,
indent: " ".to_string(),
}
}
}
#[allow(dead_code)]
fn to_kebab_case(s: &str) -> String {
let mut result = String::new();
let mut prev_is_lowercase = false;
for c in s.chars() {
if c.is_uppercase() {
if prev_is_lowercase {
result.push('-');
}
result.push(c.to_lowercase().next().unwrap());
prev_is_lowercase = false;
} else {
result.push(c);
prev_is_lowercase = true;
}
}
result
}
#[allow(dead_code)]
fn from_kebab_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = false;
for c in s.chars() {
if c == '-' {
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_uppercase().next().unwrap());
capitalize_next = false;
} else {
result.push(c);
}
}
result
}
fn serialize_directive(
directive: &ConfDirective,
output: &mut String,
depth: usize,
) -> Result<(), MapperError> {
let indent = " ".repeat(depth);
output.push_str(&indent);
output.push_str(&directive.name.value);
for arg in &directive.arguments {
output.push(' ');
if arg.is_quoted {
output.push('"');
let mut value = if arg.value.starts_with('"') && arg.value.ends_with('"') {
arg.value[1..arg.value.len() - 1].to_string()
} else {
arg.value.clone()
};
value = value.trim_end_matches(',').to_string();
output.push_str(&value);
output.push('"');
} else {
output.push_str(&arg.value);
}
}
if directive.children.is_empty() {
output.push_str(";\n");
} else {
output.push_str(" {\n");
for child in &directive.children {
serialize_directive(child, output, depth + 1)?;
}
output.push_str(&indent);
output.push_str("}\n");
}
Ok(())
}
pub trait ValueConverter: Sized {
fn from_conf_value(value: &str) -> Result<Self, MapperError>;
fn to_conf_value(&self) -> Result<String, MapperError>;
fn requires_quotes(&self) -> bool {
true }
}
impl ValueConverter for String {
fn from_conf_value(value: &str) -> Result<Self, MapperError> {
Ok(value.to_string())
}
fn to_conf_value(&self) -> Result<String, MapperError> {
let value = if self.starts_with('"') && self.ends_with('"') {
&self[1..self.len() - 1]
} else {
&self[..]
};
let value = value.trim_end_matches(',');
Ok(value.to_string())
}
fn requires_quotes(&self) -> bool {
true
}
}
impl ValueConverter for bool {
fn from_conf_value(value: &str) -> Result<Self, MapperError> {
match value.to_lowercase().as_str() {
"true" | "yes" | "on" | "1" => Ok(true),
"false" | "no" | "off" | "0" => Ok(false),
_ => Err(MapperError::ConversionError(format!(
"Cannot convert '{}' to bool",
value
))),
}
}
fn to_conf_value(&self) -> Result<String, MapperError> {
Ok(self.to_string())
}
fn requires_quotes(&self) -> bool {
false
}
}
impl ValueConverter for i32 {
fn from_conf_value(value: &str) -> Result<Self, MapperError> {
value.parse::<i32>().map_err(|e| {
MapperError::ConversionError(format!("Cannot convert '{}' to i32: {}", value, e))
})
}
fn to_conf_value(&self) -> Result<String, MapperError> {
Ok(self.to_string())
}
fn requires_quotes(&self) -> bool {
false
}
}
impl ValueConverter for f64 {
fn from_conf_value(value: &str) -> Result<Self, MapperError> {
value.parse::<f64>().map_err(|e| {
MapperError::ConversionError(format!("Cannot convert '{}' to f64: {}", value, e))
})
}
fn to_conf_value(&self) -> Result<String, MapperError> {
Ok(self.to_string())
}
fn requires_quotes(&self) -> bool {
false
}
}
impl<T: ValueConverter> ValueConverter for Option<T> {
fn from_conf_value(value: &str) -> Result<Self, MapperError> {
if value.trim().is_empty() {
Ok(None)
} else {
Ok(Some(T::from_conf_value(value)?))
}
}
fn to_conf_value(&self) -> Result<String, MapperError> {
match self {
Some(val) => val.to_conf_value(),
None => Ok("".to_string()),
}
}
fn requires_quotes(&self) -> bool {
match self {
Some(val) => val.requires_quotes(),
None => false,
}
}
}
impl<T: ValueConverter> ValueConverter for Vec<T> {
fn from_conf_value(value: &str) -> Result<Self, MapperError> {
let values = value
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| T::from_conf_value(s))
.collect::<Result<Vec<T>, _>>()?;
Ok(values)
}
fn to_conf_value(&self) -> Result<String, MapperError> {
let values: Result<Vec<String>, _> = self.iter().map(|val| val.to_conf_value()).collect();
Ok(values?.join(", "))
}
fn requires_quotes(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ConfArgument, ConfDirective};
#[test]
fn test_serialize_string_without_comma() {
let directive = ConfDirective {
name: ConfArgument {
value: "TestConfig".to_string(),
span: 0..0,
is_quoted: false,
is_triple_quoted: false,
is_expression: false,
},
arguments: vec![],
children: vec![ConfDirective {
name: ConfArgument {
value: "host".to_string(),
span: 0..0,
is_quoted: false,
is_triple_quoted: false,
is_expression: false,
},
arguments: vec![ConfArgument {
value: "127.0.0.1,".to_string(),
span: 0..0,
is_quoted: true,
is_triple_quoted: false,
is_expression: false,
}],
children: vec![],
}],
};
let mut output = String::new();
serialize_directive(&directive, &mut output, 0).unwrap();
assert!(output.contains("\"127.0.0.1\""));
assert!(!output.contains("\"127.0.0.1,\""));
}
#[test]
fn test_serialize_numeric_without_quotes() {
let directive = ConfDirective {
name: ConfArgument {
value: "TestConfig".to_string(),
span: 0..0,
is_quoted: false,
is_triple_quoted: false,
is_expression: false,
},
arguments: vec![],
children: vec![ConfDirective {
name: ConfArgument {
value: "port".to_string(),
span: 0..0,
is_quoted: false,
is_triple_quoted: false,
is_expression: false,
},
arguments: vec![ConfArgument {
value: "3000".to_string(),
span: 0..0,
is_quoted: false,
is_triple_quoted: false,
is_expression: false,
}],
children: vec![],
}],
};
let mut output = String::new();
serialize_directive(&directive, &mut output, 0).unwrap();
assert!(output.contains("port 3000;"));
assert!(!output.contains("port \"3000\";"));
}
#[test]
fn test_server_config_serialization() {
let directive = ConfDirective {
name: ConfArgument {
value: "ServerConfig".to_string(),
span: 0..0,
is_quoted: false,
is_triple_quoted: false,
is_expression: false,
},
arguments: vec![],
children: vec![
ConfDirective {
name: ConfArgument {
value: "host".to_string(),
span: 0..0,
is_quoted: false,
is_triple_quoted: false,
is_expression: false,
},
arguments: vec![ConfArgument {
value: "127.0.0.1,".to_string(),
span: 0..0,
is_quoted: true,
is_triple_quoted: false,
is_expression: false,
}],
children: vec![],
},
ConfDirective {
name: ConfArgument {
value: "port".to_string(),
span: 0..0,
is_quoted: false,
is_triple_quoted: false,
is_expression: false,
},
arguments: vec![ConfArgument {
value: "3000".to_string(),
span: 0..0,
is_quoted: false,
is_triple_quoted: false,
is_expression: false,
}],
children: vec![],
},
],
};
let mut output = String::new();
serialize_directive(&directive, &mut output, 0).unwrap();
let expected = "ServerConfig {\n host \"127.0.0.1\";\n port 3000;\n}\n";
assert_eq!(output, expected);
}
#[test]
fn test_to_conf_value_string_with_quotes() {
let value = "\"test value\"".to_string();
let result = value.to_conf_value().unwrap();
assert_eq!(result, "test value");
}
#[test]
fn test_to_conf_value_string_with_comma() {
let value = "test value,".to_string();
let result = value.to_conf_value().unwrap();
assert_eq!(result, "test value");
}
#[test]
fn test_requires_quotes() {
let string_value = String::from("test");
assert!(string_value.requires_quotes());
let int_value = 3000;
assert!(!int_value.requires_quotes());
let float_value = std::f64::consts::PI;
assert!(!float_value.requires_quotes());
let bool_value = true;
assert!(!bool_value.requires_quotes());
}
}