spin_templates/
app_info.rs1use std::{
6 collections::HashSet,
7 path::{Path, PathBuf},
8};
9
10use anyhow::ensure;
11use serde::Deserialize;
12use spin_manifest::schema::v1;
13
14use crate::store::TemplateLayout;
15
16pub(crate) struct AppInfo {
17 manifest_format: u32,
18 trigger_type: Option<String>, }
20
21impl AppInfo {
22 pub fn from_layout(layout: &TemplateLayout) -> Option<anyhow::Result<AppInfo>> {
23 Self::layout_manifest_path(layout)
24 .map(|manifest_path| Self::from_existent_template(&manifest_path))
25 }
26
27 pub fn from_file(manifest_path: &Path) -> Option<anyhow::Result<AppInfo>> {
28 if manifest_path.exists() {
29 Some(Self::from_existent_file(manifest_path))
30 } else {
31 None
32 }
33 }
34
35 fn layout_manifest_path(layout: &TemplateLayout) -> Option<PathBuf> {
36 let manifest_path = layout.content_dir().join("spin.toml");
37 if manifest_path.exists() {
38 Some(manifest_path)
39 } else {
40 None
41 }
42 }
43
44 fn from_existent_file(manifest_path: &Path) -> anyhow::Result<Self> {
45 let manifest_str = std::fs::read_to_string(manifest_path)?;
46 Self::from_manifest_text(&manifest_str)
47 }
48
49 fn from_manifest_text(manifest_str: &str) -> anyhow::Result<Self> {
50 let manifest_version = spin_manifest::ManifestVersion::detect(manifest_str)?;
51 let manifest_format = match manifest_version {
52 spin_manifest::ManifestVersion::V1 => 1,
53 spin_manifest::ManifestVersion::V2 => 2,
54 };
55 let trigger_type = match manifest_version {
56 spin_manifest::ManifestVersion::V1 => Some(
57 toml::from_str::<ManifestV1TriggerProbe>(manifest_str)?
58 .trigger
59 .trigger_type,
60 ),
61 spin_manifest::ManifestVersion::V2 => {
62 let triggers = toml::from_str::<ManifestV2TriggerProbe>(manifest_str)?
63 .trigger
64 .unwrap_or_default();
65 let type_count = triggers.len();
66 ensure!(
67 type_count <= 1,
68 "only 1 trigger type currently supported; got {type_count}"
69 );
70 triggers.into_iter().next().map(|t| t.0)
71 }
72 };
73 Ok(Self {
74 manifest_format,
75 trigger_type,
76 })
77 }
78
79 fn from_existent_template(manifest_path: &Path) -> anyhow::Result<Self> {
80 let read_to_string = std::fs::read_to_string(manifest_path)?;
84 let manifest_tpl_str = read_to_string;
85
86 Self::from_template_text(&manifest_tpl_str)
87 }
88
89 fn from_template_text(manifest_tpl_str: &str) -> anyhow::Result<Self> {
90 let is_v1_tpl = manifest_tpl_str.contains("spin_manifest_version = \"1\"");
92 let is_v2_tpl = manifest_tpl_str.contains("spin_manifest_version = 2");
93 if is_v1_tpl {
94 return Self::from_manifest_text(manifest_tpl_str);
96 }
97 if !is_v2_tpl {
98 anyhow::bail!("Unsure of template manifest version");
100 }
101
102 Self::from_v2_template_text(manifest_tpl_str)
103 }
104
105 fn from_v2_template_text(manifest_tpl_str: &str) -> anyhow::Result<Self> {
106 let trigger_types: HashSet<_> = manifest_tpl_str
107 .lines()
108 .filter_map(infer_trigger_type_from_raw_line)
109 .collect();
110 let type_count = trigger_types.len();
111 ensure!(
112 type_count <= 1,
113 "only 1 trigger type currently supported; got {type_count}"
114 );
115 let trigger_type = trigger_types.into_iter().next();
116
117 Ok(Self {
118 manifest_format: 2,
119 trigger_type,
120 })
121 }
122
123 pub fn manifest_format(&self) -> u32 {
124 self.manifest_format
125 }
126
127 pub fn trigger_type(&self) -> Option<&str> {
128 self.trigger_type.as_deref()
129 }
130}
131
132lazy_static::lazy_static! {
133 static ref EXTRACT_TRIGGER: regex::Regex =
134 regex::Regex::new(r"^\s*\[\[trigger\.(?<trigger>[a-zA-Z0-9-]+)").expect("Invalid unknown filter regex");
135}
136
137fn infer_trigger_type_from_raw_line(line: &str) -> Option<String> {
138 EXTRACT_TRIGGER
139 .captures(line)
140 .map(|c| c["trigger"].to_owned())
141}
142
143#[derive(Deserialize)]
144struct ManifestV1TriggerProbe {
145 trigger: v1::AppTriggerV1,
147}
148
149#[derive(Deserialize)]
150struct ManifestV2TriggerProbe {
151 trigger: Option<toml::value::Table>,
153}
154
155#[cfg(test)]
156mod test {
157 use super::*;
158
159 #[test]
160 fn can_extract_triggers() {
161 assert_eq!(
162 "http",
163 infer_trigger_type_from_raw_line("[[trigger.http]]").unwrap()
164 );
165 assert_eq!(
166 "http",
167 infer_trigger_type_from_raw_line(" [[trigger.http]]").unwrap()
168 );
169 assert_eq!(
170 "fie",
171 infer_trigger_type_from_raw_line(" [[trigger.fie]]").unwrap()
172 );
173 assert_eq!(
174 "x-y",
175 infer_trigger_type_from_raw_line(" [[trigger.x-y]]").unwrap()
176 );
177
178 assert_eq!(None, infer_trigger_type_from_raw_line("# [[trigger.http]]"));
179 assert_eq!(None, infer_trigger_type_from_raw_line("trigger. But,"));
180 assert_eq!(None, infer_trigger_type_from_raw_line("[[trigger. snerk"));
181 }
182
183 #[test]
184 fn can_read_app_info_from_template_v1() {
185 let tpl = r#"spin_manifest_version = "1"
186 name = "{{ thingy }}"
187 version = "1.2.3"
188 trigger = { type = "triggy", arg = "{{ another-thingy }}" }
189
190 [[component]]
191 id = "{{ thingy | kebab_case }}"
192 source = "path/to/{{ thingy | snake_case }}.wasm"
193 [component.trigger]
194 spork = "{{ utensil }}"
195 "#;
196
197 let info = AppInfo::from_template_text(tpl).unwrap();
198 assert_eq!(1, info.manifest_format);
199 assert_eq!("triggy", info.trigger_type.unwrap());
200 }
201
202 #[test]
203 fn can_read_app_info_from_template_v2() {
204 let tpl = r#"spin_manifest_version = 2
205 name = "{{ thingy }}"
206 version = "1.2.3"
207
208 [application.trigger.triggy]
209 arg = "{{ another-thingy }}"
210
211 [[trigger.triggy]]
212 spork = "{{ utensil }}"
213 component = "{{ thingy | kebab_case }}"
214
215 [component.{{ thingy | kebab_case }}]
216 source = "path/to/{{ thingy | snake_case }}.wasm"
217 "#;
218
219 let info = AppInfo::from_template_text(tpl).unwrap();
220 assert_eq!(2, info.manifest_format);
221 assert_eq!("triggy", info.trigger_type.unwrap());
222 }
223
224 #[test]
225 fn can_read_app_info_from_triggerless_template_v2() {
226 let tpl = r#"spin_manifest_version = 2
227 name = "{{ thingy }}"
228 version = "1.2.3"
229 "#;
230
231 let info = AppInfo::from_template_text(tpl).unwrap();
232 assert_eq!(2, info.manifest_format);
233 assert_eq!(None, info.trigger_type);
234 }
235}