use std::{
collections::{hash_map::Entry, HashMap},
io::ErrorKind,
path::{Path, PathBuf},
};
use etcetera::BaseStrategy;
use serde::{Deserialize, Serialize};
use crate::{
label::Label, metadata::RegistryMetadata, package::PackageRef, registry::Registry, Error,
};
mod toml;
const DEFAULT_FALLBACK_NAMESPACE_REGISTRIES: &[(&str, &str)] =
&[("wasi", "wasi.dev"), ("ba", "bytecodealliance.org")];
#[derive(Debug, Clone, Serialize)]
#[serde(into = "toml::TomlConfig")]
pub struct Config {
default_registry: Option<Registry>,
namespace_registries: HashMap<Label, RegistryMapping>,
package_registry_overrides: HashMap<PackageRef, RegistryMapping>,
fallback_namespace_registries: HashMap<Label, Registry>,
registry_configs: HashMap<Registry, RegistryConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RegistryMapping {
Registry(Registry),
Custom(CustomConfig),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomConfig {
pub registry: Registry,
pub metadata: RegistryMetadata,
}
impl Default for Config {
fn default() -> Self {
let fallback_namespace_registries = DEFAULT_FALLBACK_NAMESPACE_REGISTRIES
.iter()
.map(|(k, v)| (k.parse().unwrap(), v.parse().unwrap()))
.collect();
Self {
default_registry: Default::default(),
namespace_registries: Default::default(),
package_registry_overrides: Default::default(),
fallback_namespace_registries,
registry_configs: Default::default(),
}
}
}
impl Config {
pub fn empty() -> Self {
Self {
default_registry: Default::default(),
namespace_registries: Default::default(),
package_registry_overrides: Default::default(),
fallback_namespace_registries: Default::default(),
registry_configs: Default::default(),
}
}
pub async fn global_defaults() -> Result<Self, Error> {
let mut config = Self::default();
if let Some(global_config) = Self::read_global_config().await? {
config.merge(global_config);
}
Ok(config)
}
pub fn global_config_path() -> Option<PathBuf> {
etcetera::choose_base_strategy()
.ok()
.map(|strat| strat.config_dir().join("wasm-pkg").join("config.toml"))
}
pub async fn read_global_config() -> Result<Option<Self>, Error> {
let path = match Config::global_config_path() {
Some(path) => path,
None => return Ok(None),
};
let contents = match tokio::fs::read_to_string(&path).await {
Ok(contents) => contents,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(Error::ConfigFileIoError(err)),
};
Ok(Some(Self::from_toml(&contents)?))
}
pub async fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
let contents = tokio::fs::read_to_string(path)
.await
.map_err(Error::ConfigFileIoError)?;
Self::from_toml(&contents)
}
pub fn from_toml(contents: &str) -> Result<Self, Error> {
let toml_cfg: toml::TomlConfig =
::toml::from_str(contents).map_err(Error::invalid_config)?;
Ok(toml_cfg.into())
}
pub async fn to_file(&self, path: impl AsRef<Path>) -> Result<(), Error> {
let toml_str = ::toml::to_string(&self).map_err(Error::invalid_config)?;
tokio::fs::write(path, toml_str)
.await
.map_err(Error::ConfigFileIoError)
}
pub fn merge(&mut self, other: Self) {
let Self {
default_registry,
namespace_registries,
package_registry_overrides,
fallback_namespace_registries,
registry_configs,
} = other;
if default_registry.is_some() {
self.default_registry = default_registry;
}
self.namespace_registries.extend(namespace_registries);
self.package_registry_overrides
.extend(package_registry_overrides);
self.fallback_namespace_registries
.extend(fallback_namespace_registries);
for (registry, config) in registry_configs {
match self.registry_configs.entry(registry) {
Entry::Occupied(mut occupied) => occupied.get_mut().merge(config),
Entry::Vacant(vacant) => {
vacant.insert(config);
}
}
}
}
pub fn resolve_registry(&self, package: &PackageRef) -> Option<&Registry> {
if let Some(RegistryMapping::Registry(reg)) = self.package_registry_overrides.get(package) {
Some(reg)
} else if let Some(RegistryMapping::Custom(custom)) =
self.package_registry_overrides.get(package)
{
Some(&custom.registry)
} else if let Some(RegistryMapping::Registry(reg)) =
self.namespace_registries.get(package.namespace())
{
Some(reg)
} else if let Some(RegistryMapping::Custom(custom)) =
self.namespace_registries.get(package.namespace())
{
Some(&custom.registry)
} else if let Some(reg) = self.default_registry.as_ref() {
Some(reg)
} else if let Some(reg) = self.fallback_namespace_registries.get(package.namespace()) {
Some(reg)
} else {
None
}
}
pub fn default_registry(&self) -> Option<&Registry> {
self.default_registry.as_ref()
}
pub fn set_default_registry(&mut self, registry: Option<Registry>) {
self.default_registry = registry;
}
pub fn namespace_registry(&self, namespace: &Label) -> Option<&RegistryMapping> {
self.namespace_registries.get(namespace)
}
pub fn set_namespace_registry(&mut self, namespace: Label, registry: RegistryMapping) {
self.namespace_registries.insert(namespace, registry);
}
pub fn package_registry_override(&self, package: &PackageRef) -> Option<&RegistryMapping> {
self.package_registry_overrides.get(package)
}
pub fn set_package_registry_override(
&mut self,
package: PackageRef,
registry: RegistryMapping,
) {
self.package_registry_overrides.insert(package, registry);
}
pub fn registry_config(&self, registry: &Registry) -> Option<&RegistryConfig> {
self.registry_configs.get(registry)
}
pub fn get_or_insert_registry_config_mut(
&mut self,
registry: &Registry,
) -> &mut RegistryConfig {
if !self.registry_configs.contains_key(registry) {
self.registry_configs
.insert(registry.clone(), Default::default());
}
self.registry_configs.get_mut(registry).unwrap()
}
}
#[derive(Clone, Default)]
pub struct RegistryConfig {
default_backend: Option<String>,
backend_configs: HashMap<String, ::toml::Table>,
}
impl RegistryConfig {
pub fn merge(&mut self, other: Self) {
let Self {
default_backend: backend_type,
backend_configs,
} = other;
if backend_type.is_some() {
self.default_backend = backend_type;
}
for (ty, config) in backend_configs {
match self.backend_configs.entry(ty) {
Entry::Occupied(mut occupied) => occupied.get_mut().extend(config),
Entry::Vacant(vacant) => {
vacant.insert(config);
}
}
}
}
pub fn default_backend(&self) -> Option<&str> {
match self.default_backend.as_deref() {
Some(ty) => Some(ty),
None => {
if self.backend_configs.len() == 1 {
self.backend_configs.keys().next().map(|ty| ty.as_str())
} else {
None
}
}
}
}
pub fn set_default_backend(&mut self, default_backend: Option<String>) {
self.default_backend = default_backend;
}
pub fn configured_backend_types(&self) -> impl Iterator<Item = &str> {
self.backend_configs.keys().map(|ty| ty.as_str())
}
pub fn backend_config<'a, T: Deserialize<'a>>(
&'a self,
backend_type: &str,
) -> Result<Option<T>, Error> {
let Some(table) = self.backend_configs.get(backend_type) else {
return Ok(None);
};
let config = table.clone().try_into().map_err(Error::invalid_config)?;
Ok(Some(config))
}
pub fn set_backend_config<T: Serialize>(
&mut self,
backend_type: impl Into<String>,
backend_config: T,
) -> Result<(), Error> {
let table = ::toml::Table::try_from(backend_config).map_err(Error::invalid_config)?;
self.backend_configs.insert(backend_type.into(), table);
Ok(())
}
}
impl std::fmt::Debug for RegistryConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RegistryConfig")
.field("backend_type", &self.default_backend)
.field(
"backend_configs",
&DebugBackendConfigs(&self.backend_configs),
)
.finish()
}
}
struct DebugBackendConfigs<'a>(&'a HashMap<String, ::toml::Table>);
impl std::fmt::Debug for DebugBackendConfigs<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_map()
.entries(self.0.keys().map(|ty| (ty, &"<HIDDEN>")))
.finish()
}
}