1use crate::{OptionValue, Options, Source};
8use std::fmt::{self, Display, Formatter};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum ParseError {
29 MissingSource {
31 input: String,
33 at: usize,
35 },
36 UnexpectedEnd {
38 input: String,
40 at: usize,
42 expected: &'static str,
44 },
45 UnexpectedChar {
47 input: String,
49 at: usize,
51 found: char,
53 expected: &'static str,
55 },
56 InvalidIdentifier {
58 input: String,
60 at: usize,
62 found: String,
64 },
65 EmptyKey {
67 input: String,
69 at: usize,
71 },
72 EmptyValue {
74 input: String,
76 at: usize,
78 },
79 InvalidEscape {
81 input: String,
83 at: usize,
85 },
86 UnclosedString {
88 input: String,
90 at: usize,
92 },
93 UnclosedList {
95 input: String,
97 at: usize,
99 },
100 UnclosedMap {
102 input: String,
104 at: usize,
106 },
107 TrailingComma {
109 input: String,
111 at: usize,
113 },
114 InvalidNumber {
116 input: String,
118 at: usize,
120 found: String,
122 },
123 TrailingInput {
125 input: String,
127 at: usize,
129 rest: String,
131 },
132 InvalidOnError {
134 input: String,
136 at: usize,
138 message: String,
140 },
141}
142
143impl Display for ParseError {
144 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
145 let (input, at, message) = match self {
146 Self::MissingSource { input, at, .. } => (
147 input.as_str(),
148 *at,
149 "configuration source is required".to_string(),
150 ),
151 Self::UnexpectedEnd {
152 input,
153 at,
154 expected,
155 ..
156 } => (
157 input.as_str(),
158 *at,
159 format!("configuration source: expected {expected}, found end of input"),
160 ),
161 Self::UnexpectedChar {
162 input,
163 at,
164 found,
165 expected,
166 ..
167 } => (
168 input.as_str(),
169 *at,
170 format!("configuration source: expected {expected}, found `{found}`"),
171 ),
172 Self::InvalidIdentifier {
173 input, at, found, ..
174 } => (
175 input.as_str(),
176 *at,
177 format!("configuration source: invalid identifier `{found}`"),
178 ),
179 Self::EmptyKey { input, at, .. } => (
180 input.as_str(),
181 *at,
182 "configuration source option key cannot be empty".to_string(),
183 ),
184 Self::EmptyValue { input, at, .. } => (
185 input.as_str(),
186 *at,
187 "configuration source option value cannot be empty; use \"\"".to_string(),
188 ),
189 Self::InvalidEscape { input, at, .. } => (
190 input.as_str(),
191 *at,
192 "configuration source: invalid escape sequence in string".to_string(),
193 ),
194 Self::UnclosedString { input, at, .. } => (
195 input.as_str(),
196 *at,
197 "configuration source: unclosed string".to_string(),
198 ),
199 Self::UnclosedList { input, at, .. } => (
200 input.as_str(),
201 *at,
202 "configuration source: unclosed list".to_string(),
203 ),
204 Self::UnclosedMap { input, at, .. } => (
205 input.as_str(),
206 *at,
207 "configuration source: unclosed map".to_string(),
208 ),
209 Self::TrailingComma { input, at, .. } => (
210 input.as_str(),
211 *at,
212 "configuration source: trailing comma".to_string(),
213 ),
214 Self::InvalidNumber {
215 input, at, found, ..
216 } => (
217 input.as_str(),
218 *at,
219 format!("configuration source: invalid number `{found}`"),
220 ),
221 Self::TrailingInput {
222 input, at, rest, ..
223 } => (
224 input.as_str(),
225 *at,
226 format!("configuration source: unexpected trailing input `{rest}`"),
227 ),
228 Self::InvalidOnError { input, at, message } => (
229 input.as_str(),
230 *at,
231 format!("configuration source: {message}"),
232 ),
233 };
234 write!(
235 f,
236 "invalid configuration source at column {}: {}",
237 at + 1,
238 message
239 )?;
240 if f.alternate() {
241 write!(f, "\n {}\n ", input)?;
242 for _ in 0..at {
243 write!(f, " ")?;
244 }
245 write!(f, "^")?;
246 }
247 Ok(())
248 }
249}
250
251impl std::error::Error for ParseError {}
252
253pub fn parse(input: &str) -> Result<Source, ParseError> {
291 Parser::new(input).parse()
292}
293
294impl Display for Source {
295 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
296 write!(f, "{}", self.source())?;
297 if !self.options().is_empty() {
298 write!(f, "(")?;
299 for (index, (key, value)) in self.options().entries().iter().enumerate() {
300 if index > 0 {
301 write!(f, ",")?;
302 }
303 write!(f, "{key}=")?;
304 write_option_value(f, value)?;
305 }
306 write!(f, ")")?;
307 }
308 if self.resource_colon() || !self.resource().is_empty() {
309 write!(f, ":{}", self.resource())?;
310 }
311 Ok(())
312 }
313}
314
315fn write_option_value(f: &mut Formatter<'_>, value: &OptionValue) -> fmt::Result {
316 match value {
317 OptionValue::Bool(value) => write!(f, "{value}"),
318 OptionValue::Integer(value) => write!(f, "{value}"),
319 OptionValue::Float(value) => {
320 if value.is_finite() && value.fract() == 0.0 {
321 write!(f, "{value:.1}")
322 } else {
323 write!(f, "{value}")
324 }
325 }
326 OptionValue::String(value) => {
327 let needs_quotes = value.is_empty()
328 || !value
329 .chars()
330 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
331 || value.eq_ignore_ascii_case("true")
332 || value.eq_ignore_ascii_case("false")
333 || is_int_token(value)
334 || is_float_token(value);
335 if needs_quotes {
336 write!(f, "\"")?;
337 for ch in value.chars() {
338 match ch {
339 '"' => write!(f, "\\\"")?,
340 '\\' => write!(f, "\\\\")?,
341 '\n' => write!(f, "\\n")?,
342 '\r' => write!(f, "\\r")?,
343 '\t' => write!(f, "\\t")?,
344 ch => write!(f, "{ch}")?,
345 }
346 }
347 write!(f, "\"")
348 } else {
349 write!(f, "{value}")
350 }
351 }
352 OptionValue::List(values) => {
353 write!(f, "[")?;
354 for (index, item) in values.iter().enumerate() {
355 if index > 0 {
356 write!(f, ",")?;
357 }
358 write_option_value(f, item)?;
359 }
360 write!(f, "]")
361 }
362 OptionValue::Map(options) => {
363 write!(f, "(")?;
364 for (index, (key, item)) in options.entries().iter().enumerate() {
365 if index > 0 {
366 write!(f, ",")?;
367 }
368 write!(f, "{key}=")?;
369 write_option_value(f, item)?;
370 }
371 write!(f, ")")
372 }
373 }
374}
375
376struct Parser<'a> {
377 input: &'a str,
378 pos: usize,
379}
380
381impl<'a> Parser<'a> {
382 fn new(input: &'a str) -> Self {
383 Self { input, pos: 0 }
384 }
385
386 fn owned_input(&self) -> String {
387 self.input.to_string()
388 }
389
390 fn parse(mut self) -> Result<Source, ParseError> {
391 let source = self.parse_source()?;
392 let options_at = self.pos;
393 let options = if self.peek() == Some('(') {
394 self.parse_options_block()?
395 } else {
396 Options::default()
397 };
398 let (resource_colon, resource) = if self.peek() == Some(':') {
399 self.bump();
400 let resource = self.input[self.pos..].to_string();
401 self.pos = self.input.len();
402 (true, resource)
403 } else {
404 (false, String::new())
405 };
406 if self.pos < self.input.len() {
407 return Err(ParseError::TrailingInput {
408 input: self.owned_input(),
409 at: self.pos,
410 rest: self.input[self.pos..].to_string(),
411 });
412 }
413 if let Some(message) = validate_on_error(&options) {
414 return Err(ParseError::InvalidOnError {
415 input: self.owned_input(),
416 at: options_at,
417 message,
418 });
419 }
420 Ok(Source {
421 source,
422 options,
423 resource,
424 resource_colon,
425 })
426 }
427
428 fn parse_source(&mut self) -> Result<String, ParseError> {
429 let start = self.pos;
430 if !self
431 .peek()
432 .is_some_and(|ch| is_ident_char(ch) && !ch.is_ascii_digit())
433 {
434 if self.pos >= self.input.len() {
435 return Err(ParseError::MissingSource {
436 input: self.owned_input(),
437 at: self.pos,
438 });
439 }
440 let found = self.peek().unwrap();
441 return Err(ParseError::UnexpectedChar {
442 input: self.owned_input(),
443 at: self.pos,
444 found,
445 expected: "source identifier",
446 });
447 }
448 while self.peek().is_some_and(is_ident_char) {
449 self.bump();
450 }
451 if self.pos == start {
452 return Err(ParseError::MissingSource {
453 input: self.owned_input(),
454 at: self.pos,
455 });
456 }
457 Ok(self.input[start..self.pos].to_string())
458 }
459
460 fn parse_options_block(&mut self) -> Result<Options, ParseError> {
461 self.expect_char('(', "opening `(` for options")?;
462 let mut options = Options::default();
463 if self.peek() == Some(')') {
464 self.bump();
465 return Ok(options);
466 }
467 loop {
468 let key = self.parse_key()?;
469 self.expect_char('=', "option value after `=`")?;
470 let value = self.parse_value()?;
471 options.insert(key, value);
472 match self.peek() {
473 Some(',') => {
474 self.bump();
475 if matches!(self.peek(), Some(')' | ']' | ',')) {
476 return Err(ParseError::TrailingComma {
477 input: self.owned_input(),
478 at: self.pos,
479 });
480 }
481 }
482 Some(')') => {
483 self.bump();
484 break;
485 }
486 None => {
487 return Err(ParseError::UnclosedMap {
488 input: self.owned_input(),
489 at: self.pos,
490 });
491 }
492 Some(found) => {
493 return Err(ParseError::UnexpectedChar {
494 input: self.owned_input(),
495 at: self.pos,
496 found,
497 expected: "`,` or `)`",
498 });
499 }
500 }
501 }
502 Ok(options)
503 }
504
505 fn parse_map_value(&mut self) -> Result<OptionValue, ParseError> {
506 self.expect_char('(', "opening `(` for map")?;
507 let mut options = Options::default();
508 if self.peek() == Some(')') {
509 self.bump();
510 return Ok(OptionValue::Map(options));
511 }
512 loop {
513 let key = self.parse_key()?;
514 self.expect_char('=', "map value after `=`")?;
515 let value = self.parse_value()?;
516 options.insert(key, value);
517 match self.peek() {
518 Some(',') => {
519 self.bump();
520 if matches!(self.peek(), Some(')' | ']' | ',')) {
521 return Err(ParseError::TrailingComma {
522 input: self.owned_input(),
523 at: self.pos,
524 });
525 }
526 }
527 Some(')') => {
528 self.bump();
529 break;
530 }
531 None => {
532 return Err(ParseError::UnclosedMap {
533 input: self.owned_input(),
534 at: self.pos,
535 });
536 }
537 Some(found) => {
538 return Err(ParseError::UnexpectedChar {
539 input: self.owned_input(),
540 at: self.pos,
541 found,
542 expected: "`,` or `)`",
543 });
544 }
545 }
546 }
547 Ok(OptionValue::Map(options))
548 }
549
550 fn parse_list_value(&mut self) -> Result<OptionValue, ParseError> {
551 self.expect_char('[', "opening `[` for list")?;
552 let mut values = Vec::new();
553 if self.peek() == Some(']') {
554 self.bump();
555 return Ok(OptionValue::List(values));
556 }
557 loop {
558 values.push(self.parse_value()?);
559 match self.peek() {
560 Some(',') => {
561 self.bump();
562 if matches!(self.peek(), Some(']' | ',')) {
563 return Err(ParseError::TrailingComma {
564 input: self.owned_input(),
565 at: self.pos,
566 });
567 }
568 }
569 Some(']') => {
570 self.bump();
571 break;
572 }
573 None => {
574 return Err(ParseError::UnclosedList {
575 input: self.owned_input(),
576 at: self.pos,
577 });
578 }
579 Some(found) => {
580 return Err(ParseError::UnexpectedChar {
581 input: self.owned_input(),
582 at: self.pos,
583 found,
584 expected: "`,` or `]`",
585 });
586 }
587 }
588 }
589 Ok(OptionValue::List(values))
590 }
591
592 fn parse_key(&mut self) -> Result<String, ParseError> {
593 let start = self.pos;
594 if !self
595 .peek()
596 .is_some_and(|ch| is_ident_char(ch) && !ch.is_ascii_digit())
597 {
598 if self.peek() == Some('=') {
599 return Err(ParseError::EmptyKey {
600 input: self.owned_input(),
601 at: self.pos,
602 });
603 }
604 let found = self
605 .peek()
606 .map(|ch| ch.to_string())
607 .unwrap_or_else(|| "end of input".to_string());
608 return if self.peek().is_some() {
609 Err(ParseError::UnexpectedChar {
610 input: self.owned_input(),
611 at: self.pos,
612 found: self.peek().unwrap(),
613 expected: "option key",
614 })
615 } else {
616 Err(ParseError::InvalidIdentifier {
617 input: self.owned_input(),
618 at: self.pos,
619 found,
620 })
621 };
622 }
623 while self.peek().is_some_and(is_ident_char) {
624 self.bump();
625 }
626 if self.pos == start {
627 return Err(ParseError::EmptyKey {
628 input: self.owned_input(),
629 at: self.pos,
630 });
631 }
632 Ok(self.input[start..self.pos].to_string())
633 }
634
635 fn parse_value(&mut self) -> Result<OptionValue, ParseError> {
636 match self.peek() {
637 Some('"') => Ok(OptionValue::String(self.parse_quoted_string()?)),
638 Some('[') => self.parse_list_value(),
639 Some('(') => self.parse_map_value(),
640 Some('=') | Some(',') | Some(')') | Some(']') | Some(':') | Some('?') | None => {
641 Err(ParseError::EmptyValue {
642 input: self.owned_input(),
643 at: self.pos,
644 })
645 }
646 Some(_) => {
647 let token = self.parse_unquoted_token()?;
648 let at = self.pos - token.len();
649 let owned_input = self.input.to_string();
650 if token.eq_ignore_ascii_case("true") {
651 Ok(OptionValue::Bool(true))
652 } else if token.eq_ignore_ascii_case("false") {
653 Ok(OptionValue::Bool(false))
654 } else if token.contains('.') {
655 if !is_float_token(&token) {
656 Err(ParseError::InvalidNumber {
657 input: owned_input,
658 at,
659 found: token,
660 })
661 } else {
662 token.parse::<f64>().map(OptionValue::Float).map_err(|_| {
663 ParseError::InvalidNumber {
664 input: owned_input,
665 at,
666 found: token,
667 }
668 })
669 }
670 } else if is_int_token(&token) {
671 token.parse::<i64>().map(OptionValue::Integer).map_err(|_| {
672 ParseError::InvalidNumber {
673 input: owned_input,
674 at,
675 found: token,
676 }
677 })
678 } else {
679 Ok(OptionValue::String(token))
680 }
681 }
682 }
683 }
684
685 fn parse_unquoted_token(&mut self) -> Result<String, ParseError> {
686 let start = self.pos;
687 while self
688 .peek()
689 .is_some_and(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
690 {
691 self.bump();
692 }
693 if self.pos == start {
694 let found = self.peek().unwrap();
695 return Err(ParseError::UnexpectedChar {
696 input: self.owned_input(),
697 at: self.pos,
698 found,
699 expected: "value",
700 });
701 }
702 Ok(self.input[start..self.pos].to_string())
703 }
704
705 fn parse_quoted_string(&mut self) -> Result<String, ParseError> {
706 self.expect_char('"', "opening `\"` for string")?;
707 let start = self.pos;
708 let mut value = String::new();
709 while let Some(ch) = self.peek() {
710 if ch == '"' {
711 self.bump();
712 return Ok(value);
713 }
714 if ch == '\\' {
715 self.bump();
716 let escaped = self.peek().ok_or(ParseError::UnclosedString {
717 input: self.owned_input(),
718 at: start,
719 })?;
720 value.push(match escaped {
721 '"' => '"',
722 '\\' => '\\',
723 'n' => '\n',
724 'r' => '\r',
725 't' => '\t',
726 _ => {
727 return Err(ParseError::InvalidEscape {
728 input: self.owned_input(),
729 at: self.pos - 1,
730 });
731 }
732 });
733 self.bump();
734 continue;
735 }
736 self.bump();
737 value.push(ch);
738 }
739 Err(ParseError::UnclosedString {
740 input: self.owned_input(),
741 at: start,
742 })
743 }
744
745 fn expect_char(&mut self, expected: char, message: &'static str) -> Result<(), ParseError> {
746 match self.peek() {
747 Some(found) if found == expected => {
748 self.bump();
749 Ok(())
750 }
751 Some(found) => Err(ParseError::UnexpectedChar {
752 input: self.owned_input(),
753 at: self.pos,
754 found,
755 expected: message,
756 }),
757 None => Err(ParseError::UnexpectedEnd {
758 input: self.owned_input(),
759 at: self.pos,
760 expected: message,
761 }),
762 }
763 }
764
765 fn peek(&self) -> Option<char> {
766 self.input[self.pos..].chars().next()
767 }
768
769 fn bump(&mut self) -> Option<char> {
770 let ch = self.peek()?;
771 self.pos += ch.len_utf8();
772 Some(ch)
773 }
774}
775
776fn is_ident_char(ch: char) -> bool {
777 ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.')
778}
779
780fn is_int_token(token: &str) -> bool {
781 let Some(body) = token.strip_prefix('-').or(Some(token)) else {
782 return false;
783 };
784 !body.is_empty() && body.chars().all(|ch| ch.is_ascii_digit())
785}
786
787fn is_float_token(token: &str) -> bool {
788 let token = token.strip_prefix('-').unwrap_or(token);
789 let Some((whole, fraction)) = token.split_once('.') else {
790 return false;
791 };
792 !whole.is_empty()
793 && !fraction.is_empty()
794 && whole.chars().all(|ch| ch.is_ascii_digit())
795 && fraction.chars().all(|ch| ch.is_ascii_digit())
796}
797
798fn validate_on_error(options: &Options) -> Option<String> {
803 let value = options.get("on_error")?;
804 let OptionValue::Map(map) = value else {
805 return Some(format!(
806 "`on_error` must be a map like `(load=skip)`, found {}",
807 value.type_name()
808 ));
809 };
810 for (stage, policy) in map.iter() {
811 if !matches!(stage, "load" | "parse" | "validate") {
812 return Some(format!(
813 "unknown `on_error` stage `{stage}`; expected load, parse, or validate"
814 ));
815 }
816 match policy {
817 OptionValue::String(text)
818 if text.eq_ignore_ascii_case("skip") || text.eq_ignore_ascii_case("fail") => {}
819 _ => {
820 return Some(format!(
821 "`on_error` policy for `{stage}` must be `skip` or `fail`, found `{policy}`"
822 ));
823 }
824 }
825 }
826 None
827}