use anyhow::{anyhow, bail, Result};
use cargo_toml::{Manifest, Product};
use config::Config;
use semver::Version;
use std::{fs, path::PathBuf};
#[derive(serde::Deserialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum LanguageConfig {
Rust(RustConfig),
TinyGo(TinyGoConfig),
}
#[derive(serde::Deserialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum TypeConfig {
Actor(ActorConfig),
Provider(ProviderConfig),
Interface(InterfaceConfig),
}
#[derive(serde::Deserialize, Debug, Clone)]
pub struct ProjectConfig {
pub language: LanguageConfig,
#[serde(rename = "type")]
pub project_type: TypeConfig,
pub common: CommonConfig,
}
#[derive(serde::Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct ActorConfig {
pub claims: Vec<String>,
pub registry: Option<String>,
pub push_insecure: bool,
pub key_directory: PathBuf,
pub filename: Option<String>,
pub wasm_target: String,
pub call_alias: Option<String>,
}
#[derive(serde::Deserialize, Debug, PartialEq)]
struct RawActorConfig {
pub claims: Option<Vec<String>>,
pub registry: Option<String>,
pub push_insecure: Option<bool>,
pub key_directory: Option<PathBuf>,
pub filename: Option<String>,
pub wasm_target: Option<String>,
pub call_alias: Option<String>,
}
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(),
registry: raw_config.registry,
push_insecure: raw_config.push_insecure.unwrap_or(false),
key_directory: raw_config
.key_directory
.unwrap_or_else(|| PathBuf::from("./keys")),
filename: raw_config.filename,
wasm_target: raw_config
.wasm_target
.unwrap_or_else(|| "wasm32-unknown-unknown".to_string()),
call_alias: raw_config.call_alias,
})
}
}
#[derive(serde::Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct ProviderConfig {
pub capability_id: String,
pub vendor: String,
}
#[derive(serde::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(serde::Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct InterfaceConfig {
pub html_target: PathBuf,
pub codegen_config: PathBuf,
}
#[derive(serde::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(serde::Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct RustConfig {
pub cargo_path: Option<PathBuf>,
pub target_path: Option<PathBuf>,
}
#[derive(serde::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(serde::Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct CommonConfig {
pub name: String,
pub version: Version,
pub path: PathBuf,
pub wasm_bin_name: Option<String>,
}
#[derive(serde::Deserialize, Debug)]
struct RawProjectConfig {
pub language: String,
#[serde(rename = "type")]
pub project_type: String,
pub name: Option<String>,
pub version: Option<Version>,
pub actor: Option<RawActorConfig>,
pub provider: Option<RawProviderConfig>,
pub rust: Option<RawRustConfig>,
pub interface: Option<RawInterfaceConfig>,
pub tinygo: Option<RawTinyGoConfig>,
}
#[derive(serde::Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct TinyGoConfig {
pub tinygo_path: Option<PathBuf>,
}
#[derive(serde::Deserialize, Debug, PartialEq, Default)]
struct RawTinyGoConfig {
pub tinygo_path: Option<PathBuf>,
}
impl TryFrom<RawTinyGoConfig> for TinyGoConfig {
type Error = anyhow::Error;
fn try_from(raw_config: RawTinyGoConfig) -> Result<Self> {
Ok(Self {
tinygo_path: raw_config.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() {
return Err(anyhow!("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() {
return Err(anyhow!(
"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 {
return Err(anyhow!(
"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>,
) -> Result<CommonConfig> {
let cargo_toml_path = project_path.join("Cargo.toml");
if !cargo_toml_path.is_file() {
return Err(anyhow!(
"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,
path: project_path,
wasm_bin_name,
})
}
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.ok_or_else(|| anyhow!("Missing actor config"))?;
TypeConfig::Actor(actor_config.try_into()?)
}
"provider" => {
let provider_config = self
.provider
.ok_or_else(|| anyhow!("Missing provider config"))?;
TypeConfig::Provider(provider_config.try_into()?)
}
"interface" => {
let interface_config = self
.interface
.ok_or_else(|| anyhow!("Missing interface config"))?;
TypeConfig::Interface(interface_config.try_into()?)
}
_ => {
return Err(anyhow!("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()),
},
_ => {
return Err(anyhow!(
"Unknown language in wasmcloud.toml: {}",
self.language
));
}
};
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(),
) {
Ok(cfg) => Ok(cfg),
Err(_) if self.name.is_some() && self.version.is_some() => Ok(CommonConfig {
name: self.name.unwrap(),
version: self.version.unwrap(),
path: project_path,
wasm_bin_name: None,
}),
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"))?,
path: project_path,
wasm_bin_name: None,
}),
};
Ok(ProjectConfig {
language: language_config,
project_type: project_type_config,
common: common_config_result?,
})
}
}