Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
Signed-off-by: karthik2804 <[email protected]>
  • Loading branch information
karthik2804 committed Sep 24, 2024
1 parent 6409a9a commit ccab564
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 205 deletions.
250 changes: 46 additions & 204 deletions src/commands/add.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -40,60 +43,50 @@ 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(
resolve.select_world(main, None)?,
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<String> {
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<String> {
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<Vec<String>> {
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<String, Vec<String>> = HashMap::new();
let mut selected_interfaces: Vec<String> = Vec::new();
Expand All @@ -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();

Expand All @@ -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());
Expand All @@ -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,
Expand Down Expand Up @@ -253,93 +185,3 @@ impl AddCommand {
Ok(())
}
}

/// Converts a Resolve object to WIT content.
fn resolve_to_wit(resolve: &Resolve, package_id: PackageId) -> Result<String> {
let mut printer = WitPrinter::default();
printer.emit_docs(false);

let ids = resolve
.packages
.iter()
.map(|(id, _)| id)
.filter(|id| *id != package_id)
.collect::<Vec<_>>();

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<String> {
let manifest_path = get_spin_manifest_path()?;
let manifest = fs::read_to_string(manifest_path).await?;
let mut doc = manifest.parse::<DocumentMut>()?;

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<PathBuf> {
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:[email protected];
world deps {
}
"#;
20 changes: 20 additions & 0 deletions src/commands/bindings.rs
Original file line number Diff line number Diff line change
@@ -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<BindingsLanguage>,
pub component_id: Option<String>,
}

impl BindingsCommand {
pub async fn run(&self) -> Result<()> {
Ok(())
}
}
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod add;
pub mod bindings;
3 changes: 3 additions & 0 deletions src/common/constants.rs
Original file line number Diff line number Diff line change
@@ -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";
22 changes: 22 additions & 0 deletions src/common/interact.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use anyhow::Result;

use dialoguer::{MultiSelect, Select};

pub fn select_prompt(
prompt: &str,
selection_list: &[String],
default: Option<usize>,
) -> Result<usize> {
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<Vec<usize>> {
Ok(MultiSelect::new()
.with_prompt(prompt)
.items(selection_list)
.interact()?)
}
Loading

0 comments on commit ccab564

Please sign in to comment.