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