1use std::error::Error;
2use std::fmt;
3use std::fs;
4use std::io;
5use std::path::Path;
6
7use crate::{parse, ConfDirective, ConfOptions};
8
9#[derive(Debug)]
11pub enum MapperError {
12 ParseError(String),
14 SerializeError(String),
16 IoError(io::Error),
18 ConversionError(String),
20 MissingField(String),
22}
23
24impl Error for MapperError {}
25
26impl fmt::Display for MapperError {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 MapperError::ParseError(msg) => write!(f, "Parse error: {}", msg),
30 MapperError::SerializeError(msg) => write!(f, "Serialization error: {}", msg),
31 MapperError::IoError(err) => write!(f, "I/O error: {}", err),
32 MapperError::ConversionError(msg) => write!(f, "Conversion error: {}", msg),
33 MapperError::MissingField(name) => write!(f, "Missing required field: {}", name),
34 }
35 }
36}
37
38impl From<io::Error> for MapperError {
39 fn from(error: io::Error) -> Self {
40 MapperError::IoError(error)
41 }
42}
43
44impl From<crate::ConfError> for MapperError {
45 fn from(error: crate::ConfError) -> Self {
46 MapperError::ParseError(error.to_string())
47 }
48}
49
50pub trait FromConf: Sized {
52 fn from_directive(directive: &ConfDirective) -> Result<Self, MapperError>;
54
55 fn from_str(s: &str) -> Result<Self, MapperError> {
57 let options = MapperOptions::default().parser_options;
58 let conf_unit = parse(s, options)?;
59
60 if conf_unit.directives.is_empty() {
61 return Err(MapperError::ParseError("No directives found".into()));
62 }
63
64 Self::from_directive(&conf_unit.directives[0])
65 }
66
67 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, MapperError> {
69 let content = fs::read_to_string(path)?;
70 Self::from_str(&content)
71 }
72}
73
74pub trait ToConf {
76 fn to_directive(&self) -> Result<ConfDirective, MapperError>;
78
79 fn to_string(&self) -> Result<String, MapperError> {
81 let directive = self.to_directive()?;
82
83 let mut result = String::new();
85 serialize_directive(&directive, &mut result, 0)?;
86
87 Ok(result)
88 }
89
90 fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), MapperError> {
92 let content = self.to_string()?;
93 fs::write(path, content)?;
94 Ok(())
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct MapperOptions {
101 pub parser_options: ConfOptions,
103 pub use_kebab_case: bool,
105 pub indent: String,
107}
108
109impl Default for MapperOptions {
110 fn default() -> Self {
111 Self {
112 parser_options: ConfOptions::default(),
113 use_kebab_case: false,
114 indent: " ".to_string(),
115 }
116 }
117}
118
119#[allow(dead_code)]
121fn to_kebab_case(s: &str) -> String {
122 let mut result = String::new();
123 let mut prev_is_lowercase = false;
124
125 for c in s.chars() {
126 if c.is_uppercase() {
127 if prev_is_lowercase {
128 result.push('-');
129 }
130 result.push(c.to_lowercase().next().unwrap());
131 prev_is_lowercase = false;
132 } else {
133 result.push(c);
134 prev_is_lowercase = true;
135 }
136 }
137
138 result
139}
140
141#[allow(dead_code)]
143fn from_kebab_case(s: &str) -> String {
144 let mut result = String::new();
145 let mut capitalize_next = false;
146
147 for c in s.chars() {
148 if c == '-' {
149 capitalize_next = true;
150 } else if capitalize_next {
151 result.push(c.to_uppercase().next().unwrap());
152 capitalize_next = false;
153 } else {
154 result.push(c);
155 }
156 }
157
158 result
159}
160
161fn serialize_directive(
163 directive: &ConfDirective,
164 output: &mut String,
165 depth: usize,
166) -> Result<(), MapperError> {
167 let indent = " ".repeat(depth);
169
170 output.push_str(&indent);
172 output.push_str(&directive.name.value);
173
174 for arg in &directive.arguments {
176 output.push(' ');
177 if arg.is_quoted {
178 output.push('"');
179 let mut value = if arg.value.starts_with('"') && arg.value.ends_with('"') {
181 arg.value[1..arg.value.len() - 1].to_string()
182 } else {
183 arg.value.clone()
184 };
185
186 value = value.trim_end_matches(',').to_string();
188
189 output.push_str(&value);
190 output.push('"');
191 } else {
192 output.push_str(&arg.value);
193 }
194 }
195
196 if directive.children.is_empty() {
197 output.push_str(";\n");
198 } else {
199 output.push_str(" {\n");
200
201 for child in &directive.children {
203 serialize_directive(child, output, depth + 1)?;
204 }
205
206 output.push_str(&indent);
207 output.push_str("}\n");
208 }
209
210 Ok(())
211}
212
213pub trait ValueConverter: Sized {
215 fn from_conf_value(value: &str) -> Result<Self, MapperError>;
217
218 fn to_conf_value(&self) -> Result<String, MapperError>;
220
221 fn requires_quotes(&self) -> bool {
223 true }
225}
226
227impl ValueConverter for String {
230 fn from_conf_value(value: &str) -> Result<Self, MapperError> {
231 Ok(value.to_string())
232 }
233
234 fn to_conf_value(&self) -> Result<String, MapperError> {
235 let value = if self.starts_with('"') && self.ends_with('"') {
237 &self[1..self.len() - 1]
238 } else {
239 &self[..]
240 };
241
242 let value = value.trim_end_matches(',');
244
245 Ok(value.to_string())
246 }
247
248 fn requires_quotes(&self) -> bool {
249 true
250 }
251}
252
253impl ValueConverter for bool {
254 fn from_conf_value(value: &str) -> Result<Self, MapperError> {
255 match value.to_lowercase().as_str() {
256 "true" | "yes" | "on" | "1" => Ok(true),
257 "false" | "no" | "off" | "0" => Ok(false),
258 _ => Err(MapperError::ConversionError(format!(
259 "Cannot convert '{}' to bool",
260 value
261 ))),
262 }
263 }
264
265 fn to_conf_value(&self) -> Result<String, MapperError> {
266 Ok(self.to_string())
267 }
268
269 fn requires_quotes(&self) -> bool {
270 false
271 }
272}
273
274impl ValueConverter for i32 {
275 fn from_conf_value(value: &str) -> Result<Self, MapperError> {
276 value.parse::<i32>().map_err(|e| {
277 MapperError::ConversionError(format!("Cannot convert '{}' to i32: {}", value, e))
278 })
279 }
280
281 fn to_conf_value(&self) -> Result<String, MapperError> {
282 Ok(self.to_string())
283 }
284
285 fn requires_quotes(&self) -> bool {
286 false
287 }
288}
289
290impl ValueConverter for f64 {
291 fn from_conf_value(value: &str) -> Result<Self, MapperError> {
292 value.parse::<f64>().map_err(|e| {
293 MapperError::ConversionError(format!("Cannot convert '{}' to f64: {}", value, e))
294 })
295 }
296
297 fn to_conf_value(&self) -> Result<String, MapperError> {
298 Ok(self.to_string())
299 }
300
301 fn requires_quotes(&self) -> bool {
302 false
303 }
304}
305
306impl<T: ValueConverter> ValueConverter for Option<T> {
307 fn from_conf_value(value: &str) -> Result<Self, MapperError> {
308 if value.trim().is_empty() {
309 Ok(None)
310 } else {
311 Ok(Some(T::from_conf_value(value)?))
312 }
313 }
314
315 fn to_conf_value(&self) -> Result<String, MapperError> {
316 match self {
317 Some(val) => val.to_conf_value(),
318 None => Ok("".to_string()),
319 }
320 }
321
322 fn requires_quotes(&self) -> bool {
323 match self {
324 Some(val) => val.requires_quotes(),
325 None => false,
326 }
327 }
328}
329
330impl<T: ValueConverter> ValueConverter for Vec<T> {
331 fn from_conf_value(value: &str) -> Result<Self, MapperError> {
332 let values = value
333 .split(',')
334 .map(|s| s.trim())
335 .filter(|s| !s.is_empty())
336 .map(|s| T::from_conf_value(s))
337 .collect::<Result<Vec<T>, _>>()?;
338
339 Ok(values)
340 }
341
342 fn to_conf_value(&self) -> Result<String, MapperError> {
343 let values: Result<Vec<String>, _> = self.iter().map(|val| val.to_conf_value()).collect();
344
345 Ok(values?.join(", "))
346 }
347
348 fn requires_quotes(&self) -> bool {
349 true
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use crate::{ConfArgument, ConfDirective};
358
359 #[test]
360 fn test_serialize_string_without_comma() {
361 let directive = ConfDirective {
363 name: ConfArgument {
364 value: "TestConfig".to_string(),
365 span: 0..0,
366 is_quoted: false,
367 is_triple_quoted: false,
368 is_expression: false,
369 },
370 arguments: vec![],
371 children: vec![ConfDirective {
372 name: ConfArgument {
373 value: "host".to_string(),
374 span: 0..0,
375 is_quoted: false,
376 is_triple_quoted: false,
377 is_expression: false,
378 },
379 arguments: vec![ConfArgument {
380 value: "127.0.0.1,".to_string(),
381 span: 0..0,
382 is_quoted: true,
383 is_triple_quoted: false,
384 is_expression: false,
385 }],
386 children: vec![],
387 }],
388 };
389
390 let mut output = String::new();
392 serialize_directive(&directive, &mut output, 0).unwrap();
393
394 assert!(output.contains("\"127.0.0.1\""));
396 assert!(!output.contains("\"127.0.0.1,\""));
397 }
398
399 #[test]
400 fn test_serialize_numeric_without_quotes() {
401 let directive = ConfDirective {
403 name: ConfArgument {
404 value: "TestConfig".to_string(),
405 span: 0..0,
406 is_quoted: false,
407 is_triple_quoted: false,
408 is_expression: false,
409 },
410 arguments: vec![],
411 children: vec![ConfDirective {
412 name: ConfArgument {
413 value: "port".to_string(),
414 span: 0..0,
415 is_quoted: false,
416 is_triple_quoted: false,
417 is_expression: false,
418 },
419 arguments: vec![ConfArgument {
420 value: "3000".to_string(),
421 span: 0..0,
422 is_quoted: false,
423 is_triple_quoted: false,
424 is_expression: false,
425 }],
426 children: vec![],
427 }],
428 };
429
430 let mut output = String::new();
432 serialize_directive(&directive, &mut output, 0).unwrap();
433
434 assert!(output.contains("port 3000;"));
436 assert!(!output.contains("port \"3000\";"));
437 }
438
439 #[test]
440 fn test_server_config_serialization() {
441 let directive = ConfDirective {
443 name: ConfArgument {
444 value: "ServerConfig".to_string(),
445 span: 0..0,
446 is_quoted: false,
447 is_triple_quoted: false,
448 is_expression: false,
449 },
450 arguments: vec![],
451 children: vec![
452 ConfDirective {
453 name: ConfArgument {
454 value: "host".to_string(),
455 span: 0..0,
456 is_quoted: false,
457 is_triple_quoted: false,
458 is_expression: false,
459 },
460 arguments: vec![ConfArgument {
461 value: "127.0.0.1,".to_string(),
462 span: 0..0,
463 is_quoted: true,
464 is_triple_quoted: false,
465 is_expression: false,
466 }],
467 children: vec![],
468 },
469 ConfDirective {
470 name: ConfArgument {
471 value: "port".to_string(),
472 span: 0..0,
473 is_quoted: false,
474 is_triple_quoted: false,
475 is_expression: false,
476 },
477 arguments: vec![ConfArgument {
478 value: "3000".to_string(),
479 span: 0..0,
480 is_quoted: false,
481 is_triple_quoted: false,
482 is_expression: false,
483 }],
484 children: vec![],
485 },
486 ],
487 };
488
489 let mut output = String::new();
491 serialize_directive(&directive, &mut output, 0).unwrap();
492
493 let expected = "ServerConfig {\n host \"127.0.0.1\";\n port 3000;\n}\n";
495
496 assert_eq!(output, expected);
497 }
498
499 #[test]
500 fn test_to_conf_value_string_with_quotes() {
501 let value = "\"test value\"".to_string();
503 let result = value.to_conf_value().unwrap();
504 assert_eq!(result, "test value");
505 }
506
507 #[test]
508 fn test_to_conf_value_string_with_comma() {
509 let value = "test value,".to_string();
511 let result = value.to_conf_value().unwrap();
512 assert_eq!(result, "test value");
513 }
514
515 #[test]
516 fn test_requires_quotes() {
517 let string_value = String::from("test");
519 assert!(string_value.requires_quotes());
520
521 let int_value = 3000;
523 assert!(!int_value.requires_quotes());
524
525 let float_value = std::f64::consts::PI;
526 assert!(!float_value.requires_quotes());
527
528 let bool_value = true;
530 assert!(!bool_value.requires_quotes());
531 }
532}