spin_templates/
renderer.rs

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