use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use semver::VersionReq;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;
use wasm_metadata::{Link, LinkType, RegistryMetadata};
pub const CONFIG_FILE_NAME: &str = "wkg.toml";
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct Config {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overrides: Option<HashMap<String, Override>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
}
impl Config {
pub async fn load_from_path(path: impl AsRef<Path>) -> Result<Config> {
let contents = tokio::fs::read_to_string(path)
.await
.context("unable to load config from file")?;
let config: Config = toml::from_str(&contents).context("unable to parse config file")?;
Ok(config)
}
pub async fn load() -> Result<Config> {
let config_path = PathBuf::from(CONFIG_FILE_NAME);
if !tokio::fs::try_exists(&config_path).await? {
return Ok(Config::default());
}
Self::load_from_path(config_path).await
}
pub async fn write(&self, path: impl AsRef<Path>) -> Result<()> {
let contents = toml::to_string_pretty(self)?;
let mut file = tokio::fs::File::create(path).await?;
file.write_all(contents.as_bytes())
.await
.context("unable to write config to path")
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct Override {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<VersionReq>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct Metadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authors: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub categories: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub documentation: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub homepage: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repository: Option<String>,
}
impl From<Metadata> for wasm_metadata::RegistryMetadata {
fn from(value: Metadata) -> Self {
let mut meta = RegistryMetadata::default();
meta.set_authors(value.authors);
meta.set_categories(value.categories);
meta.set_description(value.description);
meta.set_license(value.license);
let mut links = Vec::new();
if let Some(documentation) = value.documentation {
links.push(Link {
ty: LinkType::Documentation,
value: documentation,
});
}
if let Some(homepage) = value.homepage {
links.push(Link {
ty: LinkType::Homepage,
value: homepage,
});
}
if let Some(repository) = value.repository {
links.push(Link {
ty: LinkType::Repository,
value: repository,
});
}
meta.set_links((!links.is_empty()).then_some(links));
meta
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_roundtrip() {
let tempdir = tempfile::tempdir().unwrap();
let config_path = tempdir.path().join(CONFIG_FILE_NAME);
let config = Config {
overrides: Some(HashMap::from([(
"foo:bar".to_string(),
Override {
path: Some(PathBuf::from("bar")),
version: Some(VersionReq::parse("1.0.0").unwrap()),
},
)])),
metadata: Some(Metadata {
authors: Some(vec!["foo".to_string(), "bar".to_string()]),
categories: Some(vec!["foo".to_string(), "bar".to_string()]),
description: Some("foo".to_string()),
license: Some("foo".to_string()),
documentation: Some("foo".to_string()),
homepage: Some("foo".to_string()),
repository: Some("foo".to_string()),
}),
};
config
.write(&config_path)
.await
.expect("unable to write config");
let loaded_config = Config::load_from_path(config_path)
.await
.expect("unable to load config");
assert_eq!(
config, loaded_config,
"config loaded from file does not match original config"
);
}
}