use std::{collections::HashSet, fmt::Display, fs, path::PathBuf};
use anyhow::{anyhow, bail, Context, Result};
use cargo_toml::{Manifest, Product};
use config::Config;
use semver::Version;
use serde::Deserialize;
use wasmcloud_control_interface::{RegistryCredential, RegistryCredentialMap};
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum LanguageConfig {
Rust(RustConfig),
TinyGo(TinyGoConfig),
}
#[allow(clippy::large_enum_variant)]
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum TypeConfig {
Actor(ActorConfig),
Provider(ProviderConfig),
Interface(InterfaceConfig),
}
#[derive(Deserialize, Debug, Clone)]
pub struct ProjectConfig {
pub language: LanguageConfig,
#[serde(rename = "type")]
pub project_type: TypeConfig,
pub common: CommonConfig,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct ActorConfig {
pub claims: Vec<String>,
pub push_insecure: bool,
pub key_directory: PathBuf,
pub call_alias: Option<String>,
pub wasm_target: WasmTarget,
pub wasi_preview2_adapter_path: Option<PathBuf>,
pub wit_world: Option<String>,
pub tags: Option<HashSet<String>>,
pub build_artifact: Option<PathBuf>,
pub build_command: Option<String>,
pub destination: Option<PathBuf>,
}
impl RustConfig {
pub fn build_target(&self, wasm_target: &WasmTarget) -> &'static str {
match wasm_target {
WasmTarget::CoreModule => "wasm32-unknown-unknown",
WasmTarget::WasiPreview1 | WasmTarget::WasiPreview2 => "wasm32-wasi",
}
}
}
#[derive(Deserialize, Debug, PartialEq)]
struct RawActorConfig {
pub claims: Option<Vec<String>>,
pub push_insecure: Option<bool>,
pub key_directory: Option<PathBuf>,
pub wasm_target: Option<String>,
pub wasi_preview2_adapter_path: Option<PathBuf>,
pub call_alias: Option<String>,
pub wit_world: Option<String>,
pub tags: Option<HashSet<String>>,
pub build_artifact: Option<PathBuf>,
pub build_command: Option<String>,
pub destination: Option<PathBuf>,
}
impl TryFrom<RawActorConfig> for ActorConfig {
type Error = anyhow::Error;
fn try_from(raw_config: RawActorConfig) -> Result<Self> {
Ok(Self {
claims: raw_config.claims.unwrap_or_default(),
push_insecure: raw_config.push_insecure.unwrap_or(false),
key_directory: raw_config
.key_directory
.unwrap_or_else(|| PathBuf::from("./keys")),
wasm_target: raw_config
.wasm_target
.map(WasmTarget::from)
.unwrap_or_default(),
wasi_preview2_adapter_path: raw_config.wasi_preview2_adapter_path,
call_alias: raw_config.call_alias,
wit_world: raw_config.wit_world,
tags: raw_config.tags,
build_command: raw_config.build_command,
build_artifact: raw_config.build_artifact,
destination: raw_config.destination,
})
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct ProviderConfig {
pub capability_id: String,
pub vendor: String,
}
#[derive(Deserialize, Debug, PartialEq)]
struct RawProviderConfig {
pub capability_id: String,
pub vendor: Option<String>,
}
impl TryFrom<RawProviderConfig> for ProviderConfig {
type Error = anyhow::Error;
fn try_from(raw_config: RawProviderConfig) -> Result<Self> {
Ok(Self {
capability_id: raw_config.capability_id,
vendor: raw_config.vendor.unwrap_or_else(|| "NoVendor".to_string()),
})
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct InterfaceConfig {
pub html_target: PathBuf,
pub codegen_config: PathBuf,
}
#[derive(Deserialize, Debug, PartialEq)]
struct RawInterfaceConfig {
pub html_target: Option<PathBuf>,
pub codegen_config: Option<PathBuf>,
}
impl TryFrom<RawInterfaceConfig> for InterfaceConfig {
type Error = anyhow::Error;
fn try_from(raw_config: RawInterfaceConfig) -> Result<Self> {
Ok(Self {
html_target: raw_config
.html_target
.unwrap_or_else(|| PathBuf::from("./html")),
codegen_config: raw_config
.codegen_config
.unwrap_or_else(|| PathBuf::from("./codegen.toml")),
})
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct RustConfig {
pub cargo_path: Option<PathBuf>,
pub target_path: Option<PathBuf>,
}
#[derive(Deserialize, Debug, PartialEq, Default, Clone)]
struct RawRustConfig {
pub cargo_path: Option<PathBuf>,
pub target_path: Option<PathBuf>,
}
impl TryFrom<RawRustConfig> for RustConfig {
type Error = anyhow::Error;
fn try_from(raw_config: RawRustConfig) -> Result<Self> {
Ok(Self {
cargo_path: raw_config.cargo_path,
target_path: raw_config.target_path,
})
}
}
#[derive(Deserialize, Debug, PartialEq, Default, Clone)]
struct RawRegistryConfig {
url: Option<String>,
credentials: Option<PathBuf>,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct RegistryConfig {
pub url: Option<String>,
pub credentials: Option<PathBuf>,
}
impl TryFrom<RawRegistryConfig> for RegistryConfig {
type Error = anyhow::Error;
fn try_from(raw_config: RawRegistryConfig) -> Result<Self> {
Ok(Self {
url: raw_config.url,
credentials: raw_config.credentials,
})
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct CommonConfig {
pub name: String,
pub version: Version,
pub revision: i32,
pub path: PathBuf,
pub wasm_bin_name: Option<String>,
pub registry: RegistryConfig,
}
impl CommonConfig {
pub fn wasm_bin_name(&self) -> String {
self.wasm_bin_name
.clone()
.unwrap_or_else(|| self.name.clone())
}
}
#[derive(Debug, Deserialize, Default, Clone, Eq, PartialEq)]
pub enum WasmTarget {
#[default]
#[serde(alias = "wasm32-unknown-unknown")]
CoreModule,
#[serde(alias = "wasm32-wasi", alias = "wasm32-wasi-preview1")]
WasiPreview1,
#[serde(alias = "wasm32-wasi-preview2")]
WasiPreview2,
}
impl From<&str> for WasmTarget {
fn from(value: &str) -> Self {
match value {
"wasm32-wasi-preview1" => WasmTarget::WasiPreview1,
"wasm32-wasi" => WasmTarget::WasiPreview1,
"wasm32-wasi-preview2" => WasmTarget::WasiPreview2,
_ => WasmTarget::CoreModule,
}
}
}
impl From<String> for WasmTarget {
fn from(value: String) -> Self {
value.as_str().into()
}
}
impl Display for WasmTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match &self {
WasmTarget::CoreModule => "wasm32-unknown-unknown",
WasmTarget::WasiPreview1 => "wasm32-wasi",
WasmTarget::WasiPreview2 => "wasm32-wasi-preview2",
})
}
}
#[derive(Deserialize, Debug)]
struct RawProjectConfig {
pub language: String,
#[serde(rename = "type")]
pub project_type: String,
pub name: Option<String>,
pub version: Option<Version>,
#[serde(default)]
pub revision: i32,
pub actor: Option<RawActorConfig>,
pub interface: Option<RawInterfaceConfig>,
pub provider: Option<RawProviderConfig>,
pub rust: Option<RawRustConfig>,
pub tinygo: Option<RawTinyGoConfig>,
pub registry: Option<RawRegistryConfig>,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct TinyGoConfig {
pub tinygo_path: Option<PathBuf>,
}
impl TinyGoConfig {
pub fn build_target(&self, wasm_target: &WasmTarget) -> &'static str {
match wasm_target {
WasmTarget::CoreModule => "wasm",
WasmTarget::WasiPreview1 | WasmTarget::WasiPreview2 => "wasi",
}
}
}
#[derive(Deserialize, Debug, PartialEq, Default)]
struct RawTinyGoConfig {
pub tinygo_path: Option<PathBuf>,
}
impl TryFrom<RawTinyGoConfig> for TinyGoConfig {
type Error = anyhow::Error;
fn try_from(raw: RawTinyGoConfig) -> Result<Self> {
Ok(Self {
tinygo_path: raw.tinygo_path,
})
}
}
pub fn get_config(opt_path: Option<PathBuf>, use_env: Option<bool>) -> Result<ProjectConfig> {
let mut path = opt_path.unwrap_or_else(|| PathBuf::from("."));
if !path.exists() {
bail!("path {} does not exist", path.display());
}
path = fs::canonicalize(path)?;
let (project_path, wasmcloud_path) = if path.is_dir() {
let wasmcloud_path = path.join("wasmcloud.toml");
if !wasmcloud_path.is_file() {
bail!("no wasmcloud.toml file found in {}", path.display());
}
(path, wasmcloud_path)
} else if path.is_file() {
(
path.parent()
.ok_or_else(|| anyhow!("Could not get parent path of wasmcloud.toml file"))?
.to_path_buf(),
path,
)
} else {
bail!("no wasmcloud.toml file found in {}", path.display());
};
let mut config = Config::builder().add_source(config::File::from(wasmcloud_path.clone()));
if use_env.unwrap_or(true) {
config = config.add_source(config::Environment::with_prefix("WASMCLOUD"));
}
let json_value = config
.build()
.map_err(|e| {
if e.to_string().contains("is not of a registered file format") {
return anyhow!("invalid config file: {}", wasmcloud_path.display());
}
anyhow!("{}", e)
})?
.try_deserialize::<serde_json::Value>()?;
let raw_project_config: RawProjectConfig = serde_json::from_value(json_value)?;
raw_project_config
.convert(project_path)
.map_err(|e: anyhow::Error| anyhow!("{} in {}", e, wasmcloud_path.display()))
}
impl RawProjectConfig {
fn build_common_config_from_cargo_project(
project_path: PathBuf,
name: Option<String>,
version: Option<Version>,
revision: i32,
registry: RegistryConfig,
) -> Result<CommonConfig> {
let cargo_toml_path = project_path.join("Cargo.toml");
if !cargo_toml_path.is_file() {
bail!(
"missing/invalid Cargo.toml path [{}]",
cargo_toml_path.display(),
);
}
let mut cargo_toml = Manifest::from_path(cargo_toml_path)?;
cargo_toml.complete_from_path(&project_path)?;
let cargo_pkg = cargo_toml
.package
.ok_or_else(|| anyhow!("Missing package information in Cargo.toml"))?;
let version = match version {
Some(version) => version,
None => Version::parse(cargo_pkg.version.get()?.as_str())?,
};
let name = name.unwrap_or(cargo_pkg.name);
let wasm_bin_name = match cargo_toml.lib {
Some(Product {
name: Some(lib_name),
..
}) => Some(lib_name),
_ => None,
};
Ok(CommonConfig {
name,
version,
revision,
path: project_path,
wasm_bin_name,
registry,
})
}
pub fn convert(self, project_path: PathBuf) -> Result<ProjectConfig> {
let project_type_config = match self.project_type.trim().to_lowercase().as_str() {
"actor" => {
let actor_config = self.actor.context("missing actor config")?;
TypeConfig::Actor(actor_config.try_into()?)
}
"provider" => TypeConfig::Provider(
self.provider
.context("missing provider config")?
.try_into()?,
),
"interface" => TypeConfig::Interface(
self.interface
.context("missing interface config")?
.try_into()?,
),
_ => {
bail!("unknown project type: {}", self.project_type);
}
};
let language_config = match self.language.trim().to_lowercase().as_str() {
"rust" => match self.rust {
Some(rust_config) => LanguageConfig::Rust(rust_config.try_into()?),
None => LanguageConfig::Rust(RustConfig::default()),
},
"tinygo" => match self.tinygo {
Some(tinygo_config) => LanguageConfig::TinyGo(tinygo_config.try_into()?),
None => LanguageConfig::TinyGo(TinyGoConfig::default()),
},
_ => {
bail!("unknown language in wasmcloud.toml: {}", self.language);
}
};
let registry_config = self
.registry
.map(RegistryConfig::try_from)
.transpose()?
.unwrap_or_default();
let common_config_result: Result<CommonConfig> = match language_config {
LanguageConfig::Rust(_) => {
match Self::build_common_config_from_cargo_project(
project_path.clone(),
self.name.clone(),
self.version.clone(),
self.revision,
registry_config.clone(),
) {
Ok(cfg) => Ok(cfg),
Err(_) if self.name.is_some() && self.version.is_some() => Ok(CommonConfig {
name: self.name.unwrap(),
version: self.version.unwrap(),
revision: self.revision,
path: project_path,
wasm_bin_name: None,
registry: registry_config,
}),
Err(err) => {
bail!("No Cargo.toml file found in the current directory, and name/version unspecified: {err}")
}
}
}
LanguageConfig::TinyGo(_) => Ok(CommonConfig {
name: self
.name
.ok_or_else(|| anyhow!("Missing name in wasmcloud.toml"))?,
version: self
.version
.ok_or_else(|| anyhow!("Missing version in wasmcloud.toml"))?,
revision: self.revision,
path: project_path,
wasm_bin_name: None,
registry: registry_config,
}),
};
Ok(ProjectConfig {
language: language_config,
project_type: project_type_config,
common: common_config_result?,
})
}
}
impl ProjectConfig {
pub async fn resolve_registry_credentials(
&self,
registry: impl AsRef<str>,
) -> Result<RegistryCredential> {
let credentials_file = &self.common.registry.credentials.to_owned();
let Some(credentials_file) = credentials_file else {
return Ok(RegistryCredential::default());
};
if !credentials_file.exists() {
return Ok(RegistryCredential::default());
}
let credentials = tokio::fs::read_to_string(&credentials_file)
.await
.context(format!(
"Failed to read registry credentials file {}",
credentials_file.display()
))?;
let credentials =
serde_json::from_str::<RegistryCredentialMap>(&credentials).context(format!(
"Failed to parse registry credentials from file {}",
credentials_file.display()
))?;
let Some(credentials) = credentials.get(registry.as_ref()) else {
return Ok(RegistryCredential::default());
};
Ok(credentials.clone())
}
}