onetaskgraph_core/template/answers.rs
1// llmlint: ignore-file[code_lands_in_the_domain_that_owns_it] A part of the `template` module; why that module sits in this crate is stated once, at the head of its `mod.rs`.
2//! The answers a template is rendered from, before they are checked against its variables.
3
4use std::collections::BTreeMap;
5
6use serde_json::Value;
7
8use super::TemplateError;
9
10/// One variable's answer, as it was given.
11#[derive(Debug, Clone, PartialEq)]
12enum Answer {
13 /// A value already typed — from an answers document, or built in code.
14 Value(Value),
15 /// Text a command line gave (`--var NAME=VALUE`): the literal value for a `string` or
16 /// `text` variable, YAML for every other type. Which of the two is decided by the
17 /// declaration, so it is kept as text until it meets one.
18 Text(String),
19 /// Explicitly no answer: over an earlier set of answers, it removes that name's.
20 Unset,
21}
22
23/// The answers a template is rendered from, by variable name.
24///
25/// Parsed from an answers document ([`Answers::from_yaml`]) or built in code. Nothing here
26/// knows a template's variables: an answer naming no declared variable, or of the wrong
27/// type, is refused when the answers meet the template, by [`super::Template::render`].
28#[derive(Debug, Clone, Default, PartialEq)]
29pub struct Answers {
30 entries: BTreeMap<String, Answer>,
31}
32
33impl Answers {
34 /// Answers to nothing: rendered from these, every default is taken and every required
35 /// variable is refused.
36 #[must_use]
37 pub fn new() -> Self {
38 Self::default()
39 }
40
41 /// Read an answers document: a top-level YAML mapping from variable name to value.
42 ///
43 /// An empty document is no answers.
44 ///
45 /// # Errors
46 ///
47 /// [`TemplateError::MalformedAnswers`] for a document that is not YAML, or whose top
48 /// level is not a mapping with string keys.
49 pub fn from_yaml(text: &str) -> Result<Self, TemplateError> {
50 let parsed: Value =
51 serde_norway::from_str(text).map_err(|error| TemplateError::MalformedAnswers {
52 message: format!("it is not YAML a mapping of answers can be read from: {error}"),
53 })?;
54 match parsed {
55 Value::Null => Ok(Self::new()),
56 Value::Object(entries) => Ok(Self {
57 entries: entries
58 .into_iter()
59 .map(|(name, value)| (name, Answer::Value(value)))
60 .collect(),
61 }),
62 _ => Err(TemplateError::MalformedAnswers {
63 message: "its top level is not a mapping from variable name to value".to_owned(),
64 }),
65 }
66 }
67
68 /// Answer `name` with `value`.
69 pub fn set(&mut self, name: impl Into<String>, value: Value) -> &mut Self {
70 self.entries.insert(name.into(), Answer::Value(value));
71 self
72 }
73
74 /// Answer `name` with text as a command line gives it: taken literally for a `string` or
75 /// `text` variable, and read as YAML for any other type.
76 pub fn set_text(&mut self, name: impl Into<String>, text: impl Into<String>) -> &mut Self {
77 self.entries.insert(name.into(), Answer::Text(text.into()));
78 self
79 }
80
81 /// Leave `name` unanswered — and, over earlier answers, take away the one they gave.
82 pub fn unset(&mut self, name: impl Into<String>) -> &mut Self {
83 self.entries.insert(name.into(), Answer::Unset);
84 self
85 }
86
87 /// These answers with `later` laid over them: a name `later` answers takes its answer,
88 /// a name `later` unsets is unanswered, and every other name keeps what it had.
89 #[must_use]
90 pub fn overlay(&self, later: &Self) -> Self {
91 let mut entries = self.entries.clone();
92 for (name, answer) in &later.entries {
93 entries.insert(name.clone(), answer.clone());
94 }
95 Self { entries }
96 }
97
98 /// Every name answered, in name order — an unset name is not answered.
99 pub fn names(&self) -> impl Iterator<Item = &str> {
100 self.entries
101 .iter()
102 .filter(|(_, answer)| !matches!(answer, Answer::Unset))
103 .map(|(name, _)| name.as_str())
104 }
105
106 /// The answer given for `name`, if any: a typed value, or text still to be read.
107 pub(super) fn given(&self, name: &str) -> Option<Given<'_>> {
108 match self.entries.get(name)? {
109 Answer::Value(value) => Some(Given::Value(value)),
110 Answer::Text(text) => Some(Given::Text(text)),
111 Answer::Unset => None,
112 }
113 }
114}
115
116/// An answer as [`Answers::given`] hands it over.
117pub(super) enum Given<'a> {
118 /// Already typed.
119 Value(&'a Value),
120 /// Command-line text, read by the declaration it meets.
121 Text(&'a str),
122}