wasm_pkg_core/
config.rs

1//! Type definitions and functions for working with `wkg.toml` files.
2
3use std::{
4    collections::HashMap,
5    path::{Path, PathBuf},
6};
7
8use anyhow::{Context, Result};
9use semver::VersionReq;
10use serde::{Deserialize, Serialize};
11use tokio::io::AsyncWriteExt;
12
13/// The default name of the configuration file.
14pub const CONFIG_FILE_NAME: &str = "wkg.toml";
15
16/// The structure for a wkg.toml configuration file. This file is entirely optional and is used for
17/// overriding and annotating wasm packages.
18#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
19#[serde(deny_unknown_fields)]
20pub struct Config {
21    /// Overrides for various packages
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub overrides: Option<HashMap<String, Override>>,
24    /// Additional metadata about the package. This will override any metadata already set by other
25    /// tools.
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub metadata: Option<Metadata>,
28}
29
30impl Config {
31    /// Loads a configuration file from the given path.
32    pub async fn load_from_path(path: impl AsRef<Path>) -> Result<Config> {
33        let contents = tokio::fs::read_to_string(path)
34            .await
35            .context("unable to load config from file")?;
36        let config: Config = toml::from_str(&contents).context("unable to parse config file")?;
37        Ok(config)
38    }
39
40    /// Attempts to load the configuration from the current directory. Most of the time, users of this
41    /// crate should use this function. Right now it just checks for a `wkg.toml` file in the current
42    /// directory, but we could add more resolution logic in the future. If the file is not found, a
43    /// default empty config is returned.
44    pub async fn load() -> Result<Config> {
45        let config_path = PathBuf::from(CONFIG_FILE_NAME);
46        if !tokio::fs::try_exists(&config_path).await? {
47            return Ok(Config::default());
48        }
49        Self::load_from_path(config_path).await
50    }
51
52    /// Serializes and writes the configuration to the given path.
53    pub async fn write(&self, path: impl AsRef<Path>) -> Result<()> {
54        let contents = toml::to_string_pretty(self)?;
55        let mut file = tokio::fs::File::create(path).await?;
56        file.write_all(contents.as_bytes())
57            .await
58            .context("unable to write config to path")
59    }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
63#[serde(deny_unknown_fields)]
64pub struct Override {
65    /// A path to the package on disk. If this is set, the package will be loaded from the given
66    /// path. If this is not set, the package will be loaded from the registry.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub path: Option<PathBuf>,
69    /// Overrides the version of a package specified in a world file. This is for advanced use only
70    /// and may break things.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub version: Option<VersionReq>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
76#[serde(deny_unknown_fields)]
77pub struct Metadata {
78    /// The author(s) of the package.
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub author: Option<String>,
81    /// The package description.
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub description: Option<String>,
84    /// The package license.
85    #[serde(default, skip_serializing_if = "Option::is_none", alias = "license")]
86    pub licenses: Option<String>,
87    /// The package source code URL.
88    #[serde(default, skip_serializing_if = "Option::is_none", alias = "repository")]
89    pub source: Option<String>,
90    /// The package homepage URL.
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub homepage: Option<String>,
93    /// The package source control revision.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub revision: Option<String>,
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[tokio::test]
103    async fn test_roundtrip() {
104        let tempdir = tempfile::tempdir().unwrap();
105        let config_path = tempdir.path().join(CONFIG_FILE_NAME);
106        let config = Config {
107            overrides: Some(HashMap::from([(
108                "foo:bar".to_string(),
109                Override {
110                    path: Some(PathBuf::from("bar")),
111                    version: Some(VersionReq::parse("1.0.0").unwrap()),
112                },
113            )])),
114            metadata: Some(Metadata {
115                author: Some("Foo Bar".to_string()),
116                description: Some("Foobar baz".to_string()),
117                licenses: Some("FBB".to_string()),
118                source: Some("https://gitfoo/bar".to_string()),
119                homepage: Some("https://foo.bar".to_string()),
120                revision: Some("f00ba4".to_string()),
121            }),
122        };
123
124        config
125            .write(&config_path)
126            .await
127            .expect("unable to write config");
128        let loaded_config = Config::load_from_path(config_path)
129            .await
130            .expect("unable to load config");
131        assert_eq!(
132            config, loaded_config,
133            "config loaded from file does not match original config"
134        );
135    }
136}