diff --git a/src/commands/add.rs b/src/commands/add.rs index e128a57..0c2751f 100644 --- a/src/commands/add.rs +++ b/src/commands/add.rs @@ -1,22 +1,25 @@ -use anyhow::{bail, Context, Result}; +use anyhow::Result; use clap::Subcommand; -use dialoguer::{MultiSelect, Select}; use http::HttpAddCommand; use local::LocalAddCommand; use registry::RegistryAddCommand; use spin_manifest::{ manifest_from_file, - schema::v2::{AppManifest, ComponentDependencies, ComponentDependency}, + schema::v2::{AppManifest, ComponentDependency}, }; use spin_serde::{DependencyName, DependencyPackageName, KebabId}; use std::{collections::HashMap, path::PathBuf}; use tokio::fs; -use toml_edit::DocumentMut; -use wit_component::WitPrinter; use wit_parser::{PackageId, Resolve}; -const SPIN_WIT_DIRECTORY: &str = ".wit"; -const SPIN_COMPONENTS_WIT_DIRECTORY: &str = "components"; +use crate::common::{ + constants::{SPIN_COMPONENTS_WIT_DIRECTORY, SPIN_DEPS_WIT_FILE_NAME, SPIN_WIT_DIRECTORY}, + interact::{select_multiple_prompt, select_prompt}, + manifest::{edit_component_deps_in_manifest, get_component_ids, get_spin_manifest_path}, + wit::{ + get_exported_interfaces, merge_dependecy_package, parse_component_bytes, resolve_to_wit, + }, +}; mod http; mod local; @@ -40,15 +43,17 @@ impl AddCommand { AddCommand::Registry(cmd) => cmd.get_component().await?, }; - self.validate_component(&component)?; + let (mut resolve, main) = parse_component_bytes(component)?; let mut manifest = manifest_from_file(get_spin_manifest_path()?)?; - let component_ids = self.list_component_ids(&manifest); - let selected_component = self.select_component(&component_ids)?; + let component_ids = get_component_ids(&manifest); + let selected_component_index = select_prompt( + "Select a component to add the dependency to", + &component_ids, + None, + )?; + let selected_component = &component_ids[selected_component_index]; - let decoded_wasm = wit_component::decode(&component)?; - let mut resolve = decoded_wasm.resolve().clone(); - let main = decoded_wasm.package(); let selected_interfaces = self.select_interfaces(&mut resolve, main)?; resolve.importize( @@ -56,44 +61,32 @@ impl AddCommand { Some("dependency-world".to_string()), )?; - self.write_wit_to_file(&resolve, main, &selected_component) - .await?; - self.update_manifest(&mut manifest, &selected_component, selected_interfaces) - .await?; + let component_dir = PathBuf::from(SPIN_WIT_DIRECTORY) + .join(SPIN_COMPONENTS_WIT_DIRECTORY) + .join(selected_component); - Ok(()) - } + let output_wit = component_dir.join(SPIN_DEPS_WIT_FILE_NAME); - /// List all component IDs in the manifest. - fn list_component_ids(&self, manifest: &AppManifest) -> Vec { - manifest.components.keys().map(|k| k.to_string()).collect() - } + let base_resolve_file = if std::fs::exists(&output_wit)? { + Some(&output_wit) + } else { + None + }; - /// Prompts the user to select a component from a list. - fn select_component(&self, component_ids: &[String]) -> Result { - let selected_component_index = Select::new() - .with_prompt("Select a component") - .items(component_ids) - .default(0) - .interact()?; + let (merged_resolve, main) = merge_dependecy_package(base_resolve_file, &resolve, main)?; + let wit_text = resolve_to_wit(&merged_resolve, main)?; + fs::write(output_wit, wit_text).await?; - Ok(component_ids[selected_component_index].clone()) - } + self.update_manifest(&mut manifest, selected_component, selected_interfaces) + .await?; - /// Validates the WebAssembly component. - fn validate_component(&self, component: &[u8]) -> Result<()> { - let t = wasmparser::validate(component) - .context("Provided component does not seem to be a valid component"); - match Result::from(t) { - Ok(_) => Ok(()), - Err(e) => bail!(e), - } + Ok(()) } /// Prompts the user to select an interface to import. fn select_interfaces(&self, resolve: &mut Resolve, main: PackageId) -> Result> { let world_id = resolve.select_world(main, None)?; - let exported_interfaces = self.get_exported_interfaces(resolve, world_id); + let exported_interfaces = get_exported_interfaces(resolve, world_id); let mut package_interface_map: HashMap> = HashMap::new(); let mut selected_interfaces: Vec = Vec::new(); @@ -106,15 +99,15 @@ impl AddCommand { .push(interface); } - let package_names: Vec<_> = package_interface_map.keys().collect(); + let package_names: Vec<_> = package_interface_map.keys().cloned().collect(); - let selected_package_indices = MultiSelect::new() - .with_prompt("Select packages to import (use space to select, enter to confirm)") - .items(&package_names) - .interact()?; + let selected_package_indices = select_multiple_prompt( + "Select packages to import (use space to select, enter to confirm)", + &package_names, + )?; for &package_idx in selected_package_indices.iter() { - let package_name = package_names[package_idx]; + let package_name = &package_names[package_idx]; let interfaces = package_interface_map.get(package_name).unwrap(); let interface_count = interfaces.len(); @@ -128,14 +121,14 @@ impl AddCommand { }; // Prompt user to select an interface - let selected_interface_idx = Select::new() - .with_prompt(format!( + let selected_interface_idx = select_prompt( + &format!( "Select one or all interfaces to import from package '{}'", package_name - )) - .default(0) - .items(&interface_options) - .interact()?; + ), + &interface_options, + Some(0), + )?; if interface_count > 1 && selected_interface_idx == 0 { selected_interfaces.push(package_name.clone()); @@ -148,67 +141,6 @@ impl AddCommand { Ok(selected_interfaces) } - /// Retrieves the exported interfaces from the resolved world. - fn get_exported_interfaces( - &self, - resolve: &Resolve, - world_id: wit_parser::WorldId, - ) -> Vec<(String, String)> { - resolve.worlds[world_id] - .exports - .iter() - .filter_map(|(_k, v)| match v { - wit_parser::WorldItem::Interface { id, .. } => { - let i = &resolve.interfaces[*id]; - let pkg_id = i.package.unwrap(); - let pkg = &resolve.packages[pkg_id]; - let mut pkg_name = format!("{}:{}", pkg.name.namespace, pkg.name.name); - if let Some(ver) = &pkg.name.version { - pkg_name.push_str(&format!("@{}", ver)); - } - Some((pkg_name, i.name.clone().unwrap_or_default())) - } - _ => None, - }) - .collect() - } - - /// Writes the WIT content to the specified file. - async fn write_wit_to_file( - &self, - dep_resolve: &Resolve, - dep_pkg_id: PackageId, - selected_component: &str, - ) -> Result<()> { - const SPIN_DEPS_WIT_FILE_NAME: &str = "deps.wit"; - - let component_dir = PathBuf::from(SPIN_WIT_DIRECTORY) - .join(SPIN_COMPONENTS_WIT_DIRECTORY) - .join(selected_component); - - let output_wit = component_dir.join(SPIN_DEPS_WIT_FILE_NAME); - let mut resolve = Resolve::default(); - - let deps_package_id = if std::fs::exists(&output_wit)? { - resolve.push_file(&output_wit)? - } else { - fs::create_dir_all(&component_dir).await?; - resolve.push_str("component.wit", DEFAULT_WIT)? - }; - - let deps_world_id = resolve.select_world(deps_package_id, Some("deps"))?; - let dep_main_world_id = dep_resolve.select_world(dep_pkg_id, Some("dependency-world"))?; - let remap = resolve.merge(dep_resolve.clone())?; - let dependecy_world_id = remap.map_world(dep_main_world_id, None)?; - resolve.merge_worlds(dependecy_world_id, deps_world_id)?; - - let wit_content = resolve_to_wit(&resolve, deps_package_id)?; - - fs::write(output_wit, wit_content).await?; - - Ok(()) - } - /// Updates the manifest file with the new component dependency. async fn update_manifest( &self, @@ -253,93 +185,3 @@ impl AddCommand { Ok(()) } } - -/// Converts a Resolve object to WIT content. -fn resolve_to_wit(resolve: &Resolve, package_id: PackageId) -> Result { - let mut printer = WitPrinter::default(); - printer.emit_docs(false); - - let ids = resolve - .packages - .iter() - .map(|(id, _)| id) - .filter(|id| *id != package_id) - .collect::>(); - - printer.print(resolve, package_id, &ids) -} - -// This is a helper function to edit the dependency table in the manifest file -// while preserving the order of the manifest. -async fn edit_component_deps_in_manifest( - component_id: &str, - component_deps: &ComponentDependencies, -) -> Result { - let manifest_path = get_spin_manifest_path()?; - let manifest = fs::read_to_string(manifest_path).await?; - let mut doc = manifest.parse::()?; - - let mut dependencies_table = toml_edit::Table::new(); - - for (name, dep) in &component_deps.inner { - let dep_src = match dep { - ComponentDependency::Version(version) => { - let mut ver_table = toml_edit::InlineTable::default(); - ver_table.get_or_insert("version", version); - toml_edit::Value::InlineTable(ver_table) - } - ComponentDependency::Package { - version, - registry, - package, - export: _, - } => { - let mut pkg_table = toml_edit::InlineTable::default(); - pkg_table.get_or_insert("version", version); - if let Some(reg) = registry.clone() { - pkg_table.get_or_insert("registry", reg.to_string()); - } - if let Some(pkg) = package { - pkg_table.get_or_insert("package", pkg); - } - toml_edit::Value::InlineTable(pkg_table) - } - ComponentDependency::Local { path, export: _ } => { - let mut local_table = toml_edit::InlineTable::default(); - local_table.get_or_insert("path", path.to_str().unwrap().to_owned()); - toml_edit::Value::InlineTable(local_table) - } - ComponentDependency::HTTP { - url, - digest, - export: _, - } => { - let mut http_table = toml_edit::InlineTable::default(); - http_table.get_or_insert("url", url); - http_table.get_or_insert("digest", digest); - toml_edit::Value::InlineTable(http_table) - } - }; - - dependencies_table.insert(&name.to_string(), toml_edit::Item::Value(dep_src.clone())); - } - - doc["component"][component_id]["dependencies"] = toml_edit::Item::Table(dependencies_table); - - Ok(doc.to_string()) -} - -// TODO: Eventually bring this function with the proposed Spin functionality of searching in parent Directories. -fn get_spin_manifest_path() -> Result { - let manifest_path = PathBuf::from("spin.toml"); - if !manifest_path.exists() { - bail!("No spin.toml file found in the current directory"); - } - Ok(manifest_path) -} - -const DEFAULT_WIT: &str = r#"package spin-deps:deps@0.1.0; - - world deps { - } -"#; diff --git a/src/commands/bindings.rs b/src/commands/bindings.rs new file mode 100644 index 0000000..a1cb367 --- /dev/null +++ b/src/commands/bindings.rs @@ -0,0 +1,20 @@ +use anyhow::Result; +use clap::{Args, ValueEnum}; + +#[derive(Debug, Clone, ValueEnum)] +pub enum BindingsLanguage { + Ts, + Rust, +} + +#[derive(Args, Debug)] +pub struct BindingsCommand { + pub lang: Option, + pub component_id: Option, +} + +impl BindingsCommand { + pub async fn run(&self) -> Result<()> { + Ok(()) + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index cced7b4..9132188 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1 +1,2 @@ pub mod add; +pub mod bindings; diff --git a/src/common/constants.rs b/src/common/constants.rs new file mode 100644 index 0000000..e2175a0 --- /dev/null +++ b/src/common/constants.rs @@ -0,0 +1,3 @@ +pub const SPIN_WIT_DIRECTORY: &str = ".wit"; +pub const SPIN_COMPONENTS_WIT_DIRECTORY: &str = "components"; +pub const SPIN_DEPS_WIT_FILE_NAME: &str = "deps.wit"; diff --git a/src/common/interact.rs b/src/common/interact.rs new file mode 100644 index 0000000..1cc59d5 --- /dev/null +++ b/src/common/interact.rs @@ -0,0 +1,22 @@ +use anyhow::Result; + +use dialoguer::{MultiSelect, Select}; + +pub fn select_prompt( + prompt: &str, + selection_list: &[String], + default: Option, +) -> Result { + let mut select = Select::new().with_prompt(prompt).items(selection_list); + if let Some(index) = default { + select = select.default(index); + } + Ok(select.interact()?) +} + +pub fn select_multiple_prompt(prompt: &str, selection_list: &[String]) -> Result> { + Ok(MultiSelect::new() + .with_prompt(prompt) + .items(selection_list) + .interact()?) +} diff --git a/src/common/manifest.rs b/src/common/manifest.rs new file mode 100644 index 0000000..c9d8d4c --- /dev/null +++ b/src/common/manifest.rs @@ -0,0 +1,78 @@ +use anyhow::{bail, Result}; +use spin_manifest::schema::v2::{AppManifest, ComponentDependencies, ComponentDependency}; +use std::path::PathBuf; +use tokio::fs; +use toml_edit::DocumentMut; + +pub fn get_component_ids(manifest: &AppManifest) -> Vec { + manifest.components.keys().map(|k| k.to_string()).collect() +} + +// This is a helper function to edit the dependency table in the manifest file +// while preserving the order of the manifest. +pub async fn edit_component_deps_in_manifest( + component_id: &str, + component_deps: &ComponentDependencies, +) -> Result { + let manifest_path = get_spin_manifest_path()?; + let manifest = fs::read_to_string(manifest_path).await?; + let mut doc = manifest.parse::()?; + + let mut dependencies_table = toml_edit::Table::new(); + + for (name, dep) in &component_deps.inner { + let dep_src = match dep { + ComponentDependency::Version(version) => { + let mut ver_table = toml_edit::InlineTable::default(); + ver_table.get_or_insert("version", version); + toml_edit::Value::InlineTable(ver_table) + } + ComponentDependency::Package { + version, + registry, + package, + export: _, + } => { + let mut pkg_table = toml_edit::InlineTable::default(); + pkg_table.get_or_insert("version", version); + if let Some(reg) = registry.clone() { + pkg_table.get_or_insert("registry", reg.to_string()); + } + if let Some(pkg) = package { + pkg_table.get_or_insert("package", pkg); + } + toml_edit::Value::InlineTable(pkg_table) + } + ComponentDependency::Local { path, export: _ } => { + let mut local_table = toml_edit::InlineTable::default(); + local_table.get_or_insert("path", path.to_str().unwrap().to_owned()); + toml_edit::Value::InlineTable(local_table) + } + ComponentDependency::HTTP { + url, + digest, + export: _, + } => { + let mut http_table = toml_edit::InlineTable::default(); + http_table.get_or_insert("url", url); + http_table.get_or_insert("digest", digest); + toml_edit::Value::InlineTable(http_table) + } + }; + + dependencies_table.insert(&name.to_string(), toml_edit::Item::Value(dep_src.clone())); + } + + doc["component"][component_id]["dependencies"] = toml_edit::Item::Table(dependencies_table); + + Ok(doc.to_string()) +} + +// TODO: Eventually bring this function with the proposed Spin functionality of searching in parent Directories. +pub fn get_spin_manifest_path() -> Result { + let manifest_path = PathBuf::from("spin.toml"); + if !manifest_path.exists() { + bail!("No spin.toml file found in the current directory"); + } + Ok(manifest_path) +} diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..2c392d3 --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,4 @@ +pub mod constants; +pub mod interact; +pub mod manifest; +pub mod wit; diff --git a/src/common/wit.rs b/src/common/wit.rs new file mode 100644 index 0000000..22dbb72 --- /dev/null +++ b/src/common/wit.rs @@ -0,0 +1,82 @@ +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use wit_component::WitPrinter; +use wit_parser::{PackageId, Resolve}; + +pub const DEFAULT_WIT: &str = r#"package spin-deps:deps@0.1.0; + + world deps { + } +"#; + +/// Converts a Resolve object to WIT content. +pub fn resolve_to_wit(resolve: &Resolve, package_id: PackageId) -> Result { + let mut printer = WitPrinter::default(); + printer.emit_docs(false); + + let ids = resolve + .packages + .iter() + .map(|(id, _)| id) + .filter(|id| *id != package_id) + .collect::>(); + + printer.print(resolve, package_id, &ids) +} + +pub fn parse_component_bytes(bytes: Vec) -> Result<(Resolve, PackageId)> { + wasmparser::validate(&bytes) + .context("Provided component does not seem to be a valid component")?; + + let decoded_wasm = wit_component::decode(&bytes)?; + let resolve = decoded_wasm.resolve().clone(); + let main = decoded_wasm.package(); + + Ok((resolve, main)) +} + +/// Retrieves the exported interfaces from the resolved world. +pub fn get_exported_interfaces( + resolve: &Resolve, + world_id: wit_parser::WorldId, +) -> Vec<(String, String)> { + resolve.worlds[world_id] + .exports + .iter() + .filter_map(|(_k, v)| match v { + wit_parser::WorldItem::Interface { id, .. } => { + let i = &resolve.interfaces[*id]; + let pkg_id = i.package.unwrap(); + let pkg = &resolve.packages[pkg_id]; + let mut pkg_name = format!("{}:{}", pkg.name.namespace, pkg.name.name); + if let Some(ver) = &pkg.name.version { + pkg_name.push_str(&format!("@{}", ver)); + } + Some((pkg_name, i.name.clone().unwrap_or_default())) + } + _ => None, + }) + .collect() +} + +pub fn merge_dependecy_package( + base_resolve_file: Option<&PathBuf>, + dependency_resolve: &Resolve, + dependency_pkg_id: PackageId, +) -> Result<(Resolve, PackageId)> { + let mut base_resolve = Resolve::default(); + let base_resolve_pkg_id = match base_resolve_file { + Some(path) => base_resolve.push_file(path)?, + None => base_resolve.push_str("base_resolve.wit", DEFAULT_WIT)?, + }; + let base_resolve_world_id = base_resolve.select_world(base_resolve_pkg_id, Some("deps"))?; + + let dependecy_main_world_id = + dependency_resolve.select_world(dependency_pkg_id, Some("dependency-world"))?; + let remap = base_resolve.merge(dependency_resolve.clone())?; + let dependecy_world_id = remap.map_world(dependecy_main_world_id, None)?; + base_resolve.merge_worlds(dependecy_world_id, base_resolve_world_id)?; + + Ok((base_resolve, base_resolve_pkg_id)) +} diff --git a/src/main.rs b/src/main.rs index 5e0c12b..1b5fd68 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,8 @@ use anyhow::Result; use clap::{Parser, Subcommand}; mod commands; -use commands::add::AddCommand; +mod common; +use commands::{add::AddCommand, bindings::BindingsCommand}; /// Main CLI structure for command-line argument parsing. #[derive(Parser)] @@ -18,6 +19,8 @@ enum Commands { /// Add a new component dependency #[command(subcommand)] Add(AddCommand), + /// Generates dependency bindings for selected component + Bindings(BindingsCommand), } #[tokio::main] @@ -26,6 +29,7 @@ async fn main() -> Result<()> { match app.command { Commands::Add(cmd) => cmd.run().await?, + Commands::Bindings(cmd) => cmd.run().await?, } Ok(())