Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 57 additions & 25 deletions crates/templates/src/app_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ use std::{
path::{Path, PathBuf},
};

use anyhow::ensure;
use serde::Deserialize;
use spin_manifest::schema::v1;

use crate::store::TemplateLayout;

pub(crate) struct AppInfo {
manifest_format: u32,
trigger_type: Option<String>, // 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<Vec<String>>,
}

impl AppInfo {
Expand Down Expand Up @@ -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::<ManifestV1TriggerProbe>(manifest_str)?
.trigger
.trigger_type,
),
]),
// v2 manifests may declare multiple trigger types.
spin_manifest::ManifestVersion::V2 => {
let triggers = toml::from_str::<ManifestV2TriggerProbe>(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,
})
}

Expand Down Expand Up @@ -103,29 +107,31 @@ impl AppInfo {
}

fn from_v2_template_text(manifest_tpl_str: &str) -> anyhow::Result<Self> {
let trigger_types: HashSet<_> = manifest_tpl_str
// Preserve declaration order while removing duplicate trigger types.
let mut seen = HashSet::new();
let trigger_types: Vec<String> = 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,
})
}

pub fn manifest_format(&self) -> u32 {
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()
}
}

Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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);
}
}
2 changes: 1 addition & 1 deletion crates/templates/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
7 changes: 6 additions & 1 deletion crates/templates/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/templates/src/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
},
Expand Down
2 changes: 2 additions & 0 deletions templates/http-middleware/content/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
.spin/
16 changes: 16 additions & 0 deletions templates/http-middleware/content/Cargo.toml.tmpl
Original file line number Diff line number Diff line change
@@ -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]
14 changes: 14 additions & 0 deletions templates/http-middleware/content/spin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
spin_manifest_version = 2

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's not really a clear way to test middleware as a standalone spin app unless we revive (I think)spin test

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I briefly looked at a middleware template, I assumed it would have to be add-only (not available in spin new). Even with spin test it's not clear how a standalone piece of middleware could work.


[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"]
17 changes: 17 additions & 0 deletions templates/http-middleware/content/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<Response> {
// 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)
}
10 changes: 10 additions & 0 deletions templates/http-middleware/metadata/snippets/component.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[[trigger.http]]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current templating system requires the add snippet to contain a trigger section which doesn't make sense for a middleware component (or any library component).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, weird. I think we used to check the trigger to make sure it was the same as existing triggers. I don't think that would be needed any more - let's see how the trigger info is being depended on and see if we can bin it off.

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"]
17 changes: 17 additions & 0 deletions templates/http-middleware/metadata/spin-template.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
manifest_version = "1"
id = "http-middleware"
description = "http middleware rust template"
tags = []

[new_application]
supported = false

[add_component]
skip_files = ["spin.toml"]
[add_component.snippets]
component = "component.txt"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding middleware to an existing app would require post add instructions including adding the middleware to dependencies under the correct trigger section.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be unavoidable - presumably the reason you're building your thing as middleware rather than including it in component source code is that you want to apply it to multiple triggers. (Although I guess another case is "my app is an off-the-shelf component like the fileserver but I want to wrap it." So maybe sometimes we do want to be able to auto-add it to triggers. But yeah, we don't have syntax for that yet.)


[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 = "" }
Loading