spin_templates/
renderer.rs1use anyhow::anyhow;
2use std::{collections::HashMap, path::PathBuf, sync::LazyLock};
3
4use crate::writer::{TemplateOutput, TemplateOutputs};
5
6pub(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), 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); 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 Err(e) => Ok(TemplateContent::Binary(e.into_bytes())), 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 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 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}