From e731306a1c19fa2f11c63ceda459a6900695065c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Istv=C3=A1n=20B=C3=ADr=C3=B3?= Date: Tue, 27 Aug 2024 13:41:05 +0200 Subject: [PATCH] generator support for multi-component examples --- README.md | 8 ++++++-- src/lib.rs | 44 +++++++++++++++++++++++++++++++++----------- src/model.rs | 12 ++++++++++++ src/test/main.rs | 2 +- 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7d2b762..0b98c49 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,11 @@ The following fields are required: The following fields are optional: - `requiresAdapter` is a boolean, defaults to **true**. If true, the appropriate version of the WASI Preview2 to Preview1 adapter is copied into the generated project (based on the guest language) to an `adapters` directory. -- `requiresGolemHostWIT` is a boolean, defaults to **false**. If true, the Golem specific WIT interface gets copied into `wit/deps`. +- `requiresGolemHostWIT` is a boolean, defaults to **false**. If true, the Golem specific WIT interface gets copied into `wit/deps`. - `requiresWASI` is a boolean, defaults to **false**. If true, the WASI Preview2 WIT interfaces which are compatible with Golem Cloud get copied into `wit/deps`. +- `witDepsPaths` is an array of directory paths, defaults to **null**. When set, overrides the `wit/deps` directory for the above options and allows to use multiple target dirs for supporting multi-component examples. - `exclude` is a list of sub-paths and works as a simplified `.gitignore` file. It's primary purpose is to help the development loop of working on examples and in the future it will likely be dropped in favor of just using `.gitignore` files. +- `instructions` is an optional filename, defaults to **null**. When set, overrides the __INSTRUCTIONS__ file used for the example, the file needs to be placed to same directory as the default instructions file. ### Template rules @@ -34,7 +36,7 @@ Golem examples are currently simple and not using any known template language, i When calling `golem-new` the user specifies a **template name**. The provided component name must use either `PascalCase`, `snake_case` or `kebab-case`. -There is an optional parameter for defining a **package name**, which defaults to `golem:component`. It has to be in the `pack:name` format. +There is an optional parameter for defining a **package name**, which defaults to `golem:component`. It has to be in the `pack:name` format. The first part of the package name is called **package namespace**. The following occurrences get replaced to the provided component name, applying the casing used in the template: - `component-name` @@ -46,6 +48,8 @@ The following occurrences get replaced to the provided component name, applying - `pack-name` - `pack/name` - `PackName` +- `pack-ns` +- `PackNs` ### Testing the examples The example generation and instructions can be tested with a test [cli app](/src/test/main.rs). diff --git a/src/lib.rs b/src/lib.rs index ffdd4af..13d68e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,7 +29,6 @@ impl Examples for GolemExamples { if let Some(lang_dir) = entry.as_dir() { let lang_dir_name = lang_dir.path().file_name().unwrap().to_str().unwrap(); if let Some(lang) = GuestLanguage::from_string(lang_dir_name) { - let instructions_path = lang_dir.path().join("INSTRUCTIONS"); let adapters_path = Path::new(lang.tier().name()).join("wasi_snapshot_preview1.wasm"); @@ -42,7 +41,8 @@ impl Examples for GolemExamples { { let example = parse_example( &lang, - &instructions_path, + &lang_dir.path(), + Path::new("INSTRUCTIONS"), &adapters_path, example_dir.path(), ); @@ -81,17 +81,29 @@ impl Examples for GolemExamples { .join(adapter_path.file_name().unwrap().to_str().unwrap()), )?; } - for wit_dep in &example.wit_deps { - copy_all( - &WIT, - wit_dep, - ¶meters + let wit_deps_targets = { + match &example.wit_deps_targets { + Some(paths) => paths + .iter() + .map(|path| { + parameters + .target_path + .join(parameters.component_name.as_string()) + .join(path) + }) + .collect(), + None => vec![parameters .target_path .join(parameters.component_name.as_string()) .join("wit") - .join("deps") - .join(wit_dep.file_name().unwrap().to_str().unwrap()), - )?; + .join("deps")], + } + }; + for wit_dep in &example.wit_deps { + for target_wit_deps in &wit_deps_targets { + let target = target_wit_deps.join(wit_dep.file_name().unwrap().to_str().unwrap()); + copy_all(&WIT, wit_dep, &target)?; + } } Ok(Self::instructions(example, parameters)) } @@ -205,6 +217,8 @@ fn transform(str: impl AsRef, parameters: &ExampleParameters) -> String { .replace("pack-name", ¶meters.package_name.to_kebab_case()) .replace("pack/name", ¶meters.package_name.to_string_with_slash()) .replace("PackName", ¶meters.package_name.to_pascal_case()) + .replace("pack-ns", ¶meters.package_name.namespace()) + .replace("PackNs", ¶meters.package_name.namespace_title_case()) } fn file_name_transform(str: impl AsRef, parameters: &ExampleParameters) -> String { @@ -213,7 +227,8 @@ fn file_name_transform(str: impl AsRef, parameters: &ExampleParameters) -> fn parse_example( lang: &GuestLanguage, - instructions_path: &Path, + lang_path: &Path, + default_instructions_file_name: &Path, adapters_path: &Path, example_root: &Path, ) -> Example { @@ -223,6 +238,10 @@ fn parse_example( .contents(); let metadata = serde_json::from_slice::(raw_metadata) .expect("Failed to parse metadata JSON"); + let instructions_path = match metadata.instructions { + Some(instructions_file_name) => lang_path.join(instructions_file_name), + None => lang_path.join(default_instructions_file_name), + }; let raw_instructions = EXAMPLES .get_file(instructions_path) .expect("Failed to read instructions") @@ -261,6 +280,9 @@ fn parse_example( None }, wit_deps, + wit_deps_targets: metadata + .wit_deps_paths + .map(|dirs| dirs.iter().map(|dir| PathBuf::from(dir)).collect()), exclude: metadata.exclude.iter().cloned().collect(), } } diff --git a/src/model.rs b/src/model.rs index 6651e64..e76aeac 100644 --- a/src/model.rs +++ b/src/model.rs @@ -257,6 +257,14 @@ impl PackageName { pub fn to_kebab_case(&self) -> String { format!("{}-{}", self.0 .0, self.0 .1) } + + pub fn namespace(&self) -> String { + self.0 .0.to_string() + } + + pub fn namespace_title_case(&self) -> String { + self.0 .0.to_title_case() + } } impl fmt::Display for PackageName { @@ -303,6 +311,7 @@ pub struct Example { pub instructions: String, pub adapter: Option, pub wit_deps: Vec, + pub wit_deps_targets: Option>, pub exclude: HashSet, } @@ -322,7 +331,10 @@ pub(crate) struct ExampleMetadata { pub requires_golem_host_wit: Option, #[serde(rename = "requiresWASI")] pub requires_wasi: Option, + #[serde(rename = "witDepsPaths")] + pub wit_deps_paths: Option>, pub exclude: Vec, + pub instructions: Option, } #[cfg(test)] diff --git a/src/test/main.rs b/src/test/main.rs index 3047387..9f3a2fc 100644 --- a/src/test/main.rs +++ b/src/test/main.rs @@ -84,7 +84,7 @@ fn test_example(command: &Command, example: &Example) -> Result<(), String> { ); let component_name = ComponentName::new(example.name.as_string().to_string() + "-comp"); let package_name = - PackageName::from_string("golem:component").ok_or("failed to create package name")?; + PackageName::from_string("golemx:componentx").ok_or("failed to create package name")?; let component_path = target_path.join(component_name.as_string()); println!("Target path: {}", target_path.display().to_string().blue());