1use std::{borrow::Cow, fmt};
13
14use crate::{RdNode, RdNodeKind, RdPath, RdShapeError, RdShapeErrorKind};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct RdOptionList<'a> {
19 path: RdPath,
20 pairs: Vec<RdOptionPair<'a>>,
21 diagnostics: Vec<RdOptionError>,
22 _nodes: std::marker::PhantomData<&'a [RdNode]>,
23}
24
25impl<'a> RdOptionList<'a> {
26 pub fn parse(nodes: &'a [RdNode], path: RdPath) -> Result<Self, RdOptionError> {
39 let mut text = String::new();
40 for (index, node) in nodes.iter().enumerate() {
41 match node {
42 RdNode::Text(value) | RdNode::Verb(value) => text.push_str(value),
43 other => {
44 return Err(RdShapeError::new(
45 path.with_child(index),
46 None,
47 RdShapeErrorKind::UnexpectedContent {
48 actual: RdNodeKind::of(other),
49 },
50 )
51 .into());
52 }
53 }
54 }
55
56 let mut pairs = Vec::new();
57 let mut diagnostics = Vec::new();
58 if text.is_empty() {
59 return Ok(Self {
60 path,
61 pairs,
62 diagnostics,
63 _nodes: std::marker::PhantomData,
64 });
65 }
66 for (pair_index, entry) in text.split(',').enumerate() {
67 if entry.is_empty() {
68 return Err(RdOptionError::malformed(
69 path.clone(),
70 pair_index,
71 entry,
72 RdOptionPairErrorKind::EmptyPair,
73 ));
74 }
75 let Some((raw_key, raw_value)) = entry.split_once('=') else {
76 return Err(RdOptionError::malformed(
77 path.clone(),
78 pair_index,
79 entry,
80 RdOptionPairErrorKind::MissingEquals,
81 ));
82 };
83 let key = raw_key.trim_ascii();
84 let value = raw_value.trim_ascii();
85 let reason = if key.is_empty() {
86 Some(RdOptionPairErrorKind::EmptyKey)
87 } else if value.is_empty() {
88 Some(RdOptionPairErrorKind::EmptyValue)
89 } else {
90 None
91 };
92 if let Some(reason) = reason {
93 return Err(RdOptionError::malformed(
94 path.clone(),
95 pair_index,
96 entry,
97 reason,
98 ));
99 }
100
101 let pair = RdOptionPair {
102 index: pair_index,
103 key: Cow::Owned(key.to_string()),
104 value: Cow::Owned(value.to_string()),
105 };
106 if RdSexprOptionKey::from_name(key).is_none() {
107 diagnostics.push(RdOptionError::UnknownKey {
108 path: path.clone(),
109 pair_index,
110 key: key.to_string(),
111 });
112 }
113 if let Some(first_pair_index) = pairs.iter().find(|p| p.key() == key).map(|p| p.index) {
114 diagnostics.push(RdOptionError::DuplicateKey {
115 path: path.clone(),
116 pair_index,
117 key: key.to_string(),
118 first_pair_index,
119 });
120 }
121 if let Some(known) = RdSexprOptionKey::from_name(key)
122 && decode_value(known, value).is_err()
123 {
124 diagnostics.push(RdOptionError::InvalidValue {
125 path: path.clone(),
126 pair_index,
127 key: known,
128 value: value.to_string(),
129 expected: known.value_kind(),
130 });
131 }
132 pairs.push(pair);
133 }
134
135 Ok(Self {
136 path,
137 pairs,
138 diagnostics,
139 _nodes: std::marker::PhantomData,
140 })
141 }
142
143 pub fn path(&self) -> &RdPath {
145 &self.path
146 }
147 pub fn pairs(&self) -> &[RdOptionPair<'a>] {
149 &self.pairs
150 }
151 pub fn diagnostics(&self) -> &[RdOptionError] {
153 &self.diagnostics
154 }
155 pub fn typed(&self) -> RdSexprOptionOverrides {
157 let mut result = RdSexprOptionOverrides::empty();
158 for pair in &self.pairs {
159 if let Some(key) = pair.known_key()
160 && let Ok(value) = decode_value(key, pair.value())
161 {
162 result.set(key, value);
163 }
164 }
165 result
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct RdOptionPair<'a> {
172 index: usize,
173 key: Cow<'a, str>,
174 value: Cow<'a, str>,
175}
176
177impl<'a> RdOptionPair<'a> {
178 pub fn index(&self) -> usize {
180 self.index
181 }
182 pub fn key(&self) -> &str {
184 &self.key
185 }
186 pub fn value(&self) -> &str {
188 &self.value
189 }
190 pub fn known_key(&self) -> Option<RdSexprOptionKey> {
192 RdSexprOptionKey::from_name(&self.key)
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
198#[non_exhaustive]
199pub enum RdSexprOptionKey {
200 Stage,
202 Results,
204 Echo,
206 Eval,
208 KeepSource,
210 StripWhite,
212 Width,
214 Height,
216 Fig,
218}
219
220impl RdSexprOptionKey {
221 pub fn from_name(name: &str) -> Option<Self> {
223 Some(match name {
224 "stage" => Self::Stage,
225 "results" => Self::Results,
226 "echo" => Self::Echo,
227 "eval" => Self::Eval,
228 "keep.source" => Self::KeepSource,
229 "strip.white" => Self::StripWhite,
230 "width" => Self::Width,
231 "height" => Self::Height,
232 "fig" => Self::Fig,
233 _ => return None,
234 })
235 }
236 pub const fn as_name(self) -> &'static str {
238 match self {
239 Self::Stage => "stage",
240 Self::Results => "results",
241 Self::Echo => "echo",
242 Self::Eval => "eval",
243 Self::KeepSource => "keep.source",
244 Self::StripWhite => "strip.white",
245 Self::Width => "width",
246 Self::Height => "height",
247 Self::Fig => "fig",
248 }
249 }
250 fn value_kind(self) -> RdOptionValueKind {
251 match self {
252 Self::Stage => RdOptionValueKind::Stage,
253 Self::Results => RdOptionValueKind::Results,
254 Self::Echo | Self::Eval | Self::KeepSource => RdOptionValueKind::Boolean,
255 Self::StripWhite => RdOptionValueKind::StripWhite,
256 Self::Width | Self::Height | Self::Fig => RdOptionValueKind::Boolean,
257 }
258 }
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263#[non_exhaustive]
264pub enum RdSexprStage {
265 Build,
267 Install,
269 Render,
271}
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274#[non_exhaustive]
275pub enum RdSexprResults {
276 Text,
278 Verbatim,
280 Rd,
282 Hide,
284}
285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287#[non_exhaustive]
288pub enum RdStripWhite {
289 None,
291 Trim,
293 All,
295}
296
297#[derive(Debug, Clone, Copy, PartialEq)]
299pub struct RdSexprOptionOverrides {
300 pub stage: Option<RdSexprStage>,
302 pub results: Option<RdSexprResults>,
304 pub echo: Option<bool>,
306 pub eval: Option<bool>,
308 pub keep_source: Option<bool>,
310 pub strip_white: Option<RdStripWhite>,
312}
313impl RdSexprOptionOverrides {
314 pub const fn empty() -> Self {
316 Self {
317 stage: None,
318 results: None,
319 echo: None,
320 eval: None,
321 keep_source: None,
322 strip_white: None,
323 }
324 }
325 pub fn apply_to(self, options: &mut RdEffectiveSexprOptions) {
327 if let Some(v) = self.stage {
328 options.stage = v;
329 }
330 if let Some(v) = self.results {
331 options.results = v;
332 }
333 if let Some(v) = self.echo {
334 options.echo = v;
335 }
336 if let Some(v) = self.eval {
337 options.eval = v;
338 }
339 if let Some(v) = self.keep_source {
340 options.keep_source = v;
341 }
342 if let Some(v) = self.strip_white {
343 options.strip_white = v;
344 }
345 }
346 fn set(&mut self, key: RdSexprOptionKey, value: DecodedValue) {
347 match (key, value) {
348 (RdSexprOptionKey::Stage, DecodedValue::Stage(v)) => self.stage = Some(v),
349 (RdSexprOptionKey::Results, DecodedValue::Results(v)) => self.results = Some(v),
350 (RdSexprOptionKey::Echo, DecodedValue::Boolean(v)) => self.echo = Some(v),
351 (RdSexprOptionKey::Eval, DecodedValue::Boolean(v)) => self.eval = Some(v),
352 (RdSexprOptionKey::KeepSource, DecodedValue::Boolean(v)) => self.keep_source = Some(v),
353 (RdSexprOptionKey::StripWhite, DecodedValue::StripWhite(v)) => {
354 self.strip_white = Some(v)
355 }
356 _ => {}
357 }
358 }
359}
360
361#[derive(Debug, Clone, Copy, PartialEq)]
363pub struct RdEffectiveSexprOptions {
364 pub stage: RdSexprStage,
366 pub results: RdSexprResults,
368 pub echo: bool,
370 pub eval: bool,
372 pub keep_source: bool,
374 pub strip_white: RdStripWhite,
376}
377impl Default for RdEffectiveSexprOptions {
378 fn default() -> Self {
379 Self {
380 stage: RdSexprStage::Install,
381 results: RdSexprResults::Text,
382 echo: false,
383 eval: true,
384 keep_source: true,
385 strip_white: RdStripWhite::Trim,
386 }
387 }
388}
389
390#[derive(Debug, Clone, PartialEq, Eq)]
392#[non_exhaustive]
393pub enum RdOptionError {
394 Shape(RdShapeError),
396 MalformedPair {
398 path: RdPath,
399 pair_index: usize,
400 text: String,
401 reason: RdOptionPairErrorKind,
402 },
403 UnknownKey {
405 path: RdPath,
406 pair_index: usize,
407 key: String,
408 },
409 DuplicateKey {
411 path: RdPath,
412 pair_index: usize,
413 key: String,
414 first_pair_index: usize,
415 },
416 InvalidValue {
418 path: RdPath,
419 pair_index: usize,
420 key: RdSexprOptionKey,
421 value: String,
422 expected: RdOptionValueKind,
423 },
424}
425impl RdOptionError {
426 fn malformed(
427 path: RdPath,
428 pair_index: usize,
429 text: &str,
430 reason: RdOptionPairErrorKind,
431 ) -> Self {
432 Self::MalformedPair {
433 path,
434 pair_index,
435 text: text.to_string(),
436 reason,
437 }
438 }
439
440 pub fn path(&self) -> &RdPath {
442 match self {
443 Self::Shape(error) => error.path(),
444 Self::MalformedPair { path, .. }
445 | Self::UnknownKey { path, .. }
446 | Self::DuplicateKey { path, .. }
447 | Self::InvalidValue { path, .. } => path,
448 }
449 }
450}
451impl From<RdShapeError> for RdOptionError {
452 fn from(error: RdShapeError) -> Self {
453 Self::Shape(error)
454 }
455}
456impl fmt::Display for RdOptionError {
457 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
458 match self {
459 Self::Shape(e) => e.fmt(f),
460 Self::MalformedPair {
461 path,
462 pair_index,
463 reason,
464 ..
465 } => write!(f, "malformed option pair {pair_index}: {reason} at {path}"),
466 Self::UnknownKey {
467 path,
468 pair_index,
469 key,
470 } => write!(
471 f,
472 "unknown option key '{key}' (pair {pair_index}) at {path}"
473 ),
474 Self::DuplicateKey {
475 path,
476 pair_index,
477 key,
478 first_pair_index,
479 } => write!(
480 f,
481 "duplicate option key '{key}' (pair {pair_index}; first pair {first_pair_index}) at {path}"
482 ),
483 Self::InvalidValue {
484 path,
485 pair_index,
486 key,
487 value,
488 expected,
489 } => write!(
490 f,
491 "invalid value '{value}' for option {} (pair {pair_index}; expected {expected}) at {path}",
492 key.as_name()
493 ),
494 }
495 }
496}
497impl std::error::Error for RdOptionError {}
498
499#[derive(Debug, Clone, Copy, PartialEq, Eq)]
501#[non_exhaustive]
502pub enum RdOptionPairErrorKind {
503 EmptyPair,
505 MissingEquals,
507 EmptyKey,
509 EmptyValue,
511}
512#[derive(Debug, Clone, Copy, PartialEq, Eq)]
514#[non_exhaustive]
515pub enum RdOptionValueKind {
516 Boolean,
518 Stage,
520 Results,
522 StripWhite,
524}
525impl fmt::Display for RdOptionPairErrorKind {
526 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
527 f.write_str(match self {
528 Self::EmptyPair => "empty pair",
529 Self::MissingEquals => "missing '='",
530 Self::EmptyKey => "empty key",
531 Self::EmptyValue => "empty value",
532 })
533 }
534}
535impl fmt::Display for RdOptionValueKind {
536 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537 f.write_str(match self {
538 Self::Boolean => "boolean",
539 Self::Stage => "stage",
540 Self::Results => "results",
541 Self::StripWhite => "strip.white",
542 })
543 }
544}
545
546#[derive(Debug, Clone, Copy)]
547enum DecodedValue {
548 Boolean(bool),
549 Stage(RdSexprStage),
550 Results(RdSexprResults),
551 StripWhite(RdStripWhite),
552}
553fn decode_value(key: RdSexprOptionKey, value: &str) -> Result<DecodedValue, ()> {
554 match key {
555 RdSexprOptionKey::Echo | RdSexprOptionKey::Eval | RdSexprOptionKey::KeepSource => value
556 .eq_ignore_ascii_case("true")
557 .then_some(DecodedValue::Boolean(true))
558 .or_else(|| {
559 value
560 .eq_ignore_ascii_case("false")
561 .then_some(DecodedValue::Boolean(false))
562 })
563 .ok_or(()),
564 RdSexprOptionKey::Stage => match value {
565 "build" => Ok(DecodedValue::Stage(RdSexprStage::Build)),
566 "install" => Ok(DecodedValue::Stage(RdSexprStage::Install)),
567 "render" => Ok(DecodedValue::Stage(RdSexprStage::Render)),
568 _ => Err(()),
569 },
570 RdSexprOptionKey::Results => match value {
571 "text" => Ok(DecodedValue::Results(RdSexprResults::Text)),
572 "verbatim" => Ok(DecodedValue::Results(RdSexprResults::Verbatim)),
573 "rd" => Ok(DecodedValue::Results(RdSexprResults::Rd)),
574 "hide" => Ok(DecodedValue::Results(RdSexprResults::Hide)),
575 _ => Err(()),
576 },
577 RdSexprOptionKey::StripWhite => {
578 if value.eq_ignore_ascii_case("true") {
579 Ok(DecodedValue::StripWhite(RdStripWhite::Trim))
580 } else if value.eq_ignore_ascii_case("all") {
581 Ok(DecodedValue::StripWhite(RdStripWhite::All))
582 } else if value.eq_ignore_ascii_case("false") {
583 Ok(DecodedValue::StripWhite(RdStripWhite::None))
584 } else {
585 Err(())
586 }
587 }
588 RdSexprOptionKey::Width | RdSexprOptionKey::Height | RdSexprOptionKey::Fig => {
589 Ok(DecodedValue::Boolean(false))
590 }
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 use crate::{RdPathSegment, RdTag};
598
599 fn path() -> RdPath {
600 RdPath::new(vec![RdPathSegment::TopLevel(4)])
601 }
602 fn text(value: &str) -> RdNode {
603 RdNode::Text(value.to_string())
604 }
605
606 #[test]
607 fn concatenates_text_nodes_and_preserves_pairs() {
608 let nodes = [text(" stage = build,echo=true"), text(", results = rd ")];
609 let parsed = RdOptionList::parse(&nodes, path()).unwrap();
610 assert_eq!(
611 parsed.pairs().iter().map(|p| p.index()).collect::<Vec<_>>(),
612 [0, 1, 2]
613 );
614 assert_eq!(parsed.pairs()[0].key(), "stage");
615 assert_eq!(parsed.pairs()[0].value(), "build");
616 assert_eq!(parsed.typed().stage, Some(RdSexprStage::Build));
617 assert_eq!(parsed.typed().echo, Some(true));
618 assert_eq!(parsed.typed().results, Some(RdSexprResults::Rd));
619 }
620
621 #[test]
622 fn accepts_verb_and_mixed_string_leaves() {
623 let verb = [RdNode::Verb("stage=build".to_string())];
624 assert_eq!(
625 RdOptionList::parse(&verb, path()).unwrap().typed().stage,
626 Some(RdSexprStage::Build)
627 );
628 let mixed = [
629 RdNode::Text("stage=build,".to_string()),
630 RdNode::Verb("results=rd".to_string()),
631 ];
632 let parsed = RdOptionList::parse(&mixed, path()).unwrap();
633 assert_eq!(parsed.typed().stage, Some(RdSexprStage::Build));
634 assert_eq!(parsed.typed().results, Some(RdSexprResults::Rd));
635 }
636
637 #[test]
638 fn malformed_reasons_are_hard_errors() {
639 for (input, expected) in [
640 ("", RdOptionPairErrorKind::EmptyPair),
641 ("noequals", RdOptionPairErrorKind::MissingEquals),
642 (" =value", RdOptionPairErrorKind::EmptyKey),
643 ("key= ", RdOptionPairErrorKind::EmptyValue),
644 ] {
645 let nodes = [text(input)];
646 let result = RdOptionList::parse(&nodes, path());
647 if input.is_empty() {
648 assert!(result.is_ok());
649 } else {
650 assert_eq!(result.as_ref().unwrap_err().path(), &path());
651 assert!(
652 matches!(result, Err(RdOptionError::MalformedPair { reason, .. }) if reason == expected)
653 );
654 }
655 }
656 let error = RdOptionList::parse(&[text("a=1,,b=2")], path()).unwrap_err();
657 assert_eq!(error.path(), &path());
658 assert!(matches!(
659 error,
660 RdOptionError::MalformedPair {
661 reason: RdOptionPairErrorKind::EmptyPair,
662 ..
663 }
664 ));
665 }
666
667 #[test]
668 fn rejects_non_text_at_node_path() {
669 let nodes = [RdNode::tagged(RdTag::Title, None, vec![])];
670 let error = RdOptionList::parse(&nodes, path()).unwrap_err();
671 assert!(
672 matches!(error, RdOptionError::Shape(shape) if shape.path() == &path().with_child(0) && matches!(shape.kind(), RdShapeErrorKind::UnexpectedContent { actual: RdNodeKind::Tagged }))
673 );
674 }
675
676 #[test]
677 fn decodes_all_typed_keys_and_case_insensitive_values() {
678 let nodes = [text(
679 "stage=render,results=verbatim,echo=TrUe,eval=false,keep.source=FALSE,strip.white=TRUE",
680 )];
681 let typed = RdOptionList::parse(&nodes, path()).unwrap().typed();
682 assert_eq!(typed.stage, Some(RdSexprStage::Render));
683 assert_eq!(typed.results, Some(RdSexprResults::Verbatim));
684 assert_eq!(typed.echo, Some(true));
685 assert_eq!(typed.eval, Some(false));
686 assert_eq!(typed.keep_source, Some(false));
687 assert_eq!(typed.strip_white, Some(RdStripWhite::Trim));
688 for value in ["false", "all", "true"] {
689 let nodes = [text(&format!("strip.white={value}"))];
690 let parsed = RdOptionList::parse(&nodes, path()).unwrap();
691 assert_eq!(
692 parsed.typed().strip_white,
693 Some(match value {
694 "false" => RdStripWhite::None,
695 "all" => RdStripWhite::All,
696 _ => RdStripWhite::Trim,
697 })
698 );
699 }
700 }
701
702 #[test]
703 fn collects_soft_diagnostics_and_keeps_pairs() {
704 let nodes = [text("unknown=x,echo=true,echo=maybe,echo=false")];
705 let parsed = RdOptionList::parse(&nodes, path()).unwrap();
706 assert_eq!(parsed.pairs().len(), 4);
707 assert!(
708 parsed
709 .diagnostics()
710 .iter()
711 .any(|e| matches!(e, RdOptionError::UnknownKey { key, .. } if key == "unknown"))
712 );
713 assert!(
714 parsed
715 .diagnostics()
716 .iter()
717 .all(|error| error.path() == &path())
718 );
719 assert!(parsed.diagnostics().iter().any(|e| matches!(
720 e,
721 RdOptionError::DuplicateKey {
722 first_pair_index: 1,
723 pair_index: 2,
724 ..
725 }
726 )));
727 assert!(parsed.diagnostics().iter().any(|e| matches!(
728 e,
729 RdOptionError::InvalidValue {
730 pair_index: 2,
731 expected: RdOptionValueKind::Boolean,
732 ..
733 }
734 )));
735 assert_eq!(parsed.typed().echo, Some(false));
736 }
737
738 #[test]
739 fn formats_soft_diagnostics_with_pair_indices() {
740 let nodes = [text("unknown=x,echo=true,echo=maybe")];
741 let parsed = RdOptionList::parse(&nodes, path()).unwrap();
742 let messages: Vec<_> = parsed
743 .diagnostics()
744 .iter()
745 .map(ToString::to_string)
746 .collect();
747 assert_eq!(
748 messages,
749 [
750 "unknown option key 'unknown' (pair 0) at top-level[4]",
751 "duplicate option key 'echo' (pair 2; first pair 1) at top-level[4]",
752 "invalid value 'maybe' for option echo (pair 2; expected boolean) at top-level[4]",
753 ]
754 );
755 }
756
757 #[test]
758 fn invalid_duplicate_does_not_erase_last_valid_value() {
759 let nodes = [text("stage=build,stage=bad")];
760 let parsed = RdOptionList::parse(&nodes, path()).unwrap();
761 assert_eq!(parsed.typed().stage, Some(RdSexprStage::Build));
762 }
763
764 #[test]
765 fn figure_keys_are_known_but_raw_only() {
766 let nodes = [text("width=bad,height=also-bad,fig=anything")];
767 let parsed = RdOptionList::parse(&nodes, path()).unwrap();
768 assert_eq!(
769 parsed
770 .pairs()
771 .iter()
772 .map(|p| p.known_key())
773 .collect::<Vec<_>>(),
774 [
775 Some(RdSexprOptionKey::Width),
776 Some(RdSexprOptionKey::Height),
777 Some(RdSexprOptionKey::Fig)
778 ]
779 );
780 assert_eq!(parsed.typed(), RdSexprOptionOverrides::empty());
781 assert!(parsed.diagnostics().is_empty());
782 }
783
784 #[test]
785 fn empty_inputs_and_defaults_are_valid() {
786 assert!(RdOptionList::parse(&[], path()).unwrap().pairs().is_empty());
787 assert!(
788 RdOptionList::parse(&[text("")], path())
789 .unwrap()
790 .pairs()
791 .is_empty()
792 );
793 let defaults = RdEffectiveSexprOptions::default();
794 assert_eq!(
795 defaults,
796 RdEffectiveSexprOptions {
797 stage: RdSexprStage::Install,
798 results: RdSexprResults::Text,
799 echo: false,
800 eval: true,
801 keep_source: true,
802 strip_white: RdStripWhite::Trim
803 }
804 );
805 }
806
807 #[test]
808 fn option_error_is_displayable_standard_error() {
809 let error = RdOptionList::parse(&[text("bad")], path()).unwrap_err();
810 assert!(error.to_string().contains("missing '='"));
811 let _: Box<dyn std::error::Error> = Box::new(error);
812 }
813}