wasm_pkg_common/
config.rs

1use std::{
2    collections::{hash_map::Entry, HashMap},
3    io::ErrorKind,
4    path::{Path, PathBuf},
5};
6
7use etcetera::BaseStrategy;
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    label::Label, metadata::RegistryMetadata, package::PackageRef, registry::Registry, Error,
12};
13
14mod toml;
15
16const DEFAULT_FALLBACK_NAMESPACE_REGISTRIES: &[(&str, &str)] =
17    &[("wasi", "wasi.dev"), ("ba", "bytecodealliance.org")];
18
19/// Wasm Package registry configuration.
20///
21/// Most consumers are expected to start with [`Config::global_defaults`] to
22/// provide a consistent baseline user experience. Where needed, these defaults
23/// can be overridden with application-specific config via [`Config::merge`] or
24/// other mutation methods.
25#[derive(Debug, Clone, Serialize)]
26#[serde(into = "toml::TomlConfig")]
27pub struct Config {
28    default_registry: Option<Registry>,
29    namespace_registries: HashMap<Label, RegistryMapping>,
30    package_registry_overrides: HashMap<PackageRef, RegistryMapping>,
31    // Note: these are only used for hard-coded defaults currently
32    fallback_namespace_registries: HashMap<Label, Registry>,
33    registry_configs: HashMap<Registry, RegistryConfig>,
34}
35
36/// Possible options for namespace configuration.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(untagged)]
39pub enum RegistryMapping {
40    /// Use the given registry address (this will fetch the well-known registry metadata from the given hostname).
41    Registry(Registry),
42    /// Use custom configuration for reaching a registry
43    Custom(CustomConfig),
44}
45
46/// Custom registry configuration
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct CustomConfig {
49    /// A valid name for the registry. This still must be a valid [`Registry`] in that it should
50    /// look like a valid hostname. When doing custom configuration however, this is just used as a
51    /// key to identify the configuration for this namespace
52    pub registry: Registry,
53    /// The metadata for the registry. This is used to determine the protocol to use for the
54    /// registry as well as mapping information for the registry.
55    pub metadata: RegistryMetadata,
56}
57
58impl Default for Config {
59    fn default() -> Self {
60        let fallback_namespace_registries = DEFAULT_FALLBACK_NAMESPACE_REGISTRIES
61            .iter()
62            .map(|(k, v)| (k.parse().unwrap(), v.parse().unwrap()))
63            .collect();
64        Self {
65            default_registry: Default::default(),
66            namespace_registries: Default::default(),
67            package_registry_overrides: Default::default(),
68            fallback_namespace_registries,
69            registry_configs: Default::default(),
70        }
71    }
72}
73
74impl Config {
75    /// Returns an empty config.
76    ///
77    /// Note that this may differ from the `Default` implementation, which
78    /// includes hard-coded global defaults.
79    pub fn empty() -> Self {
80        Self {
81            default_registry: Default::default(),
82            namespace_registries: Default::default(),
83            package_registry_overrides: Default::default(),
84            fallback_namespace_registries: Default::default(),
85            registry_configs: Default::default(),
86        }
87    }
88
89    /// Loads config from several default sources.
90    ///
91    /// The following sources are loaded in this order, with later sources
92    /// merged into (overriding) earlier sources.
93    /// - Hard-coded defaults
94    /// - User-global config file (e.g. `~/.config/wasm-pkg/config.toml`)
95    ///
96    /// Note: This list is expected to expand in the future to include
97    /// "workspace" config files like `./.wasm-pkg/config.toml`.
98    pub async fn global_defaults() -> Result<Self, Error> {
99        let mut config = Self::default();
100        if let Some(global_config) = Self::read_global_config().await? {
101            config.merge(global_config);
102        }
103        Ok(config)
104    }
105
106    /// Returns the default global config file location
107    pub fn global_config_path() -> Option<PathBuf> {
108        etcetera::choose_base_strategy()
109            .ok()
110            .map(|strat| strat.config_dir().join("wasm-pkg").join("config.toml"))
111    }
112
113    /// Reads config from the default global config file location
114    pub async fn read_global_config() -> Result<Option<Self>, Error> {
115        let path = match Config::global_config_path() {
116            Some(path) => path,
117            None => return Ok(None),
118        };
119        let contents = match tokio::fs::read_to_string(&path).await {
120            Ok(contents) => contents,
121            Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None),
122            Err(err) => return Err(Error::ConfigFileIoError(err)),
123        };
124        Ok(Some(Self::from_toml(&contents)?))
125    }
126
127    /// Reads config from a TOML file at the given path.
128    pub async fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
129        let contents = tokio::fs::read_to_string(path)
130            .await
131            .map_err(Error::ConfigFileIoError)?;
132        Self::from_toml(&contents)
133    }
134
135    /// Parses config from the given TOML contents.
136    pub fn from_toml(contents: &str) -> Result<Self, Error> {
137        let toml_cfg: toml::TomlConfig =
138            ::toml::from_str(contents).map_err(Error::invalid_config)?;
139        Ok(toml_cfg.into())
140    }
141
142    /// Writes the config to a TOML file at the given path.
143    pub async fn to_file(&self, path: impl AsRef<Path>) -> Result<(), Error> {
144        let toml_str = ::toml::to_string(&self).map_err(Error::invalid_config)?;
145        tokio::fs::write(path, toml_str)
146            .await
147            .map_err(Error::ConfigFileIoError)
148    }
149
150    /// Merges the given other config into this one.
151    pub fn merge(&mut self, other: Self) {
152        let Self {
153            default_registry,
154            namespace_registries,
155            package_registry_overrides,
156            fallback_namespace_registries,
157            registry_configs,
158        } = other;
159        if default_registry.is_some() {
160            self.default_registry = default_registry;
161        }
162        self.namespace_registries.extend(namespace_registries);
163        self.package_registry_overrides
164            .extend(package_registry_overrides);
165        self.fallback_namespace_registries
166            .extend(fallback_namespace_registries);
167        for (registry, config) in registry_configs {
168            match self.registry_configs.entry(registry) {
169                Entry::Occupied(mut occupied) => occupied.get_mut().merge(config),
170                Entry::Vacant(vacant) => {
171                    vacant.insert(config);
172                }
173            }
174        }
175    }
176
177    /// Resolves a [`Registry`] for the given [`PackageRef`].
178    ///
179    /// Resolution returns the first of these that matches:
180    /// - A package registry exactly matching the package
181    /// - A namespace registry matching the package's namespace
182    /// - The default registry
183    /// - Hard-coded fallbacks for certain well-known namespaces
184    pub fn resolve_registry(&self, package: &PackageRef) -> Option<&Registry> {
185        if let Some(RegistryMapping::Registry(reg)) = self.package_registry_overrides.get(package) {
186            Some(reg)
187        } else if let Some(RegistryMapping::Custom(custom)) =
188            self.package_registry_overrides.get(package)
189        {
190            Some(&custom.registry)
191        } else if let Some(RegistryMapping::Registry(reg)) =
192            self.namespace_registries.get(package.namespace())
193        {
194            Some(reg)
195        } else if let Some(RegistryMapping::Custom(custom)) =
196            self.namespace_registries.get(package.namespace())
197        {
198            Some(&custom.registry)
199        } else if let Some(reg) = self.default_registry.as_ref() {
200            Some(reg)
201        } else if let Some(reg) = self.fallback_namespace_registries.get(package.namespace()) {
202            Some(reg)
203        } else {
204            None
205        }
206    }
207
208    /// Returns the default registry.
209    pub fn default_registry(&self) -> Option<&Registry> {
210        self.default_registry.as_ref()
211    }
212
213    /// Sets the default registry.
214    ///
215    /// To unset the default registry, pass `None`.
216    pub fn set_default_registry(&mut self, registry: Option<Registry>) {
217        self.default_registry = registry;
218    }
219
220    /// Returns a registry for the given namespace.
221    ///
222    /// Does not fall back to the default registry; see
223    /// [`Self::resolve_registry`].
224    pub fn namespace_registry(&self, namespace: &Label) -> Option<&RegistryMapping> {
225        self.namespace_registries.get(namespace)
226    }
227
228    /// Sets a registry for the given namespace.
229    pub fn set_namespace_registry(&mut self, namespace: Label, registry: RegistryMapping) {
230        self.namespace_registries.insert(namespace, registry);
231    }
232
233    /// Returns a registry override configured for the given package.
234    ///
235    /// Does not fall back to namespace or default registries; see
236    /// [`Self::resolve_registry`].
237    pub fn package_registry_override(&self, package: &PackageRef) -> Option<&RegistryMapping> {
238        self.package_registry_overrides.get(package)
239    }
240
241    /// Sets a registry override for the given package.
242    pub fn set_package_registry_override(
243        &mut self,
244        package: PackageRef,
245        registry: RegistryMapping,
246    ) {
247        self.package_registry_overrides.insert(package, registry);
248    }
249
250    /// Returns [`RegistryConfig`] for the given registry.
251    pub fn registry_config(&self, registry: &Registry) -> Option<&RegistryConfig> {
252        self.registry_configs.get(registry)
253    }
254
255    /// Returns a mutable [`RegistryConfig`] for the given registry, inserting
256    /// an empty one if needed.
257    pub fn get_or_insert_registry_config_mut(
258        &mut self,
259        registry: &Registry,
260    ) -> &mut RegistryConfig {
261        if !self.registry_configs.contains_key(registry) {
262            self.registry_configs
263                .insert(registry.clone(), Default::default());
264        }
265        self.registry_configs.get_mut(registry).unwrap()
266    }
267}
268
269#[derive(Clone, Default)]
270pub struct RegistryConfig {
271    default_backend: Option<String>,
272    backend_configs: HashMap<String, ::toml::Table>,
273}
274
275impl RegistryConfig {
276    /// Merges the given other config into this one.
277    pub fn merge(&mut self, other: Self) {
278        let Self {
279            default_backend: backend_type,
280            backend_configs,
281        } = other;
282        if backend_type.is_some() {
283            self.default_backend = backend_type;
284        }
285        for (ty, config) in backend_configs {
286            match self.backend_configs.entry(ty) {
287                Entry::Occupied(mut occupied) => occupied.get_mut().extend(config),
288                Entry::Vacant(vacant) => {
289                    vacant.insert(config);
290                }
291            }
292        }
293    }
294
295    /// Returns default backend type, if one is configured. If none are configured and there is only
296    /// one type of configured backend, this will return that type.
297    pub fn default_backend(&self) -> Option<&str> {
298        match self.default_backend.as_deref() {
299            Some(ty) => Some(ty),
300            None => {
301                if self.backend_configs.len() == 1 {
302                    self.backend_configs.keys().next().map(|ty| ty.as_str())
303                } else {
304                    None
305                }
306            }
307        }
308    }
309
310    /// Sets the default backend type.
311    ///
312    /// To unset the default backend type, pass `None`.
313    pub fn set_default_backend(&mut self, default_backend: Option<String>) {
314        self.default_backend = default_backend;
315    }
316
317    /// Returns an iterator of configured backend types.
318    pub fn configured_backend_types(&self) -> impl Iterator<Item = &str> {
319        self.backend_configs.keys().map(|ty| ty.as_str())
320    }
321
322    /// Attempts to deserialize backend config with the given type.
323    ///
324    /// Returns `Ok(None)` if no configuration was provided.
325    /// Returns `Err` if configuration was provided but deserialization failed.
326    pub fn backend_config<'a, T: Deserialize<'a>>(
327        &'a self,
328        backend_type: &str,
329    ) -> Result<Option<T>, Error> {
330        let Some(table) = self.backend_configs.get(backend_type) else {
331            return Ok(None);
332        };
333        let config = table.clone().try_into().map_err(Error::invalid_config)?;
334        Ok(Some(config))
335    }
336
337    /// Set the backend config of the given type by serializing the given config.
338    pub fn set_backend_config<T: Serialize>(
339        &mut self,
340        backend_type: impl Into<String>,
341        backend_config: T,
342    ) -> Result<(), Error> {
343        let table = ::toml::Table::try_from(backend_config).map_err(Error::invalid_config)?;
344        self.backend_configs.insert(backend_type.into(), table);
345        Ok(())
346    }
347}
348
349impl std::fmt::Debug for RegistryConfig {
350    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351        f.debug_struct("RegistryConfig")
352            .field("backend_type", &self.default_backend)
353            .field(
354                "backend_configs",
355                &DebugBackendConfigs(&self.backend_configs),
356            )
357            .finish()
358    }
359}
360
361// Redact backend configs, which may contain sensitive values.
362struct DebugBackendConfigs<'a>(&'a HashMap<String, ::toml::Table>);
363
364impl std::fmt::Debug for DebugBackendConfigs<'_> {
365    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366        f.debug_map()
367            .entries(self.0.keys().map(|ty| (ty, &"<HIDDEN>")))
368            .finish()
369    }
370}