Skip to main content

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}