config_lib/parsers/
properties_parser.rs1use crate::error::{Error, Result};
2use crate::value::Value;
3use std::collections::BTreeMap;
4
5pub fn parse(source: &str) -> Result<Value> {
7 let mut parser = PropertiesParser::new(source.to_string());
8 parser.parse()
9}
10
11pub struct PropertiesParser {
27 input: String,
28 position: usize,
29 line: usize,
30 column: usize,
31}
32
33impl PropertiesParser {
34 pub fn new(input: String) -> Self {
36 Self {
37 input,
38 position: 0,
39 line: 1,
40 column: 1,
41 }
42 }
43
44 pub fn parse(&mut self) -> Result<Value> {
46 let mut properties = BTreeMap::new();
47
48 while !self.at_end() {
49 self.skip_whitespace_and_comments();
50
51 if self.at_end() {
52 break;
53 }
54
55 let (key, value) = self.parse_property()?;
56 properties.insert(key, value);
57 }
58
59 Ok(Value::table(properties))
60 }
61
62 fn parse_property(&mut self) -> Result<(String, Value)> {
63 let key = self.parse_key()?;
64 self.skip_whitespace();
65
66 if self.current_char() != '=' && self.current_char() != ':' {
68 return Err(Error::Parse {
69 message: format!("Expected '=' or ':', found '{}'", self.current_char()),
70 line: self.line,
71 column: self.column,
72 file: None,
73 });
74 }
75
76 self.advance(); self.skip_whitespace();
78
79 let value = self.parse_value()?;
80
81 Ok((key, value))
82 }
83
84 fn parse_key(&mut self) -> Result<String> {
85 let mut key = String::new();
86
87 while !self.at_end() {
88 let ch = self.current_char();
89
90 match ch {
91 '=' | ':' => break,
92 '\\' => {
93 self.advance();
94 if self.at_end() {
95 return Err(Error::Parse {
96 message: "Unexpected end of input in key".to_string(),
97 line: self.line,
98 column: self.column,
99 file: None,
100 });
101 }
102
103 let escaped = self.parse_escape()?;
104 key.push_str(&escaped);
105 }
106 '\n' | '\r' => {
107 return Err(Error::Parse {
108 message: "Unexpected newline in key".to_string(),
109 line: self.line,
110 column: self.column,
111 file: None,
112 });
113 }
114 _ => {
115 key.push(ch);
116 self.advance();
117 }
118 }
119 }
120
121 if key.trim().is_empty() {
122 return Err(Error::Parse {
123 message: "Empty key name".to_string(),
124 line: self.line,
125 column: self.column,
126 file: None,
127 });
128 }
129
130 Ok(key.trim().to_string())
131 }
132
133 fn parse_value(&mut self) -> Result<Value> {
134 let mut value = String::new();
135
136 while !self.at_end() {
137 let ch = self.current_char();
138
139 match ch {
140 '\\' => {
141 self.advance();
142 if self.at_end() {
143 break;
144 }
145
146 if self.current_char() == '\n' || self.current_char() == '\r' {
148 self.skip_newline();
149 self.skip_whitespace();
150 continue;
151 }
152
153 let escaped = self.parse_escape()?;
154 value.push_str(&escaped);
155 }
156 '\n' | '\r' => break,
157 _ => {
158 value.push(ch);
159 self.advance();
160 }
161 }
162 }
163
164 let trimmed = value.trim();
165 Ok(self.infer_value_type(trimmed))
166 }
167
168 fn parse_escape(&mut self) -> Result<String> {
169 let ch = self.current_char();
170 self.advance();
171
172 match ch {
173 'n' => Ok("\n".to_string()),
174 't' => Ok("\t".to_string()),
175 'r' => Ok("\r".to_string()),
176 '\\' => Ok("\\".to_string()),
177 '=' => Ok("=".to_string()),
178 ':' => Ok(":".to_string()),
179 ' ' => Ok(" ".to_string()),
180 'u' => self.parse_unicode_escape(),
181 _ => Ok(ch.to_string()),
182 }
183 }
184
185 fn parse_unicode_escape(&mut self) -> Result<String> {
186 let mut hex_digits = String::new();
187
188 for _ in 0..4 {
189 if self.at_end() {
190 return Err(Error::Parse {
191 message: "Incomplete unicode escape".to_string(),
192 line: self.line,
193 column: self.column,
194 file: None,
195 });
196 }
197
198 let ch = self.current_char();
199 if ch.is_ascii_hexdigit() {
200 hex_digits.push(ch);
201 self.advance();
202 } else {
203 return Err(Error::Parse {
204 message: format!("Invalid hex digit in unicode escape: '{ch}'"),
205 line: self.line,
206 column: self.column,
207 file: None,
208 });
209 }
210 }
211
212 let code_point = match u32::from_str_radix(&hex_digits, 16) {
213 Ok(cp) => cp,
214 Err(_) => {
215 return Err(Error::Parse {
216 message: format!("Invalid hex digits in unicode escape: {hex_digits}"),
217 line: self.line,
218 column: self.column,
219 file: None,
220 });
221 }
222 };
223 if let Some(unicode_char) = char::from_u32(code_point) {
224 Ok(unicode_char.to_string())
225 } else {
226 Err(Error::Parse {
227 message: format!("Invalid unicode code point: {code_point}"),
228 line: self.line,
229 column: self.column,
230 file: None,
231 })
232 }
233 }
234
235 fn infer_value_type(&self, value: &str) -> Value {
236 if value.is_empty() {
237 return Value::string(String::new());
238 }
239
240 match value.to_lowercase().as_str() {
242 "true" => return Value::bool(true),
243 "false" => return Value::bool(false),
244 _ => {}
245 }
246
247 if let Ok(int_val) = value.parse::<i64>() {
249 return Value::integer(int_val);
250 }
251
252 if let Ok(float_val) = value.parse::<f64>() {
254 return Value::float(float_val);
255 }
256
257 Value::string(value.to_string())
259 }
260
261 fn skip_whitespace_and_comments(&mut self) {
262 while !self.at_end() {
263 match self.current_char() {
264 ' ' | '\t' => self.advance(),
265 '\n' | '\r' => self.skip_newline(),
266 '#' | '!' => self.skip_comment(),
267 _ => break,
268 }
269 }
270 }
271
272 fn skip_whitespace(&mut self) {
273 while !self.at_end() && (self.current_char() == ' ' || self.current_char() == '\t') {
274 self.advance();
275 }
276 }
277
278 fn skip_comment(&mut self) {
279 while !self.at_end() && self.current_char() != '\n' && self.current_char() != '\r' {
280 self.advance();
281 }
282 }
283
284 fn skip_newline(&mut self) {
285 if !self.at_end() && self.current_char() == '\r' {
286 self.advance();
287 }
288 if !self.at_end() && self.current_char() == '\n' {
289 self.advance();
290 }
291 }
292
293 fn current_char(&self) -> char {
294 self.input.chars().nth(self.position).unwrap_or('\0')
295 }
296
297 fn at_end(&self) -> bool {
298 self.position >= self.input.len()
299 }
300
301 fn advance(&mut self) {
302 if !self.at_end() {
303 if self.current_char() == '\n' {
304 self.line += 1;
305 self.column = 1;
306 } else {
307 self.column += 1;
308 }
309 self.position += 1;
310 }
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_simple_properties() {
320 let input = "key1=value1\nkey2=123\nbool_key=true";
321 let mut parser = PropertiesParser::new(input.to_string());
322 let result = parser.parse().unwrap();
323
324 if let Value::Table(table) = result {
325 assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
326 assert_eq!(table.get("key2").unwrap().as_integer().unwrap(), 123);
327 assert!(table.get("bool_key").unwrap().as_bool().unwrap());
328 }
329 }
330
331 #[test]
332 fn test_comments() {
333 let input = "# This is a comment\nkey1=value1\n! Another comment\nkey2=value2";
334 let mut parser = PropertiesParser::new(input.to_string());
335 let result = parser.parse().unwrap();
336
337 if let Value::Table(table) = result {
338 assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
339 assert_eq!(table.get("key2").unwrap().as_string().unwrap(), "value2");
340 }
341 }
342
343 #[test]
344 fn test_escape_sequences() {
345 let input = r"key1=line1\nline2\ttab";
346 let mut parser = PropertiesParser::new(input.to_string());
347 let result = parser.parse().unwrap();
348
349 if let Value::Table(table) = result {
350 assert_eq!(
351 table.get("key1").unwrap().as_string().unwrap(),
352 "line1\nline2\ttab"
353 );
354 }
355 }
356
357 #[test]
358 fn test_colon_separator() {
359 let input = "key1:value1\nkey2: value2";
360 let mut parser = PropertiesParser::new(input.to_string());
361 let result = parser.parse().unwrap();
362
363 if let Value::Table(table) = result {
364 assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
365 assert_eq!(table.get("key2").unwrap().as_string().unwrap(), "value2");
366 }
367 }
368}