Skip to content

Commit 18071ef

Browse files
committed
Add Redis to existing app
Signed-off-by: itowlson <[email protected]>
1 parent 40496e3 commit 18071ef

File tree

7 files changed

+179
-7
lines changed

7 files changed

+179
-7
lines changed

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: 102 additions & 7 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,
@@ -96,6 +97,20 @@ 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>),
99114
}
100115

101116
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
@@ -241,9 +256,12 @@ impl Template {
241256
}
242257
}
243258

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

249267
pub(crate) fn parameters(
@@ -253,7 +271,7 @@ impl Template {
253271
let variant = self.variant(variant_kind).unwrap(); // TODO: for now
254272
self.parameters
255273
.iter()
256-
.filter(|p| !variant.skip_parameter(p))
274+
.filter(move |p| !variant.skip_parameter(p))
257275
}
258276

259277
pub(crate) fn parameter(&self, name: impl AsRef<str>) -> Option<&TemplateParameter> {
@@ -277,9 +295,9 @@ impl Template {
277295
self.variants.contains_key(&variant.kind())
278296
}
279297

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

285303
/// Creates a runner for the template, governed by the given options. Call
@@ -355,6 +373,29 @@ impl Template {
355373
skip_files: raw.skip_files.unwrap_or_default(),
356374
skip_parameters: raw.skip_parameters.unwrap_or_default(),
357375
snippets: raw.snippets.unwrap_or_default(),
376+
conditions: raw
377+
.conditions
378+
.unwrap_or_default()
379+
.into_values()
380+
.map(Self::parse_conditional)
381+
.collect(),
382+
}
383+
}
384+
385+
fn parse_conditional(conditional: RawConditional) -> Conditional {
386+
Conditional {
387+
condition: Self::parse_condition(conditional.condition),
388+
skip_files: conditional.skip_files.unwrap_or_default(),
389+
skip_parameters: conditional.skip_parameters.unwrap_or_default(),
390+
skip_snippets: conditional.skip_snippets.unwrap_or_default(),
391+
}
392+
}
393+
394+
fn parse_condition(condition: RawCondition) -> Condition {
395+
match condition {
396+
RawCondition::ManifestEntryExists(path) => {
397+
Condition::ManifestEntryExists(path.split('.').map(|s| s.to_string()).collect_vec())
398+
}
358399
}
359400
}
360401

@@ -528,6 +569,43 @@ impl TemplateVariant {
528569
pub(crate) fn skip_parameter(&self, parameter: &TemplateParameter) -> bool {
529570
self.skip_parameters.iter().any(|p| &parameter.id == p)
530571
}
572+
573+
fn resolve_conditions(&self, variant_info: &TemplateVariantInfo) -> Self {
574+
let mut resolved = self.clone();
575+
for condition in &self.conditions {
576+
if condition.condition.is_true(variant_info) {
577+
resolved
578+
.skip_files
579+
.append(&mut condition.skip_files.clone());
580+
resolved
581+
.skip_parameters
582+
.append(&mut condition.skip_parameters.clone());
583+
resolved
584+
.snippets
585+
.retain(|id, _| !condition.skip_snippets.contains(id));
586+
}
587+
}
588+
resolved
589+
}
590+
}
591+
592+
impl Condition {
593+
fn is_true(&self, variant_info: &TemplateVariantInfo) -> bool {
594+
match self {
595+
Self::ManifestEntryExists(path) => match variant_info {
596+
TemplateVariantInfo::NewApplication => false,
597+
TemplateVariantInfo::AddComponent { manifest_path } => {
598+
let Ok(toml_text) = std::fs::read_to_string(manifest_path) else {
599+
return false;
600+
};
601+
let Ok(table) = toml::from_str::<toml::Value>(&toml_text) else {
602+
return false;
603+
};
604+
get_at(table, path).is_some()
605+
}
606+
},
607+
}
608+
}
531609
}
532610

533611
fn parse_string_constraints(raw: &RawParameter) -> anyhow::Result<StringConstraints> {
@@ -559,3 +637,20 @@ fn validate_v1_manifest(raw: &RawTemplateManifestV1) -> anyhow::Result<()> {
559637
}
560638
Ok(())
561639
}
640+
641+
fn get_at(value: toml::Value, path: &[String]) -> Option<toml::Value> {
642+
match path.split_first() {
643+
None => Some(value), // we are at the end of the path and we have a thing
644+
Some((first, rest)) => {
645+
match value.as_table() {
646+
None => None, // we need to key into it and we can't
647+
Some(t) => {
648+
match t.get(first) {
649+
None => None, // we tried to key into it and no match
650+
Some(v) => get_at(v.clone(), rest), // we pathed into it! keep pathing
651+
}
652+
}
653+
}
654+
}
655+
}
656+
}

templates/redis-rust/content/spin.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ source = "target/wasm32-wasi/release/{{project-name | snake_case}}.wasm"
1818
allowed_outbound_hosts = []
1919
[component.{{project-name | kebab_case}}.build]
2020
command = "cargo build --target wasm32-wasi --release"
21+
watch = ["src/**/*.rs", "Cargo.toml"]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[application.trigger.redis]
2+
address = "{{redis-address}}"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[[trigger.redis]]
2+
channel = "{{redis-channel}}"
3+
component = "{{project-name | kebab_case}}"
4+
5+
[component.{{project-name | kebab_case}}]
6+
source = "{{ output-path }}/target/wasm32-wasi/release/{{project-name | snake_case}}.wasm"
7+
allowed_outbound_hosts = []
8+
[component.{{project-name | kebab_case}}.build]
9+
command = "cargo build --target wasm32-wasi --release"
10+
workdir = "{{ output-path }}"
11+
watch = ["src/**/*.rs", "Cargo.toml"]

templates/redis-rust/metadata/spin-template.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@ id = "redis-rust"
33
description = "Redis message handler using Rust"
44
tags = ["redis", "rust"]
55

6+
[add_component]
7+
skip_files = ["spin.toml"]
8+
[add_component.snippets]
9+
component = "component.txt"
10+
application_trigger = "application-trigger.txt"
11+
[add_component.conditions.address_exists]
12+
condition = { manifest_entry_exists = "application.trigger.redis" }
13+
skip_parameters = ["redis-address"]
14+
skip_snippets = ["application_trigger"]
15+
616
[parameters]
717
project-description = { type = "string", prompt = "Description", default = "" }
818
redis-address = { type = "string", prompt = "Redis address", default = "redis://localhost:6379" }

0 commit comments

Comments
 (0)