Skip to main content

devset_core/
vars.rs

1//! Template variables: declared by profiles, answered by the target, rendered into payload.
2
3use alloc::collections::BTreeMap;
4use core::str;
5use std::collections::HashSet;
6
7use derive_more::{AsRef, Display, Into};
8use minijinja::{AutoEscape, Environment, UndefinedBehavior, Value};
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12use crate::errors::{ParseError, ProfileError, Result, VarError};
13use crate::gate::Pattern;
14use crate::path::RelPath;
15use crate::profile::{MANIFEST, PAYLOAD};
16use crate::resolve::{Layer, LayerFile};
17use crate::target::Target;
18use crate::tree::Tree;
19
20/// The name templates see the target's profiles under, which no variable may take.
21pub(crate) const RESERVED: &str = "devset";
22
23/// A variable's name: an identifier, `[A-Za-z_][A-Za-z0-9_]*`, but not `devset`.
24#[derive(
25    Clone,
26    PartialEq,
27    Eq,
28    PartialOrd,
29    Ord,
30    Hash,
31    AsRef,
32    Display,
33    derive_more::Debug,
34    Into,
35    Serialize,
36    Deserialize,
37    JsonSchema,
38)]
39#[as_ref(str)]
40#[debug("{_0:?}")]
41#[into(String)]
42#[serde(try_from = "String", into = "String")]
43#[schemars(inline)]
44pub struct VarName(Box<str>);
45
46/// A `[vars.name]` entry.
47#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
48#[serde(deny_unknown_fields)]
49pub struct VarSpec {
50    /// What to ask; the name if unset.
51    #[serde(default)]
52    pub prompt: Option<String>,
53    /// The answer offered.
54    #[serde(default)]
55    pub default: Option<String>,
56}
57
58/// A variable the target has not answered.
59#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct Question {
61    /// The variable.
62    pub name: VarName,
63    /// What to ask.
64    pub prompt: String,
65    /// The answer offered: `None` when no layer offers one, or layers offer different ones.
66    pub default: Option<String>,
67}
68
69impl VarName {
70    /// The name as written.
71    #[must_use]
72    pub fn as_str(&self) -> &str {
73        &self.0
74    }
75}
76
77impl TryFrom<String> for VarName {
78    type Error = VarError;
79
80    fn try_from(name: String) -> Result<Self, VarError> {
81        let mut chars = name.chars();
82        let valid = chars.next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
83            && chars.all(|c| c.is_ascii_alphanumeric() || c == '_');
84        match (valid, name.as_str()) {
85            (false, _) => Err(VarError::Name { name }),
86            (true, RESERVED) => Err(VarError::Reserved),
87            (true, _) => Ok(Self(name.into_boxed_str())),
88        }
89    }
90}
91
92/// What the layers say about one variable, merged.
93#[derive(Default)]
94struct Declared<'a> {
95    /// The first prompt given.
96    prompt: Option<&'a str>,
97    /// The default all layers agree on.
98    default: Offer<'a>,
99}
100
101/// A default, as the layers offer it.
102#[derive(Clone, Copy, Default, PartialEq, Eq)]
103enum Offer<'a> {
104    /// No layer offers one.
105    #[default]
106    None,
107    /// Every layer that offers one offers this.
108    Agreed(&'a str),
109    /// Layers disagree; the target must answer.
110    Disputed,
111}
112
113/// The target's answer to every variable the layers declare.
114///
115/// # Errors
116/// - [`VarError::Unknown`], the target answers a variable no layer declares.
117/// - [`VarError::Unanswered`], with every question, any is unanswered.
118pub(crate) fn answers(layers: &[Layer], target: &Target) -> Result<BTreeMap<VarName, String>> {
119    let mut declared: BTreeMap<&VarName, Declared<'_>> = BTreeMap::new();
120    for (name, spec) in layers.iter().flat_map(Layer::vars) {
121        let entry = declared.entry(name).or_default();
122        entry.prompt = entry.prompt.or(spec.prompt.as_deref());
123        entry.default = match (entry.default, spec.default.as_deref()) {
124            (offer, None) => offer,
125            (Offer::None, Some(default)) => Offer::Agreed(default),
126            (Offer::Agreed(first), Some(default)) if first == default => Offer::Agreed(first),
127            (Offer::Agreed(_) | Offer::Disputed, Some(_)) => Offer::Disputed,
128        };
129    }
130    if let Some(name) = target.given().find(|name| !declared.contains_key(name)) {
131        let declared = declared.keys().map(|&name| name.clone()).collect();
132        return Err(VarError::Unknown { name: name.clone(), declared }.into());
133    }
134    let mut answers = BTreeMap::new();
135    let mut questions = Vec::new();
136    for (name, Declared { prompt, default }) in declared {
137        match target.answers().get(name) {
138            Some(answer) => {
139                answers.insert(name.clone(), answer.clone());
140            },
141            None => questions.push(Question {
142                name: name.clone(),
143                prompt: prompt.map_or_else(|| name.to_string(), str::to_owned),
144                default: match default {
145                    Offer::Agreed(default) => Some(default.to_owned()),
146                    Offer::None | Offer::Disputed => None,
147                },
148            }),
149        }
150    }
151    if questions.is_empty() { Ok(answers) } else { Err(VarError::Unanswered { questions }.into()) }
152}
153
154/// The graph, as templates see it under `devset`.
155#[derive(Debug, Serialize)]
156pub(crate) struct Context {
157    /// The target directory's name.
158    target: String,
159    /// Every active profile's name.
160    profiles: Vec<String>,
161    /// Each layer, in the order they apply.
162    layers: Vec<LayerContext>,
163}
164
165/// A layer, as templates see it in `devset.layers`.
166#[derive(Debug, Serialize)]
167struct LayerContext {
168    /// Its profile's name.
169    profile: String,
170    /// Its source's name, or where it is for a source a profile requires by `git`.
171    source: String,
172    /// Its features that are on.
173    features: Vec<String>,
174    /// Whether the target configures it, rather than a profile requiring it.
175    configured: bool,
176}
177
178/// What one layer's templates see under `devset`: the graph, and the layer's own features.
179#[derive(Serialize)]
180struct Devset<'a> {
181    /// The target directory's name.
182    target: &'a str,
183    /// Every active profile's name.
184    profiles: &'a [String],
185    /// Each layer.
186    layers: &'a [LayerContext],
187    /// This layer's features that are on.
188    features: &'a [String],
189}
190
191impl Context {
192    /// The graph of `layers`, applied to `target`.
193    pub(crate) fn of(target: &Target, layers: &[Layer]) -> Self {
194        let layers: Vec<LayerContext> = layers
195            .iter()
196            .map(|layer| LayerContext {
197                profile: layer.name().to_string(),
198                source: layer
199                    .source_name()
200                    .map_or_else(|| layer.source().to_string(), ToString::to_string),
201                features: layer.features().keys().map(ToString::to_string).collect(),
202                configured: layer.configured(),
203            })
204            .collect();
205        Self {
206            target: target.root().file_name().unwrap_or_default().to_owned(),
207            profiles: layers.iter().map(|layer| layer.profile.clone()).collect(),
208            layers,
209        }
210    }
211
212    /// What `layer`'s templates see: `answers`, and `devset`.
213    fn vars(&self, layer: &Layer, answers: &BTreeMap<VarName, String>) -> Value {
214        let empty = Vec::new();
215        let features = self
216            .layers
217            .iter()
218            .find(|l| *layer.name() == *l.profile)
219            .map_or(&empty, |l| &l.features);
220        let devset = Devset {
221            target: &self.target,
222            profiles: &self.profiles,
223            layers: &self.layers,
224            features,
225        };
226        let mut vars: BTreeMap<&str, Value> = answers
227            .iter()
228            .map(|(name, answer)| (name.as_str(), Value::from(answer.as_str())))
229            .collect();
230        vars.insert(RESERVED, Value::from_serialize(&devset));
231        Value::from_serialize(&vars)
232    }
233}
234
235/// Renders templates for one layer: its answers and `devset` in scope, a variable nothing
236/// declares refused.
237struct Renderer<'a> {
238    /// The environment: strict, unescaped, trailing newlines kept.
239    env: Environment<'static>,
240    /// What the templates see.
241    vars: Value,
242    /// The names templates may use: the answers', `devset`, and the environment's globals.
243    known: HashSet<String>,
244    /// Every declared variable, for a message.
245    declared: &'a BTreeMap<VarName, String>,
246}
247
248impl<'a> Renderer<'a> {
249    /// A renderer for `layer`.
250    fn new(layer: &Layer, answers: &'a BTreeMap<VarName, String>, context: &Context) -> Self {
251        let mut env = Environment::new();
252        env.set_undefined_behavior(UndefinedBehavior::Strict);
253        env.set_keep_trailing_newline(true);
254        env.set_auto_escape_callback(|_| AutoEscape::None);
255        let mut known: HashSet<String> = env.globals().map(|(name, _)| name.to_owned()).collect();
256        known.extend(answers.keys().map(ToString::to_string));
257        known.insert(RESERVED.to_owned());
258        Self { env, vars: context.vars(layer, answers), known, declared: answers }
259    }
260
261    /// `source`, a template named `name` in `file`, rendered.
262    ///
263    /// # Errors
264    /// - [`VarError::Undeclared`], it uses a variable no profile declares.
265    /// - [`Error::Parse`](crate::Error::Parse), it does not parse or render, pointing at where.
266    fn render(&self, name: &RelPath, source: &str, file: &str) -> Result<String> {
267        let failed = |e: &minijinja::Error| {
268            let message = e
269                .detail()
270                .map_or_else(|| e.kind().to_string(), |detail| format!("{}: {detail}", e.kind()));
271            ParseError { file: file.to_owned(), text: source.to_owned(), span: e.range(), message }
272        };
273        let template =
274            self.env.template_from_named_str(name.as_str(), source).map_err(|e| failed(&e))?;
275        let undeclared = template
276            .undeclared_variables(false)
277            .into_iter()
278            .filter(|name| !self.known.contains(name))
279            .min();
280        if let Some(undeclared) = undeclared {
281            let declared = self.declared.keys().cloned().collect();
282            return Err(
283                VarError::Undeclared { path: name.clone(), name: undeclared, declared }.into()
284            );
285        }
286        Ok(template.render(&self.vars).map_err(|e| failed(&e))?)
287    }
288
289    /// `written`, a path or a pattern in `file`, its variables answered; unchanged without any.
290    fn text(&self, written: &str, file: &str) -> Result<String> {
291        if !written.contains('{') {
292            return Ok(written.to_owned());
293        }
294        let name = RelPath::new(MANIFEST)?;
295        self.render(&name, written, file)
296    }
297}
298
299/// Renders each layer's paths, the paths and globs its gates name, and its scaffolds' sentinels.
300///
301/// # Errors
302/// - [`ProfileError::Manifest`], a path renders to one that is not a path in the target.
303/// - [`ProfileError::SamePath`], two of a layer's paths render to one.
304/// - [`ProfileError::Pattern`], a pattern renders to neither a path nor a glob.
305/// - As [`render`], a variable is undeclared, or a template does not parse.
306pub(crate) fn render_paths(
307    layers: &mut [Layer], answers: &BTreeMap<VarName, String>, context: &Context,
308) -> Result<()> {
309    for layer in layers {
310        let (specs, scaffolds) = layer.take_specs();
311        let templates = Renderer::new(layer, answers, context);
312        let file = layer.label(MANIFEST);
313        let pattern = |written: &str| -> Result<Pattern> {
314            let rendered = templates.text(written, &file)?;
315            Pattern::parse(&rendered).map_err(|reason| {
316                ProfileError::Pattern { profile: layer.name().clone(), pattern: rendered, reason }
317                    .into()
318            })
319        };
320        let mut files: BTreeMap<RelPath, LayerFile> = BTreeMap::new();
321        for (written, spec) in specs {
322            let rendered = templates.text(written.as_str(), &file)?;
323            let path = RelPath::new(&rendered).map_err(|e| ProfileError::Manifest {
324                file: file.clone(),
325                message: format!("[files.\"{written}\"] is {rendered:?} once answered, which is not a path in the target: {e}"),
326            })?;
327            let exists =
328                spec.when.exists.iter().map(|each| pattern(each)).collect::<Result<_>>()?;
329            let entry = LayerFile { written: written.clone(), spec, exists, gated: None };
330            if let Some(first) = files.insert(path.clone(), entry) {
331                let (profile, first) = (layer.name().clone(), first.written);
332                return Err(ProfileError::SamePath { profile, path, first, second: written }.into());
333            }
334        }
335        let scaffolds = scaffolds
336            .into_iter()
337            .map(|(name, spec)| {
338                Ok((name, spec.unless.0.iter().map(|each| pattern(each)).collect::<Result<_>>()?))
339            })
340            .collect::<Result<_>>()?;
341        layer.set_files(files, scaffolds);
342    }
343    Ok(())
344}
345
346/// Renders the payload of every file that applies, and every starter, with `answers` and the
347/// graph; digests each layer.
348///
349/// # Errors
350/// - [`VarError::Undeclared`], a template uses a variable no profile declares.
351/// - [`VarError::Template`], a template is not UTF-8.
352/// - [`Error::Parse`](crate::Error::Parse), a template does not parse or render, pointing at where.
353pub(crate) fn render(
354    layers: &mut [Layer], answers: &BTreeMap<VarName, String>, context: &Context,
355) -> Result<()> {
356    for layer in layers {
357        let templates = Renderer::new(layer, answers, context);
358        let (mut payload, mut starters) = (Tree::default(), Tree::default());
359        let bytes_of = |written: &RelPath, template: bool| -> Result<Vec<u8>> {
360            let bytes = layer.written().get(written).unwrap_or_default();
361            if !template {
362                return Ok(bytes.to_vec());
363            }
364            let source = str::from_utf8(bytes).map_err(|e| VarError::Template {
365                path: written.clone(),
366                reason: format!("it is not UTF-8: {e}"),
367            })?;
368            let file = layer.label(&format!("{PAYLOAD}/{written}"));
369            Ok(templates.render(written, source, &file)?.into_bytes())
370        };
371        for (path, file) in layer.files().iter().filter(|(_, file)| file.gated.is_none()) {
372            payload.put(path.clone(), &bytes_of(&file.written, file.spec.template)?);
373            if let Some(starter) = &file.spec.starter {
374                starters.put(path.clone(), &bytes_of(starter, file.spec.template)?);
375            }
376        }
377        layer.set_payload(payload, starters);
378    }
379    Ok(())
380}
381
382#[cfg(test)]
383mod tests {
384    use super::VarName;
385
386    #[test]
387    fn names_are_identifiers() {
388        for name in ["author", "_x", "target_cpu2"] {
389            assert!(VarName::try_from(name.to_owned()).is_ok(), "{name} is valid");
390        }
391        for name in ["", "2x", "a-b", "a.b", "é", "devset"] {
392            assert!(VarName::try_from(name.to_owned()).is_err(), "{name:?} is invalid");
393        }
394    }
395}