Skip to content

Commit cfc203e

Browse files
authored
Merge pull request #889 from itowlson/templates-new-component
`spin new`: add component to existing application
2 parents 5d5ac4b + 5981484 commit cfc203e

File tree

16 files changed

+703
-24
lines changed

16 files changed

+703
-24
lines changed

crates/plugins/src/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ mod store;
77
pub use store::PluginStore;
88

99
/// List of Spin internal subcommands
10-
pub(crate) const SPIN_INTERNAL_COMMANDS: [&str; 9] = [
10+
pub(crate) const SPIN_INTERNAL_COMMANDS: [&str; 10] = [
1111
"templates",
1212
"up",
1313
"new",
14+
"add",
1415
"bindle",
1516
"deploy",
1617
"build",

crates/templates/src/app_info.rs

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Information about the application manifest that is of
2+
// interest to the template system. spin_loader does too
3+
// much processing to fit our needs here.
4+
5+
use std::path::{Path, PathBuf};
6+
7+
use anyhow::Context;
8+
9+
use crate::store::TemplateLayout;
10+
11+
#[derive(Debug, serde::Deserialize)]
12+
#[serde(tag = "spin_version")]
13+
pub(crate) enum AppInfo {
14+
/// A manifest with API version 1.
15+
#[serde(rename = "1")]
16+
V1(AppInfoV1),
17+
}
18+
19+
#[derive(Debug, serde::Deserialize)]
20+
pub(crate) struct AppInfoV1 {
21+
trigger: TriggerInfo,
22+
}
23+
24+
#[derive(Debug, serde::Deserialize)]
25+
pub(crate) struct TriggerInfo {
26+
#[serde(rename = "type")]
27+
trigger_type: String,
28+
}
29+
30+
impl AppInfo {
31+
pub(crate) fn from_layout(layout: &TemplateLayout) -> Option<anyhow::Result<AppInfo>> {
32+
Self::layout_manifest_path(layout)
33+
.map(|manifest_path| Self::from_existent_file(&manifest_path))
34+
}
35+
36+
pub(crate) fn from_file(manifest_path: &Path) -> Option<anyhow::Result<AppInfo>> {
37+
if manifest_path.exists() {
38+
Some(Self::from_existent_file(manifest_path))
39+
} else {
40+
None
41+
}
42+
}
43+
44+
fn layout_manifest_path(layout: &TemplateLayout) -> Option<PathBuf> {
45+
let manifest_path = layout.content_dir().join("spin.toml");
46+
if manifest_path.exists() {
47+
Some(manifest_path)
48+
} else {
49+
None
50+
}
51+
}
52+
53+
fn from_existent_file(manifest_path: &Path) -> anyhow::Result<AppInfo> {
54+
let manifest_text =
55+
std::fs::read_to_string(manifest_path).context("Can't read manifest file")?;
56+
toml::from_str(&manifest_text).context("Can't parse manifest file")
57+
}
58+
59+
pub(crate) fn trigger_type(&self) -> &str {
60+
match self {
61+
Self::V1(info) => &info.trigger.trigger_type,
62+
}
63+
}
64+
}

crates/templates/src/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
#![deny(missing_docs)]
44

5+
mod app_info;
56
mod constraints;
67
mod custom_filters;
78
mod directory;
@@ -18,4 +19,4 @@ mod template;
1819
pub use manager::*;
1920
pub use run::{Run, RunOptions, TemplatePreparationResult};
2021
pub use source::TemplateSource;
21-
pub use template::Template;
22+
pub use template::{Template, TemplateVariantKind};

crates/templates/src/manager.rs

+209-1
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ mod tests {
414414
PathBuf::from(crate_dir).join("tests")
415415
}
416416

417-
const TPLS_IN_THIS: usize = 8;
417+
const TPLS_IN_THIS: usize = 9;
418418

419419
#[tokio::test]
420420
async fn can_install_into_new_directory() {
@@ -631,10 +631,12 @@ mod tests {
631631
.into_iter()
632632
.collect();
633633
let options = RunOptions {
634+
variant: crate::template::TemplateVariantKind::NewApplication,
634635
output_path: output_dir.clone(),
635636
name: "my project".to_owned(),
636637
values,
637638
accept_defaults: false,
639+
existing_manifest: None,
638640
};
639641

640642
template
@@ -669,10 +671,12 @@ mod tests {
669671
let output_dir = dest_temp_dir.path().join("myproj");
670672
let values = HashMap::new();
671673
let options = RunOptions {
674+
variant: crate::template::TemplateVariantKind::NewApplication,
672675
output_path: output_dir.clone(),
673676
name: "my project".to_owned(),
674677
values,
675678
accept_defaults: true,
679+
existing_manifest: None,
676680
};
677681

678682
template
@@ -716,10 +720,12 @@ mod tests {
716720
.into_iter()
717721
.collect();
718722
let options = RunOptions {
723+
variant: crate::template::TemplateVariantKind::NewApplication,
719724
output_path: output_dir.clone(),
720725
name: "custom-filter-test".to_owned(),
721726
values,
722727
accept_defaults: false,
728+
existing_manifest: None,
723729
};
724730

725731
template
@@ -737,4 +743,206 @@ mod tests {
737743
assert!(message.contains("p2/studly = nOmNoMnOm"));
738744
assert!(message.contains("p1/clappy = b👏i👏s👏c👏u👏i👏t👏s"));
739745
}
746+
747+
#[tokio::test]
748+
async fn can_add_component_from_template() {
749+
let temp_dir = tempdir().unwrap();
750+
let store = TemplateStore::new(temp_dir.path());
751+
let manager = TemplateManager { store };
752+
let source = TemplateSource::File(project_root());
753+
754+
manager
755+
.install(&source, &InstallOptions::default(), &DiscardingReporter)
756+
.await
757+
.unwrap();
758+
759+
let dest_temp_dir = tempdir().unwrap();
760+
let application_dir = dest_temp_dir.path().join("multi");
761+
762+
// Set up the containing app
763+
{
764+
let template = manager.get("http-empty").unwrap().unwrap();
765+
766+
let values = [
767+
("project-description".to_owned(), "my desc".to_owned()),
768+
("http-base".to_owned(), "/".to_owned()),
769+
]
770+
.into_iter()
771+
.collect();
772+
let options = RunOptions {
773+
variant: crate::template::TemplateVariantKind::NewApplication,
774+
output_path: application_dir.clone(),
775+
name: "my multi project".to_owned(),
776+
values,
777+
accept_defaults: false,
778+
existing_manifest: None,
779+
};
780+
781+
template
782+
.run(options)
783+
.silent()
784+
.await
785+
.execute()
786+
.await
787+
.unwrap();
788+
}
789+
790+
let spin_toml_path = application_dir.join("spin.toml");
791+
assert!(spin_toml_path.exists(), "expected spin.toml to be created");
792+
793+
// Now add a component
794+
{
795+
let template = manager.get("http-rust").unwrap().unwrap();
796+
797+
let output_dir = "hello";
798+
let values = [
799+
("project-description".to_owned(), "hello".to_owned()),
800+
("http-path".to_owned(), "/hello".to_owned()),
801+
]
802+
.into_iter()
803+
.collect();
804+
let options = RunOptions {
805+
variant: crate::template::TemplateVariantKind::AddComponent,
806+
output_path: PathBuf::from(output_dir),
807+
name: "hello".to_owned(),
808+
values,
809+
accept_defaults: false,
810+
existing_manifest: Some(spin_toml_path.clone()),
811+
};
812+
813+
template
814+
.run(options)
815+
.silent()
816+
.await
817+
.execute()
818+
.await
819+
.unwrap();
820+
}
821+
822+
// And another
823+
{
824+
let template = manager.get("http-rust").unwrap().unwrap();
825+
826+
let output_dir = "encore";
827+
let values = [
828+
("project-description".to_owned(), "hello 2".to_owned()),
829+
("http-path".to_owned(), "/hello-2".to_owned()),
830+
]
831+
.into_iter()
832+
.collect();
833+
let options = RunOptions {
834+
variant: crate::template::TemplateVariantKind::AddComponent,
835+
output_path: PathBuf::from(output_dir),
836+
name: "hello 2".to_owned(),
837+
values,
838+
accept_defaults: false,
839+
existing_manifest: Some(spin_toml_path.clone()),
840+
};
841+
842+
template
843+
.run(options)
844+
.silent()
845+
.await
846+
.execute()
847+
.await
848+
.unwrap();
849+
}
850+
851+
let cargo1 = tokio::fs::read_to_string(application_dir.join("hello/Cargo.toml"))
852+
.await
853+
.unwrap();
854+
assert!(cargo1.contains("name = \"hello\""));
855+
856+
let cargo2 = tokio::fs::read_to_string(application_dir.join("encore/Cargo.toml"))
857+
.await
858+
.unwrap();
859+
assert!(cargo2.contains("name = \"hello-2\""));
860+
861+
let spin_toml = tokio::fs::read_to_string(&spin_toml_path).await.unwrap();
862+
assert!(spin_toml.contains("source = \"hello/target/wasm32-wasi/release/hello.wasm\""));
863+
assert!(spin_toml.contains("source = \"encore/target/wasm32-wasi/release/hello_2.wasm\""));
864+
}
865+
866+
#[tokio::test]
867+
async fn cannot_add_component_that_does_not_match_trigger() {
868+
let temp_dir = tempdir().unwrap();
869+
let store = TemplateStore::new(temp_dir.path());
870+
let manager = TemplateManager { store };
871+
let source = TemplateSource::File(project_root());
872+
873+
manager
874+
.install(&source, &InstallOptions::default(), &DiscardingReporter)
875+
.await
876+
.unwrap();
877+
878+
let dest_temp_dir = tempdir().unwrap();
879+
let application_dir = dest_temp_dir.path().join("multi");
880+
881+
// Set up the containing app
882+
{
883+
let template = manager.get("redis-rust").unwrap().unwrap();
884+
885+
let values = [
886+
("project-description".to_owned(), "my desc".to_owned()),
887+
(
888+
"redis-address".to_owned(),
889+
"redis://localhost:6379".to_owned(),
890+
),
891+
(
892+
"redis-channel".to_owned(),
893+
"the-horrible-knuckles".to_owned(),
894+
),
895+
]
896+
.into_iter()
897+
.collect();
898+
let options = RunOptions {
899+
variant: crate::template::TemplateVariantKind::NewApplication,
900+
output_path: application_dir.clone(),
901+
name: "my multi project".to_owned(),
902+
values,
903+
accept_defaults: false,
904+
existing_manifest: None,
905+
};
906+
907+
template
908+
.run(options)
909+
.silent()
910+
.await
911+
.execute()
912+
.await
913+
.unwrap();
914+
}
915+
916+
let spin_toml_path = application_dir.join("spin.toml");
917+
assert!(spin_toml_path.exists(), "expected spin.toml to be created");
918+
919+
// Now add a component
920+
{
921+
let template = manager.get("http-rust").unwrap().unwrap();
922+
923+
let output_dir = "hello";
924+
let values = [
925+
("project-description".to_owned(), "hello".to_owned()),
926+
("http-path".to_owned(), "/hello".to_owned()),
927+
]
928+
.into_iter()
929+
.collect();
930+
let options = RunOptions {
931+
variant: crate::template::TemplateVariantKind::AddComponent,
932+
output_path: PathBuf::from(output_dir),
933+
name: "hello".to_owned(),
934+
values,
935+
accept_defaults: false,
936+
existing_manifest: Some(spin_toml_path.clone()),
937+
};
938+
939+
template
940+
.run(options)
941+
.silent()
942+
.await
943+
.execute()
944+
.await
945+
.expect_err("Expected to fail to add component, but it succeeded");
946+
}
947+
}
740948
}

crates/templates/src/reader.rs

+12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::HashMap;
2+
13
use anyhow::Context;
24
use indexmap::IndexMap;
35
use serde::Deserialize;
@@ -15,10 +17,20 @@ pub(crate) enum RawTemplateManifest {
1517
pub(crate) struct RawTemplateManifestV1 {
1618
pub id: String,
1719
pub description: Option<String>,
20+
pub trigger_type: Option<String>,
21+
pub add_component: Option<RawTemplateVariant>,
1822
pub parameters: Option<IndexMap<String, RawParameter>>,
1923
pub custom_filters: Option<Vec<RawCustomFilter>>,
2024
}
2125

26+
#[derive(Debug, Deserialize)]
27+
#[serde(deny_unknown_fields, rename_all = "snake_case")]
28+
pub(crate) struct RawTemplateVariant {
29+
pub skip_files: Option<Vec<String>>,
30+
pub skip_parameters: Option<Vec<String>>,
31+
pub snippets: Option<HashMap<String, String>>,
32+
}
33+
2234
#[derive(Debug, Deserialize)]
2335
#[serde(deny_unknown_fields, rename_all = "snake_case")]
2436
pub(crate) struct RawParameter {

0 commit comments

Comments
 (0)