Skip to content

Commit 5a449eb

Browse files
authored
Merge pull request #2446 from itowlson/templates-redis-allow-add
Add Redis components to existing app
2 parents b4e3369 + 1a0285e commit 5a449eb

File tree

9 files changed

+366
-8
lines changed

9 files changed

+366
-8
lines changed

crates/templates/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod run;
1717
mod source;
1818
mod store;
1919
mod template;
20+
mod toml;
2021
mod writer;
2122

2223
pub use manager::*;

crates/templates/src/reader.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,48 @@ pub(crate) struct RawTemplateVariant {
3333
pub skip_files: Option<Vec<String>>,
3434
pub skip_parameters: Option<Vec<String>>,
3535
pub snippets: Option<HashMap<String, String>>,
36+
pub conditions: Option<HashMap<String, RawConditional>>,
37+
}
38+
39+
#[derive(Debug, Deserialize)]
40+
#[serde(deny_unknown_fields, rename_all = "snake_case")]
41+
pub(crate) struct RawConditional {
42+
pub condition: RawCondition,
43+
pub skip_files: Option<Vec<String>>,
44+
pub skip_parameters: Option<Vec<String>>,
45+
pub skip_snippets: Option<Vec<String>>,
46+
}
47+
48+
#[derive(Debug, Deserialize)]
49+
#[serde(
50+
deny_unknown_fields,
51+
rename_all = "snake_case",
52+
try_from = "toml::Value"
53+
)]
54+
pub(crate) enum RawCondition {
55+
ManifestEntryExists(String),
56+
}
57+
58+
impl TryFrom<toml::Value> for RawCondition {
59+
type Error = anyhow::Error;
60+
61+
fn try_from(value: toml::Value) -> Result<Self, Self::Error> {
62+
let Some(table) = value.as_table() else {
63+
anyhow::bail!("Invalid condition: should be a single-entry table");
64+
};
65+
if table.keys().len() != 1 {
66+
anyhow::bail!("Invalid condition: should be a single-entry table");
67+
}
68+
let Some(value) = table.get("manifest_entry_exists") else {
69+
anyhow::bail!("Invalid condition: unknown condition type");
70+
};
71+
let Some(path) = value.as_str() else {
72+
anyhow::bail!(
73+
"Invalid condition: 'manifest_entry_exists' should be a dotted-path string"
74+
);
75+
};
76+
Ok(Self::ManifestEntryExists(path.to_owned()))
77+
}
3678
}
3779

3880
#[derive(Debug, Deserialize)]

crates/templates/src/run.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,17 @@ impl Run {
281281
Err(anyhow::anyhow!("Spin doesn't know what to do with a 'component' snippet outside an 'add component' operation")),
282282
}
283283
},
284+
"application_trigger" => {
285+
match &self.options.variant {
286+
TemplateVariantInfo::AddComponent { manifest_path } =>
287+
Ok(RenderOperation::AppendToml(
288+
manifest_path.clone(),
289+
content,
290+
)),
291+
TemplateVariantInfo::NewApplication =>
292+
Err(anyhow::anyhow!("Spin doesn't know what to do with an 'application_trigger' snippet outside an 'add component' operation")),
293+
}
294+
},
284295
"variables" => {
285296
match &self.options.variant {
286297
TemplateVariantInfo::AddComponent { manifest_path } =>

crates/templates/src/template.rs

Lines changed: 215 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ use std::{
55

66
use anyhow::{anyhow, Context};
77
use indexmap::IndexMap;
8+
use itertools::Itertools;
89
use regex::Regex;
910

1011
use crate::{
1112
constraints::StringConstraints,
1213
reader::{
13-
RawExtraOutput, RawParameter, RawTemplateManifest, RawTemplateManifestV1,
14-
RawTemplateVariant,
14+
RawCondition, RawConditional, RawExtraOutput, RawParameter, RawTemplateManifest,
15+
RawTemplateManifestV1, RawTemplateVariant,
1516
},
1617
run::{Run, RunOptions},
1718
store::TemplateLayout,
@@ -46,7 +47,7 @@ enum TemplateVariantKind {
4647
}
4748

4849
/// The variant mode in which a template should be run.
49-
#[derive(Clone, Debug, Eq, PartialEq)]
50+
#[derive(Clone, Debug)]
5051
pub enum TemplateVariantInfo {
5152
/// Create a new application from the template.
5253
NewApplication,
@@ -96,6 +97,22 @@ pub(crate) struct TemplateVariant {
9697
skip_files: Vec<String>,
9798
skip_parameters: Vec<String>,
9899
snippets: HashMap<String, String>,
100+
conditions: Vec<Conditional>,
101+
}
102+
103+
#[derive(Clone, Debug)]
104+
pub(crate) struct Conditional {
105+
condition: Condition,
106+
skip_files: Vec<String>,
107+
skip_parameters: Vec<String>,
108+
skip_snippets: Vec<String>,
109+
}
110+
111+
#[derive(Clone, Debug)]
112+
pub(crate) enum Condition {
113+
ManifestEntryExists(Vec<String>),
114+
#[cfg(test)]
115+
Always(bool),
99116
}
100117

101118
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
@@ -241,9 +258,12 @@ impl Template {
241258
}
242259
}
243260

244-
fn variant(&self, variant_info: &TemplateVariantInfo) -> Option<&TemplateVariant> {
261+
// TODO: we should resolve this once at the start of Run and then use that forever
262+
fn variant(&self, variant_info: &TemplateVariantInfo) -> Option<TemplateVariant> {
245263
let kind = variant_info.kind();
246-
self.variants.get(&kind)
264+
self.variants
265+
.get(&kind)
266+
.map(|vt| vt.resolve_conditions(variant_info))
247267
}
248268

249269
pub(crate) fn parameters(
@@ -253,7 +273,7 @@ impl Template {
253273
let variant = self.variant(variant_kind).unwrap(); // TODO: for now
254274
self.parameters
255275
.iter()
256-
.filter(|p| !variant.skip_parameter(p))
276+
.filter(move |p| !variant.skip_parameter(p))
257277
}
258278

259279
pub(crate) fn parameter(&self, name: impl AsRef<str>) -> Option<&TemplateParameter> {
@@ -277,9 +297,9 @@ impl Template {
277297
self.variants.contains_key(&variant.kind())
278298
}
279299

280-
pub(crate) fn snippets(&self, variant_kind: &TemplateVariantInfo) -> &HashMap<String, String> {
300+
pub(crate) fn snippets(&self, variant_kind: &TemplateVariantInfo) -> HashMap<String, String> {
281301
let variant = self.variant(variant_kind).unwrap(); // TODO: for now
282-
&variant.snippets
302+
variant.snippets
283303
}
284304

285305
/// Creates a runner for the template, governed by the given options. Call
@@ -355,6 +375,29 @@ impl Template {
355375
skip_files: raw.skip_files.unwrap_or_default(),
356376
skip_parameters: raw.skip_parameters.unwrap_or_default(),
357377
snippets: raw.snippets.unwrap_or_default(),
378+
conditions: raw
379+
.conditions
380+
.unwrap_or_default()
381+
.into_values()
382+
.map(Self::parse_conditional)
383+
.collect(),
384+
}
385+
}
386+
387+
fn parse_conditional(conditional: RawConditional) -> Conditional {
388+
Conditional {
389+
condition: Self::parse_condition(conditional.condition),
390+
skip_files: conditional.skip_files.unwrap_or_default(),
391+
skip_parameters: conditional.skip_parameters.unwrap_or_default(),
392+
skip_snippets: conditional.skip_snippets.unwrap_or_default(),
393+
}
394+
}
395+
396+
fn parse_condition(condition: RawCondition) -> Condition {
397+
match condition {
398+
RawCondition::ManifestEntryExists(path) => {
399+
Condition::ManifestEntryExists(path.split('.').map(|s| s.to_string()).collect_vec())
400+
}
358401
}
359402
}
360403

@@ -528,6 +571,45 @@ impl TemplateVariant {
528571
pub(crate) fn skip_parameter(&self, parameter: &TemplateParameter) -> bool {
529572
self.skip_parameters.iter().any(|p| &parameter.id == p)
530573
}
574+
575+
fn resolve_conditions(&self, variant_info: &TemplateVariantInfo) -> Self {
576+
let mut resolved = self.clone();
577+
for condition in &self.conditions {
578+
if condition.condition.is_true(variant_info) {
579+
resolved
580+
.skip_files
581+
.append(&mut condition.skip_files.clone());
582+
resolved
583+
.skip_parameters
584+
.append(&mut condition.skip_parameters.clone());
585+
resolved
586+
.snippets
587+
.retain(|id, _| !condition.skip_snippets.contains(id));
588+
}
589+
}
590+
resolved
591+
}
592+
}
593+
594+
impl Condition {
595+
fn is_true(&self, variant_info: &TemplateVariantInfo) -> bool {
596+
match self {
597+
Self::ManifestEntryExists(path) => match variant_info {
598+
TemplateVariantInfo::NewApplication => false,
599+
TemplateVariantInfo::AddComponent { manifest_path } => {
600+
let Ok(toml_text) = std::fs::read_to_string(manifest_path) else {
601+
return false;
602+
};
603+
let Ok(table) = toml::from_str::<toml::Value>(&toml_text) else {
604+
return false;
605+
};
606+
crate::toml::get_at(table, path).is_some()
607+
}
608+
},
609+
#[cfg(test)]
610+
Self::Always(b) => *b,
611+
}
612+
}
531613
}
532614

533615
fn parse_string_constraints(raw: &RawParameter) -> anyhow::Result<StringConstraints> {
@@ -559,3 +641,128 @@ fn validate_v1_manifest(raw: &RawTemplateManifestV1) -> anyhow::Result<()> {
559641
}
560642
Ok(())
561643
}
644+
645+
#[cfg(test)]
646+
mod test {
647+
use super::*;
648+
649+
struct TempFile(tempfile::TempDir, PathBuf);
650+
651+
impl TempFile {
652+
fn path(&self) -> PathBuf {
653+
self.1.clone()
654+
}
655+
}
656+
657+
fn make_temp_manifest(content: &str) -> TempFile {
658+
let temp_dir = tempfile::tempdir().unwrap();
659+
let temp_file = temp_dir.path().join("spin.toml");
660+
std::fs::write(&temp_file, content).unwrap();
661+
TempFile(temp_dir, temp_file)
662+
}
663+
664+
#[test]
665+
fn manifest_entry_exists_condition_is_false_for_new_app() {
666+
let condition = Template::parse_condition(RawCondition::ManifestEntryExists(
667+
"application.trigger.redis".to_owned(),
668+
));
669+
assert!(!condition.is_true(&TemplateVariantInfo::NewApplication));
670+
}
671+
672+
#[test]
673+
fn manifest_entry_exists_condition_is_false_if_not_present_in_existing_manifest() {
674+
let temp_file =
675+
make_temp_manifest("name = \"hello\"\n[application.trigger.http]\nbase = \"/\"");
676+
let condition = Template::parse_condition(RawCondition::ManifestEntryExists(
677+
"application.trigger.redis".to_owned(),
678+
));
679+
assert!(!condition.is_true(&TemplateVariantInfo::AddComponent {
680+
manifest_path: temp_file.path()
681+
}));
682+
}
683+
684+
#[test]
685+
fn manifest_entry_exists_condition_is_true_if_present_in_existing_manifest() {
686+
let temp_file = make_temp_manifest(
687+
"name = \"hello\"\n[application.trigger.redis]\nchannel = \"HELLO\"",
688+
);
689+
let condition = Template::parse_condition(RawCondition::ManifestEntryExists(
690+
"application.trigger.redis".to_owned(),
691+
));
692+
assert!(condition.is_true(&TemplateVariantInfo::AddComponent {
693+
manifest_path: temp_file.path()
694+
}));
695+
}
696+
697+
#[test]
698+
fn manifest_entry_exists_condition_is_false_if_path_does_not_exist() {
699+
let condition = Template::parse_condition(RawCondition::ManifestEntryExists(
700+
"application.trigger.redis".to_owned(),
701+
));
702+
assert!(!condition.is_true(&TemplateVariantInfo::AddComponent {
703+
manifest_path: PathBuf::from("this/file/does/not.exist")
704+
}));
705+
}
706+
707+
#[test]
708+
fn selected_variant_respects_target() {
709+
let add_component_vt = TemplateVariant {
710+
conditions: vec![Conditional {
711+
condition: Condition::Always(true),
712+
skip_files: vec!["test2".to_owned()],
713+
skip_parameters: vec!["p1".to_owned()],
714+
skip_snippets: vec!["s1".to_owned()],
715+
}],
716+
skip_files: vec!["test1".to_owned()],
717+
snippets: [
718+
("s1".to_owned(), "s1val".to_owned()),
719+
("s2".to_owned(), "s2val".to_owned()),
720+
]
721+
.into_iter()
722+
.collect(),
723+
..Default::default()
724+
};
725+
let variants = [
726+
(
727+
TemplateVariantKind::NewApplication,
728+
TemplateVariant::default(),
729+
),
730+
(TemplateVariantKind::AddComponent, add_component_vt),
731+
]
732+
.into_iter()
733+
.collect();
734+
let template = Template {
735+
id: "test".to_owned(),
736+
tags: HashSet::new(),
737+
description: None,
738+
installed_from: InstalledFrom::Unknown,
739+
trigger: TemplateTriggerCompatibility::Any,
740+
variants,
741+
parameters: vec![],
742+
extra_outputs: vec![],
743+
snippets_dir: None,
744+
content_dir: None,
745+
};
746+
747+
let variant_info = TemplateVariantInfo::NewApplication;
748+
let variant = template.variant(&variant_info).unwrap();
749+
assert!(variant.skip_files.is_empty());
750+
assert!(variant.skip_parameters.is_empty());
751+
assert!(variant.snippets.is_empty());
752+
753+
let add_variant_info = TemplateVariantInfo::AddComponent {
754+
manifest_path: PathBuf::from("dummy"),
755+
};
756+
let add_variant = template.variant(&add_variant_info).unwrap();
757+
// the conditional skip_files and skip_parameters are added to the variant's skip lists
758+
assert_eq!(2, add_variant.skip_files.len());
759+
assert!(add_variant.skip_files.contains(&"test1".to_owned()));
760+
assert!(add_variant.skip_files.contains(&"test2".to_owned()));
761+
assert_eq!(1, add_variant.skip_parameters.len());
762+
assert!(add_variant.skip_parameters.contains(&"p1".to_owned()));
763+
// the conditional skip_snippets are *removed from* the variant's snippets list
764+
assert_eq!(1, add_variant.snippets.len());
765+
assert!(!add_variant.snippets.contains_key("s1"));
766+
assert!(add_variant.snippets.contains_key("s2"));
767+
}
768+
}

0 commit comments

Comments
 (0)