Skip to main content

spin_templates/
renderer.rs

1use anyhow::anyhow;
2use std::{collections::HashMap, path::PathBuf, sync::LazyLock};
3
4use crate::writer::{TemplateOutput, TemplateOutputs};
5
6// A template that has been evaluated and parsed, with all the values
7// it needs to render.
8pub(crate) struct TemplateRenderer {
9    pub render_operations: Vec<RenderOperation>,
10    pub parameter_values: HashMap<String, String>,
11}
12
13pub(crate) enum TemplateContent {
14    Template(liquid::Template),
15    Binary(Vec<u8>),
16}
17
18pub(crate) enum RenderOperation {
19    AppendToml(PathBuf, TemplateContent),
20    MergeToml(PathBuf, MergeTarget, TemplateContent), // file to merge into, table to merge into, content to merge
21    WriteFile(PathBuf, TemplateContent),
22    CreateDirectory(PathBuf, std::sync::Arc<liquid::Template>),
23}
24
25pub(crate) enum MergeTarget {
26    Application(&'static str),
27}
28
29impl TemplateRenderer {
30    pub(crate) fn render(self) -> anyhow::Result<TemplateOutputs> {
31        let globals = self.renderer_globals();
32
33        let outputs = self
34            .render_operations
35            .into_iter()
36            .map(|so| so.render(&globals))
37            .collect::<anyhow::Result<Vec<_>>>()?;
38
39        if outputs.is_empty() {
40            return Err(anyhow!("Nothing to create"));
41        }
42
43        Ok(TemplateOutputs::new(outputs))
44    }
45
46    fn renderer_globals(&self) -> liquid::Object {
47        let mut object = liquid::Object::new();
48
49        for (k, v) in &self.parameter_values {
50            object.insert(
51                k.to_owned().into(),
52                liquid_core::Value::Scalar(v.to_owned().into()),
53            );
54        }
55
56        object
57    }
58}
59
60impl RenderOperation {
61    fn render(self, globals: &liquid::Object) -> anyhow::Result<TemplateOutput> {
62        match self {
63            Self::WriteFile(path, content) => {
64                let rendered = content.render(globals)?;
65                Ok(TemplateOutput::WriteFile(path, rendered))
66            }
67            Self::AppendToml(path, content) => {
68                let rendered = content.render(globals)?;
69                let rendered_text = String::from_utf8(rendered)?;
70                Ok(TemplateOutput::AppendToml(path, rendered_text))
71            }
72            Self::MergeToml(path, target, content) => {
73                let rendered = content.render(globals)?;
74                let rendered_text = String::from_utf8(rendered)?;
75                let MergeTarget::Application(target_table) = target;
76                Ok(TemplateOutput::MergeToml(path, target_table, rendered_text))
77            }
78            Self::CreateDirectory(path, template) => {
79                let rendered = template.render(globals)?;
80                let path = path.join(rendered); // TODO: should we validate that `rendered` was relative?`
81                Ok(TemplateOutput::CreateDirectory(path))
82            }
83        }
84    }
85}
86
87impl TemplateContent {
88    pub(crate) fn infer_from_bytes(
89        raw: Vec<u8>,
90        parser: &liquid::Parser,
91    ) -> anyhow::Result<TemplateContent> {
92        match String::from_utf8(raw) {
93            // `into_bytes` hands back the original bytes that failed UTF-8 validation
94            Err(e) => Ok(TemplateContent::Binary(e.into_bytes())), // TODO: try other encodings!
95            Ok(s) => {
96                match parser.parse(&s) {
97                    Ok(t) => Ok(TemplateContent::Template(t)),
98                    Err(e) => match understand_liquid_error(e) {
99                        TemplateParseFailure::Other(_e) => {
100                            // TODO: emit a warning?
101                            Ok(TemplateContent::Binary(s.into_bytes()))
102                        }
103                        TemplateParseFailure::UnknownFilter(id) => {
104                            Err(anyhow!("internal error in template: unknown filter '{id}'"))
105                        }
106                    },
107                }
108            }
109        }
110    }
111
112    fn render(self, globals: &liquid::Object) -> anyhow::Result<Vec<u8>> {
113        match self {
114            Self::Template(t) => {
115                let text = t.render(globals)?;
116                Ok(text.into_bytes())
117            }
118            Self::Binary(v) => Ok(v),
119        }
120    }
121}
122
123enum TemplateParseFailure {
124    UnknownFilter(String),
125    Other(liquid::Error),
126}
127
128static UNKNOWN_FILTER: LazyLock<regex::Regex> = LazyLock::new(|| {
129    regex::Regex::new("requested filter=(\\S+)").expect("Invalid unknown filter regex")
130});
131
132fn understand_liquid_error(e: liquid::Error) -> TemplateParseFailure {
133    let err_str = e.to_string();
134
135    // They should use typed errors like we, er, don't
136    match err_str.lines().next() {
137        None => TemplateParseFailure::Other(e),
138        Some("liquid: Unknown filter") => match UNKNOWN_FILTER.captures(&err_str) {
139            None => TemplateParseFailure::Other(e),
140            Some(captures) => match captures.get(1) {
141                None => TemplateParseFailure::Other(e),
142                Some(id) => TemplateParseFailure::UnknownFilter(id.as_str().to_owned()),
143            },
144        },
145        _ => TemplateParseFailure::Other(e),
146    }
147}