diff --git a/crates/templates/src/manager.rs b/crates/templates/src/manager.rs index 5accd15cce..53b82b0476 100644 --- a/crates/templates/src/manager.rs +++ b/crates/templates/src/manager.rs @@ -566,7 +566,7 @@ mod tests { } } - const TPLS_IN_THIS: usize = 11; + const TPLS_IN_THIS: usize = 12; #[tokio::test] async fn can_install_into_new_directory() { diff --git a/crates/templates/src/reader.rs b/crates/templates/src/reader.rs index 40b05a2a55..d1e59d3c34 100644 --- a/crates/templates/src/reader.rs +++ b/crates/templates/src/reader.rs @@ -30,12 +30,20 @@ pub(crate) struct RawTemplateManifestV1 { #[serde(deny_unknown_fields, rename_all = "snake_case")] pub(crate) struct RawTemplateVariant { pub supported: Option, + pub copy_into: Option, pub skip_files: Option>, pub skip_parameters: Option>, pub snippets: Option>, pub conditions: Option>, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum RawCopyInto { + Root, + Own, +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields, rename_all = "snake_case")] pub(crate) struct RawConditional { diff --git a/crates/templates/src/renderer.rs b/crates/templates/src/renderer.rs index faf0233ca3..fef41071c9 100644 --- a/crates/templates/src/renderer.rs +++ b/crates/templates/src/renderer.rs @@ -19,10 +19,24 @@ pub(crate) enum TemplateContent { pub(crate) enum RenderOperation { AppendToml(PathBuf, TemplateContent), MergeToml(PathBuf, MergeTarget, TemplateContent), // file to merge into, table to merge into, content to merge - WriteFile(PathBuf, TemplateContent), + WriteFile(TemplateablePath, TemplateContent), CreateDirectory(PathBuf, std::sync::Arc), } +pub(crate) enum TemplateablePath { + Plain(PathBuf), + Template(liquid::Template), +} + +impl TemplateablePath { + fn render(self, globals: &liquid::Object) -> anyhow::Result { + Ok(match self { + TemplateablePath::Plain(path) => path, + TemplateablePath::Template(template) => PathBuf::from(template.render(globals)?), + }) + } +} + pub(crate) enum MergeTarget { Application(&'static str), } @@ -63,6 +77,7 @@ impl RenderOperation { match self { Self::WriteFile(path, content) => { let rendered = content.render(globals)?; + let path = path.render(globals)?; Ok(TemplateOutput::WriteFile(path, rendered)) } Self::AppendToml(path, content) => { diff --git a/crates/templates/src/run.rs b/crates/templates/src/run.rs index afea19a722..6f3a823cc3 100644 --- a/crates/templates/src/run.rs +++ b/crates/templates/src/run.rs @@ -99,7 +99,7 @@ impl Run { // TODO: rationalise `path` and `dir` let to = self.generation_target_dir(); - if !self.options.allow_overwrite { + if !self.allow_overwrite() { match interaction.allow_generate_into(&to) { Cancellable::Cancelled => return Ok(None), Cancellable::Ok(_) => (), @@ -157,6 +157,14 @@ impl Run { } } + fn allow_overwrite(&self) -> bool { + // If the template variant asks to be generated into the app root, + // we assume that it knows what it's doing and intends to avoid + // overwriting. This is true for the one use case we have, although + // we need to track if it's a flawed assumption in general cases. + self.options.allow_overwrite || self.template.use_root(&self.options.variant) + } + fn included_files( &self, from: &Path, @@ -179,11 +187,33 @@ impl Run { let outputs = Self::to_output_paths(from, to, template_contents); let file_ops = outputs .into_iter() - .map(|(path, content)| RenderOperation::WriteFile(path, content)) + .map(|(path, content)| { + RenderOperation::WriteFile(Self::templateable_path(path, parser), content) + }) .collect(); Ok(file_ops) } + fn templateable_path( + path: PathBuf, + parser: &liquid::Parser, + ) -> crate::renderer::TemplateablePath { + let path_str = path.display().to_string(); + if !path_str.contains("{{") { + return crate::renderer::TemplateablePath::Plain(path); + } + + // Windows file paths can't contain the pipe character used in templates. + // This masterful workaround will definitely not confuse anybody or cause + // any weird hiccups in six months time. + let path_str = path_str.replace("!!", "|"); + + match parser.parse(&path_str) { + Ok(t) => crate::renderer::TemplateablePath::Template(t), + Err(_) => crate::renderer::TemplateablePath::Plain(path), + } + } + async fn special_values(&self) -> HashMap { let mut values = HashMap::new(); @@ -206,10 +236,15 @@ impl Run { fn generation_target_dir(&self) -> PathBuf { match &self.options.variant { TemplateVariantInfo::NewApplication => self.options.output_path.clone(), - TemplateVariantInfo::AddComponent { manifest_path } => manifest_path - .parent() - .unwrap() - .join(&self.options.output_path), + TemplateVariantInfo::AddComponent { manifest_path } => { + let root = manifest_path.parent().unwrap(); + + if self.template.use_root(&self.options.variant) { + root.to_owned() + } else { + root.join(&self.options.output_path) + } + } } } diff --git a/crates/templates/src/template.rs b/crates/templates/src/template.rs index 58c07ca648..50490891af 100644 --- a/crates/templates/src/template.rs +++ b/crates/templates/src/template.rs @@ -11,8 +11,8 @@ use regex::Regex; use crate::{ constraints::StringConstraints, reader::{ - RawCondition, RawConditional, RawExtraOutput, RawParameter, RawTemplateManifest, - RawTemplateManifestV1, RawTemplateVariant, + RawCondition, RawConditional, RawCopyInto, RawExtraOutput, RawParameter, + RawTemplateManifest, RawTemplateManifestV1, RawTemplateVariant, }, run::{Run, RunOptions}, store::TemplateLayout, @@ -94,8 +94,16 @@ impl TemplateVariantInfo { } } +#[derive(Clone, Debug, Default)] +enum CopyInto { + #[default] + OwnDirectory, + Root, +} + #[derive(Clone, Debug, Default)] pub(crate) struct TemplateVariant { + copy_into: CopyInto, skip_files: Vec, skip_parameters: Vec, snippets: HashMap, @@ -385,7 +393,9 @@ impl Template { } fn parse_template_variant(raw: RawTemplateVariant) -> TemplateVariant { + let copy_into = raw.copy_into.map(Self::parse_copy_into).unwrap_or_default(); TemplateVariant { + copy_into, skip_files: raw.skip_files.unwrap_or_default(), skip_parameters: raw.skip_parameters.unwrap_or_default(), snippets: raw.snippets.unwrap_or_default(), @@ -398,6 +408,13 @@ impl Template { } } + fn parse_copy_into(raw: RawCopyInto) -> CopyInto { + match raw { + RawCopyInto::Root => CopyInto::Root, + RawCopyInto::Own => CopyInto::OwnDirectory, + } + } + fn parse_conditional(conditional: RawConditional) -> Conditional { Conditional { condition: Self::parse_condition(conditional.condition), @@ -452,6 +469,11 @@ impl Template { .collect() } + pub(crate) fn use_root(&self, variant_info: &TemplateVariantInfo) -> bool { + let variant = self.variant(variant_info).unwrap(); // TODO: for now + matches!(variant.copy_into, CopyInto::Root) + } + pub(crate) fn check_compatible_trigger(&self, app_trigger: Option<&str>) -> anyhow::Result<()> { // The application we are merging into might not have a trigger yet, in which case // we're good to go. diff --git a/templates/http-rust-workspace/content/.gitignore b/templates/http-rust-workspace/content/.gitignore new file mode 100644 index 0000000000..386474fa59 --- /dev/null +++ b/templates/http-rust-workspace/content/.gitignore @@ -0,0 +1,2 @@ +target/ +.spin/ diff --git a/templates/http-rust-workspace/content/Cargo.toml.tmpl b/templates/http-rust-workspace/content/Cargo.toml.tmpl new file mode 100644 index 0000000000..a25a2cbe37 --- /dev/null +++ b/templates/http-rust-workspace/content/Cargo.toml.tmpl @@ -0,0 +1,7 @@ +[workspace] +resolver = "2" +members = ["crates/*"] + +[workspace.dependencies] +anyhow = "1" +spin-sdk = "3.1.0" diff --git a/templates/http-rust-workspace/content/crates/{{project-name !! kebab_case}}/Cargo.toml.tmpl b/templates/http-rust-workspace/content/crates/{{project-name !! kebab_case}}/Cargo.toml.tmpl new file mode 100644 index 0000000000..b8c3c62320 --- /dev/null +++ b/templates/http-rust-workspace/content/crates/{{project-name !! kebab_case}}/Cargo.toml.tmpl @@ -0,0 +1,14 @@ +[package] +name = "{{project-name | kebab_case}}" +authors = ["{{authors}}"] +description = "{{project-description}}" +version = "0.1.0" +rust-version = "1.78" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = { workspace = true } +spin-sdk = { workspace = true } diff --git a/templates/http-rust-workspace/content/crates/{{project-name !! kebab_case}}/src/lib.rs b/templates/http-rust-workspace/content/crates/{{project-name !! kebab_case}}/src/lib.rs new file mode 100644 index 0000000000..ee6fc18cb9 --- /dev/null +++ b/templates/http-rust-workspace/content/crates/{{project-name !! kebab_case}}/src/lib.rs @@ -0,0 +1,13 @@ +use spin_sdk::http::{IntoResponse, Request, Response}; +use spin_sdk::http_component; + +/// A simple Spin HTTP component. +#[http_component] +fn handle_{{project-name | snake_case}}(req: Request) -> anyhow::Result { + println!("Handling request to {:?}", req.header("spin-full-url")); + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body("Hello World!") + .build()) +} diff --git a/templates/http-rust-workspace/content/spin.toml b/templates/http-rust-workspace/content/spin.toml new file mode 100644 index 0000000000..ee1ff66f9c --- /dev/null +++ b/templates/http-rust-workspace/content/spin.toml @@ -0,0 +1,18 @@ +spin_manifest_version = 2 + +[application] +name = "{{project-name | kebab_case}}" +version = "0.1.0" +authors = ["{{authors}}"] +description = "{{project-description}}" + +[[trigger.http]] +route = "{{http-path}}" +component = "{{project-name | kebab_case}}" + +[component.{{project-name | kebab_case}}] +source = "target/wasm32-wasip1/release/{{project-name | snake_case}}.wasm" +allowed_outbound_hosts = [] +[component.{{project-name | kebab_case}}.build] +command = "cargo build -p {{project-name | kebab_case}} --target wasm32-wasip1 --release" +watch = ["crates/{{project-name | kebab_case}}/src/**/*.rs", "crates/{{project-name | kebab_case}}/Cargo.toml"] diff --git a/templates/http-rust-workspace/metadata/snippets/component.txt b/templates/http-rust-workspace/metadata/snippets/component.txt new file mode 100644 index 0000000000..c1b148b554 --- /dev/null +++ b/templates/http-rust-workspace/metadata/snippets/component.txt @@ -0,0 +1,10 @@ +[[trigger.http]] +route = "{{http-path}}" +component = "{{project-name | kebab_case}}" + +[component.{{project-name | kebab_case}}] +source = "target/wasm32-wasip1/release/{{project-name | snake_case}}.wasm" +allowed_outbound_hosts = [] +[component.{{project-name | kebab_case}}.build] +command = "cargo build -p {{project-name | kebab_case}} --target wasm32-wasip1 --release" +watch = ["crates/{{project-name | kebab_case}}/src/**/*.rs", "crates/{{project-name | kebab_case}}/Cargo.toml"] diff --git a/templates/http-rust-workspace/metadata/spin-template.toml b/templates/http-rust-workspace/metadata/spin-template.toml new file mode 100644 index 0000000000..ee6974f2d3 --- /dev/null +++ b/templates/http-rust-workspace/metadata/spin-template.toml @@ -0,0 +1,14 @@ +manifest_version = "1" +id = "http-rust-workspace" +description = "HTTP request handler using Rust with shared workspace" +tags = ["http", "rust"] + +[add_component] +copy_into = "root" +skip_files = ["spin.toml", "Cargo.toml.tmpl"] +[add_component.snippets] +component = "component.txt" + +[parameters] +project-description = { type = "string", prompt = "Description", default = "" } +http-path = { type = "string", prompt = "HTTP path", default = "/...", pattern = "^/\\S*$" } \ No newline at end of file