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 = u32::from_str_radix(&hex_digits, 16).unwrap();
213 if let Some(unicode_char) = char::from_u32(code_point) {
214 Ok(unicode_char.to_string())
215 } else {
216 Err(Error::Parse {
217 message: format!("Invalid unicode code point: {code_point}"),
218 line: self.line,
219 column: self.column,
220 file: None,
221 })
222 }
223 }
224
225 fn infer_value_type(&self, value: &str) -> Value {
226 if value.is_empty() {
227 return Value::string(String::new());
228 }
229
230 match value.to_lowercase().as_str() {
232 "true" => return Value::bool(true),
233 "false" => return Value::bool(false),
234 _ => {}
235 }
236
237 if let Ok(int_val) = value.parse::<i64>() {
239 return Value::integer(int_val);
240 }
241
242 if let Ok(float_val) = value.parse::<f64>() {
244 return Value::float(float_val);
245 }
246
247 Value::string(value.to_string())
249 }
250
251 fn skip_whitespace_and_comments(&mut self) {
252 while !self.at_end() {
253 match self.current_char() {
254 ' ' | '\t' => self.advance(),
255 '\n' | '\r' => self.skip_newline(),
256 '#' | '!' => self.skip_comment(),
257 _ => break,
258 }
259 }
260 }
261
262 fn skip_whitespace(&mut self) {
263 while !self.at_end() && (self.current_char() == ' ' || self.current_char() == '\t') {
264 self.advance();
265 }
266 }
267
268 fn skip_comment(&mut self) {
269 while !self.at_end() && self.current_char() != '\n' && self.current_char() != '\r' {
270 self.advance();
271 }
272 }
273
274 fn skip_newline(&mut self) {
275 if !self.at_end() && self.current_char() == '\r' {
276 self.advance();
277 }
278 if !self.at_end() && self.current_char() == '\n' {
279 self.advance();
280 }
281 }
282
283 fn current_char(&self) -> char {
284 self.input.chars().nth(self.position).unwrap_or('\0')
285 }
286
287 fn at_end(&self) -> bool {
288 self.position >= self.input.len()
289 }
290
291 fn advance(&mut self) {
292 if !self.at_end() {
293 if self.current_char() == '\n' {
294 self.line += 1;
295 self.column = 1;
296 } else {
297 self.column += 1;
298 }
299 self.position += 1;
300 }
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn test_simple_properties() {
310 let input = "key1=value1\nkey2=123\nbool_key=true";
311 let mut parser = PropertiesParser::new(input.to_string());
312 let result = parser.parse().unwrap();
313
314 if let Value::Table(table) = result {
315 assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
316 assert_eq!(table.get("key2").unwrap().as_integer().unwrap(), 123);
317 assert!(table.get("bool_key").unwrap().as_bool().unwrap());
318 }
319 }
320
321 #[test]
322 fn test_comments() {
323 let input = "# This is a comment\nkey1=value1\n! Another comment\nkey2=value2";
324 let mut parser = PropertiesParser::new(input.to_string());
325 let result = parser.parse().unwrap();
326
327 if let Value::Table(table) = result {
328 assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
329 assert_eq!(table.get("key2").unwrap().as_string().unwrap(), "value2");
330 }
331 }
332
333 #[test]
334 fn test_escape_sequences() {
335 let input = r"key1=line1\nline2\ttab";
336 let mut parser = PropertiesParser::new(input.to_string());
337 let result = parser.parse().unwrap();
338
339 if let Value::Table(table) = result {
340 assert_eq!(
341 table.get("key1").unwrap().as_string().unwrap(),
342 "line1\nline2\ttab"
343 );
344 }
345 }
346
347 #[test]
348 fn test_colon_separator() {
349 let input = "key1:value1\nkey2: value2";
350 let mut parser = PropertiesParser::new(input.to_string());
351 let result = parser.parse().unwrap();
352
353 if let Value::Table(table) = result {
354 assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
355 assert_eq!(table.get("key2").unwrap().as_string().unwrap(), "value2");
356 }
357 }
358}