From e5667efda22ed76f2811cd779490e9e7dff0b99c Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Tue, 14 Jul 2026 17:34:42 -0400 Subject: [PATCH 1/2] WIP: add http middleware template Signed-off-by: Michelle Dhanani --- templates/http-middleware/content/.gitignore | 2 ++ .../http-middleware/content/Cargo.toml.tmpl | 16 ++++++++++++++++ templates/http-middleware/content/spin.toml | 14 ++++++++++++++ templates/http-middleware/content/src/lib.rs | 17 +++++++++++++++++ .../metadata/snippets/component.txt | 10 ++++++++++ .../http-middleware/metadata/spin-template.toml | 14 ++++++++++++++ 6 files changed, 73 insertions(+) create mode 100644 templates/http-middleware/content/.gitignore create mode 100644 templates/http-middleware/content/Cargo.toml.tmpl create mode 100644 templates/http-middleware/content/spin.toml create mode 100644 templates/http-middleware/content/src/lib.rs create mode 100644 templates/http-middleware/metadata/snippets/component.txt create mode 100644 templates/http-middleware/metadata/spin-template.toml diff --git a/templates/http-middleware/content/.gitignore b/templates/http-middleware/content/.gitignore new file mode 100644 index 0000000000..06df055bbf --- /dev/null +++ b/templates/http-middleware/content/.gitignore @@ -0,0 +1,2 @@ +target/ +.spin/ \ No newline at end of file diff --git a/templates/http-middleware/content/Cargo.toml.tmpl b/templates/http-middleware/content/Cargo.toml.tmpl new file mode 100644 index 0000000000..f2eed7b83a --- /dev/null +++ b/templates/http-middleware/content/Cargo.toml.tmpl @@ -0,0 +1,16 @@ +[package] +name = "{{project-name | kebab_case}}" +authors = ["{{authors}}"] +description = "{{project-description}}" +version = "0.1.0" +rust-version = "1.93" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1" +spin-sdk = "6.0.0" + +[workspace] \ No newline at end of file diff --git a/templates/http-middleware/content/spin.toml b/templates/http-middleware/content/spin.toml new file mode 100644 index 0000000000..83453e7d44 --- /dev/null +++ b/templates/http-middleware/content/spin.toml @@ -0,0 +1,14 @@ +spin_manifest_version = 2 + +[application] +name = "{{project-name | kebab_case}}" +version = "0.1.0" +authors = ["{{authors}}"] +description = "{{project-description}}" + +[component.{{project-name | kebab_case}}] +source = "target/wasm32-wasip2/release/{{project-name | snake_case}}.wasm" +allowed_outbound_hosts = [] +[component.{{project-name | kebab_case}}.build] +command = "cargo build --target wasm32-wasip2 --release" +watch = ["src/**/*.rs", "Cargo.toml"] \ No newline at end of file diff --git a/templates/http-middleware/content/src/lib.rs b/templates/http-middleware/content/src/lib.rs new file mode 100644 index 0000000000..1e1ff978b3 --- /dev/null +++ b/templates/http-middleware/content/src/lib.rs @@ -0,0 +1,17 @@ +use spin_sdk::http::{self, HeaderName, HeaderValue, Request, Response}; +use spin_sdk::http_service; + +#[http_service] +async fn handle(mut req: Request) -> http::Result { + // Request runs on the way in, before the next handler. + eprintln!("[middleware] --> {} {}", req.method(), req.uri().path()); + + // Forward the (modified) request to the next handler in the chain and wait + // for its response. + let mut resp = http::next(req).await?; + + // Response runs on the way out, after the next handler + eprintln!("[middleware] <-- {}", resp.status()); + + Ok(resp) +} \ No newline at end of file diff --git a/templates/http-middleware/metadata/snippets/component.txt b/templates/http-middleware/metadata/snippets/component.txt new file mode 100644 index 0000000000..d7f196a71d --- /dev/null +++ b/templates/http-middleware/metadata/snippets/component.txt @@ -0,0 +1,10 @@ +[[trigger.http]] +component = "{{project-name | kebab_case}}" + +[component.{{project-name | kebab_case}}] +source = "{{ output-path }}/target/wasm32-wasip2/release/{{project-name | snake_case}}.wasm" +allowed_outbound_hosts = [] +[component.{{project-name | kebab_case}}.build] +command = "cargo build --target wasm32-wasip2 --release" +workdir = "{{ output-path }}" +watch = ["src/**/*.rs", "Cargo.toml"] \ No newline at end of file diff --git a/templates/http-middleware/metadata/spin-template.toml b/templates/http-middleware/metadata/spin-template.toml new file mode 100644 index 0000000000..3ea0f35213 --- /dev/null +++ b/templates/http-middleware/metadata/spin-template.toml @@ -0,0 +1,14 @@ +manifest_version = "1" +id = "http-middleware" +description = "http middleware rust template" +tags = [] + +[add_component] +skip_files = ["spin.toml"] +[add_component.snippets] +component = "component.txt" + +[parameters] +# These are placehodlers for the template parameters +# You can add more parameters here, or remove them if you don't need them. +project-description = { type = "string", prompt = "Description", default = "" } \ No newline at end of file From d2f4c0ea0ecf4d56f1b001325055dacbd74a545c Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Tue, 21 Jul 2026 12:47:52 -0400 Subject: [PATCH 2/2] rm template section in add_component snippet + rm outdated trigger compatibility check for manifest v2 + support spin add for http-middleware template + rm support for spin new for http-middleware template Signed-off-by: Michelle Noorali --- crates/templates/src/app_info.rs | 82 +++++++++++++------ crates/templates/src/manager.rs | 2 +- crates/templates/src/run.rs | 7 +- crates/templates/src/template.rs | 2 +- .../metadata/spin-template.toml | 3 + 5 files changed, 68 insertions(+), 28 deletions(-) diff --git a/crates/templates/src/app_info.rs b/crates/templates/src/app_info.rs index e47059454f..92b2103ed3 100644 --- a/crates/templates/src/app_info.rs +++ b/crates/templates/src/app_info.rs @@ -7,7 +7,6 @@ use std::{ path::{Path, PathBuf}, }; -use anyhow::ensure; use serde::Deserialize; use spin_manifest::schema::v1; @@ -15,7 +14,11 @@ use crate::store::TemplateLayout; pub(crate) struct AppInfo { manifest_format: u32, - trigger_type: Option, // None = v2 template does not contain any triggers yet + // Used for v1 manifests, which have exactly one trigger type and are the + // only manifests subject to a compatibility check. v2 manifests may host + // multiple trigger types. Triggerless templates also allowed for all + // manifest versions. + trigger_types: Option>, } impl AppInfo { @@ -52,27 +55,28 @@ impl AppInfo { spin_manifest::ManifestVersion::V1 => 1, spin_manifest::ManifestVersion::V2 => 2, }; - let trigger_type = match manifest_version { - spin_manifest::ManifestVersion::V1 => Some( + let trigger_types = match manifest_version { + // v1 manifests always have exactly one trigger type. + spin_manifest::ManifestVersion::V1 => Some(vec![ toml::from_str::(manifest_str)? .trigger .trigger_type, - ), + ]), + // v2 manifests may declare multiple trigger types. spin_manifest::ManifestVersion::V2 => { let triggers = toml::from_str::(manifest_str)? .trigger .unwrap_or_default(); - let type_count = triggers.len(); - ensure!( - type_count <= 1, - "only 1 trigger type currently supported; got {type_count}" - ); - triggers.into_iter().next().map(|t| t.0) + if triggers.is_empty() { + None + } else { + Some(triggers.into_iter().map(|(trigger_type, _)| trigger_type).collect()) + } } }; Ok(Self { manifest_format, - trigger_type, + trigger_types, }) } @@ -103,20 +107,22 @@ impl AppInfo { } fn from_v2_template_text(manifest_tpl_str: &str) -> anyhow::Result { - let trigger_types: HashSet<_> = manifest_tpl_str + // Preserve declaration order while removing duplicate trigger types. + let mut seen = HashSet::new(); + let trigger_types: Vec = manifest_tpl_str .lines() .filter_map(infer_trigger_type_from_raw_line) + .filter(|trigger_type| seen.insert(trigger_type.clone())) .collect(); - let type_count = trigger_types.len(); - ensure!( - type_count <= 1, - "only 1 trigger type currently supported; got {type_count}" - ); - let trigger_type = trigger_types.into_iter().next(); + let trigger_types = if trigger_types.is_empty() { + None + } else { + Some(trigger_types) + }; Ok(Self { manifest_format: 2, - trigger_type, + trigger_types, }) } @@ -124,8 +130,8 @@ impl AppInfo { self.manifest_format } - pub fn trigger_type(&self) -> Option<&str> { - self.trigger_type.as_deref() + pub fn trigger_types(&self) -> Option<&[String]> { + self.trigger_types.as_deref() } } @@ -196,7 +202,7 @@ mod test { let info = AppInfo::from_template_text(tpl).unwrap(); assert_eq!(1, info.manifest_format); - assert_eq!("triggy", info.trigger_type.unwrap()); + assert_eq!(vec!["triggy".to_owned()], info.trigger_types.unwrap()); } #[test] @@ -218,7 +224,33 @@ mod test { let info = AppInfo::from_template_text(tpl).unwrap(); assert_eq!(2, info.manifest_format); - assert_eq!("triggy", info.trigger_type.unwrap()); + assert_eq!(vec!["triggy".to_owned()], info.trigger_types.unwrap()); + } + + #[test] + fn can_read_app_info_from_multi_trigger_template_v2() { + let tpl = r#"spin_manifest_version = 2 + name = "{{ thingy }}" + version = "1.2.3" + + [application.trigger.triggy] + arg = "{{ another-thingy }}" + + [[trigger.triggy]] + spork = "{{ utensil }}" + component = "{{ thingy | kebab_case }}" + + [[trigger.anothertrigger]] + spork = "{{ utensil }}" + component = "{{ thingy | kebab_case }}" + + [component.{{ thingy | kebab_case }}] + source = "path/to/{{ thingy | snake_case }}.wasm" + "#; + + let info = AppInfo::from_template_text(tpl).unwrap(); + assert_eq!(2, info.manifest_format); + assert_eq!(vec!["triggy".to_owned(), "anothertrigger".to_owned()], info.trigger_types.unwrap()); } #[test] @@ -230,6 +262,6 @@ mod test { let info = AppInfo::from_template_text(tpl).unwrap(); assert_eq!(2, info.manifest_format); - assert_eq!(None, info.trigger_type); + assert_eq!(None, info.trigger_types); } } diff --git a/crates/templates/src/manager.rs b/crates/templates/src/manager.rs index 39b0f5e7c8..92f06f4cee 100644 --- a/crates/templates/src/manager.rs +++ b/crates/templates/src/manager.rs @@ -615,7 +615,7 @@ mod tests { } } - const TPLS_IN_THIS: usize = 12; + const TPLS_IN_THIS: usize = 13; #[tokio::test] async fn can_install_into_new_directory() { diff --git a/crates/templates/src/run.rs b/crates/templates/src/run.rs index 7ddc47371f..9fb3cbd9c1 100644 --- a/crates/templates/src/run.rs +++ b/crates/templates/src/run.rs @@ -254,7 +254,12 @@ impl Run { match crate::app_info::AppInfo::from_file(manifest_path) { Some(Ok(app_info)) if app_info.manifest_format() == 1 => self .template - .check_compatible_trigger(app_info.trigger_type()), + .check_compatible_trigger( + app_info + .trigger_types() + .and_then(|types| types.first()) + .map(|trigger_type| trigger_type.as_str()), + ), _ => Ok(()), // Fail forgiving - don't block the user if things are under construction } } diff --git a/crates/templates/src/template.rs b/crates/templates/src/template.rs index cd0c10a8c5..a23ce8ca26 100644 --- a/crates/templates/src/template.rs +++ b/crates/templates/src/template.rs @@ -345,7 +345,7 @@ impl Template { fn infer_trigger_type(layout: &TemplateLayout) -> TemplateTriggerCompatibility { match crate::app_info::AppInfo::from_layout(layout) { - Some(Ok(app_info)) => match app_info.trigger_type() { + Some(Ok(app_info)) => match app_info.trigger_types().and_then(|types| types.first()) { None => TemplateTriggerCompatibility::Any, Some(t) => TemplateTriggerCompatibility::Only(t.to_owned()), }, diff --git a/templates/http-middleware/metadata/spin-template.toml b/templates/http-middleware/metadata/spin-template.toml index 3ea0f35213..c87e6d8075 100644 --- a/templates/http-middleware/metadata/spin-template.toml +++ b/templates/http-middleware/metadata/spin-template.toml @@ -3,6 +3,9 @@ id = "http-middleware" description = "http middleware rust template" tags = [] +[new_application] +supported = false + [add_component] skip_files = ["spin.toml"] [add_component.snippets]