wasm_compose/
config.rs

1//! Module for composition configuration.
2
3use anyhow::{Context, Result};
4use indexmap::IndexMap;
5use serde_derive::Deserialize;
6use std::{
7    fs,
8    path::{Path, PathBuf},
9    str::FromStr,
10};
11
12/// An explicit transitive dependency of a composed component.
13#[derive(Debug, Clone, Deserialize, Default)]
14#[serde(rename_all = "kebab-case", deny_unknown_fields)]
15pub struct Dependency {
16    /// The path to the dependency's component file.
17    pub path: PathBuf,
18}
19
20impl FromStr for Dependency {
21    type Err = ();
22
23    fn from_str(s: &str) -> Result<Self, Self::Err> {
24        Ok(Self { path: s.into() })
25    }
26}
27
28/// An argument of an instantiation.
29#[derive(Debug, Clone, Deserialize, Default)]
30#[serde(rename_all = "kebab-case", deny_unknown_fields)]
31pub struct InstantiationArg {
32    /// The name of the instance passed as the argument.
33    pub instance: String,
34
35    /// The name of the instance export to use as the argument.
36    ///
37    /// If `None`, the instance itself will be used as the argument.
38    #[serde(default)]
39    pub export: Option<String>,
40}
41
42impl FromStr for InstantiationArg {
43    type Err = ();
44
45    fn from_str(s: &str) -> Result<Self, Self::Err> {
46        Ok(Self {
47            instance: s.to_string(),
48            export: None,
49        })
50    }
51}
52
53/// An instantiation of a component.
54#[derive(Debug, Clone, Deserialize, Default)]
55#[serde(rename_all = "kebab-case", deny_unknown_fields)]
56pub struct Instantiation {
57    /// The name of the dependency being instantiated.
58    ///
59    /// Defaults to a dependency with the same name as the instantiation.
60    pub dependency: Option<String>,
61
62    /// The explicit instantiation arguments.
63    ///
64    /// Maps the argument name to the name of the instance to pass as
65    /// the argument.
66    #[serde(default, deserialize_with = "de::index_map")]
67    pub arguments: IndexMap<String, InstantiationArg>,
68}
69
70/// The configuration for composing a WebAssembly component.
71#[derive(Default, Debug, Clone, Deserialize)]
72#[serde(rename_all = "kebab-case", deny_unknown_fields)]
73pub struct Config {
74    /// The path of the configuration file's directory.
75    ///
76    /// All paths are relative to this directory.
77    #[serde(skip)]
78    pub dir: PathBuf,
79
80    /// Components whose exports define import dependencies to fulfill from.
81    #[serde(default)]
82    pub definitions: Vec<PathBuf>,
83
84    /// The paths to search when automatically resolving dependencies.
85    ///
86    /// The config directory is always searched first.
87    #[serde(default)]
88    pub search_paths: Vec<PathBuf>,
89
90    /// Whether or not to skip validation of the output component.
91    #[serde(default)]
92    pub skip_validation: bool,
93
94    /// Whether or not to import components in the composed component.
95    ///
96    /// By default, components are defined rather than imported in
97    /// the composed component.
98    #[serde(default)]
99    pub import_components: bool,
100
101    /// Whether or not to disallow instance imports in the output component.
102    ///
103    /// Enabling this option will cause an error if a dependency cannot be
104    /// located.
105    #[serde(default)]
106    pub disallow_imports: bool,
107
108    /// The explicit, transitive dependencies of the root component.
109    #[serde(default, deserialize_with = "de::index_map")]
110    pub dependencies: IndexMap<String, Dependency>,
111
112    /// The explicit instantiations of the composed component.
113    #[serde(default)]
114    pub instantiations: IndexMap<String, Instantiation>,
115}
116
117impl Config {
118    /// Reads a composition configuration from the given path.
119    pub fn from_file(path: impl Into<PathBuf>) -> Result<Self> {
120        let path = path.into();
121
122        log::info!("reading configuration file `{}`", path.display());
123
124        let config = fs::read_to_string(&path)
125            .with_context(|| format!("failed to read configuration file `{}`", path.display()))?;
126
127        let mut config: Config = serde_yaml::from_str(&config)
128            .with_context(|| format!("failed to parse configuration file `{}`", path.display()))?;
129
130        config.dir = path.parent().map(Path::to_path_buf).unwrap_or_default();
131
132        Ok(config)
133    }
134
135    /// Gets the dependency name for the given instance name.
136    pub fn dependency_name<'a>(&'a self, instance: &'a str) -> &'a str {
137        self.instantiations
138            .get(instance)
139            .and_then(|i| i.dependency.as_deref())
140            .unwrap_or(instance)
141    }
142}
143
144mod de {
145    use indexmap::IndexMap;
146    use serde::{
147        Deserialize, Deserializer,
148        de::{self, MapAccess, Visitor},
149    };
150    use std::{fmt, hash::Hash, marker::PhantomData, str::FromStr};
151
152    /// Utility function for deserializing index maps where the values can
153    /// be deserialized either from a string or from a map value.
154    pub fn index_map<'de, K, V, D>(deserializer: D) -> Result<IndexMap<K, V>, D::Error>
155    where
156        K: Hash + Eq + Deserialize<'de>,
157        V: Deserialize<'de> + FromStr<Err = ()>,
158        D: Deserializer<'de>,
159    {
160        deserializer.deserialize_map(MapVisitor(PhantomData))
161    }
162
163    struct MapVisitor<K, V>(PhantomData<(K, V)>);
164
165    impl<'de, K, V> Visitor<'de> for MapVisitor<K, V>
166    where
167        K: Hash + Eq + Deserialize<'de>,
168        V: Deserialize<'de> + FromStr<Err = ()>,
169    {
170        type Value = IndexMap<K, V>;
171
172        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
173            formatter.write_str("map")
174        }
175
176        fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
177        where
178            M: MapAccess<'de>,
179        {
180            struct Wrapper<V>(V);
181
182            impl<'de, V> Deserialize<'de> for Wrapper<V>
183            where
184                V: Deserialize<'de> + FromStr<Err = ()>,
185            {
186                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
187                where
188                    D: Deserializer<'de>,
189                {
190                    Ok(Self(
191                        deserializer.deserialize_any(StringOrMapVisitor(PhantomData))?,
192                    ))
193                }
194            }
195
196            let mut map = Self::Value::with_capacity(access.size_hint().unwrap_or(0));
197            while let Some((key, value)) = access.next_entry::<_, Wrapper<V>>()? {
198                map.insert(key, value.0);
199            }
200
201            Ok(map)
202        }
203    }
204
205    struct StringOrMapVisitor<V>(PhantomData<V>);
206
207    impl<'de, V> Visitor<'de> for StringOrMapVisitor<V>
208    where
209        V: Deserialize<'de> + FromStr<Err = ()>,
210    {
211        type Value = V;
212
213        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
214            formatter.write_str("string or map")
215        }
216
217        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
218        where
219            E: de::Error,
220        {
221            Ok(V::from_str(value).unwrap())
222        }
223
224        fn visit_map<M>(self, access: M) -> Result<Self::Value, M::Error>
225        where
226            M: MapAccess<'de>,
227        {
228            Deserialize::deserialize(de::value::MapAccessDeserializer::new(access))
229        }
230    }
231}