asimov_module/models/
module_manifest.rs

1// This is free and unencumbered software released into the public domain.
2
3use alloc::{string::String, vec::Vec};
4
5#[derive(Clone, Debug, Default)]
6#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
7pub struct ModuleManifest {
8    pub name: String,
9    pub label: String,
10    pub summary: String,
11    pub links: Vec<String>,
12
13    #[cfg_attr(
14        feature = "serde",
15        serde(default, skip_serializing_if = "Provides::is_empty")
16    )]
17    pub provides: Provides,
18
19    #[cfg_attr(
20        feature = "serde",
21        serde(default, skip_serializing_if = "Handles::is_empty")
22    )]
23    pub handles: Handles,
24
25    #[cfg_attr(
26        feature = "serde",
27        serde(alias = "configuration", skip_serializing_if = "Option::is_none")
28    )]
29    pub config: Option<Configuration>,
30}
31
32#[cfg(feature = "std")]
33#[derive(Debug, thiserror::Error)]
34pub enum ReadVarError {
35    #[error("variable named `{0}` not found in module manifest")]
36    UnknownVar(String),
37
38    #[error("a value for variable `{0}` was not configured")]
39    UnconfiguredVar(String),
40
41    #[error("failed to read variable `{name}`: {source}")]
42    Io {
43        name: String,
44        #[source]
45        source: std::io::Error,
46    },
47}
48
49impl ModuleManifest {
50    #[cfg(all(feature = "std", feature = "serde"))]
51    pub fn read_manifest(module_name: &str) -> std::io::Result<Self> {
52        let directory = asimov_env::paths::asimov_root().join("modules");
53        let search_paths = [
54            ("installed", "json"),
55            ("installed", "yaml"), // legacy, new installs are converted to JSON
56            ("", "yaml"),          // legacy, new installs go to `installed/`
57        ];
58
59        for (sub_dir, ext) in search_paths {
60            let file = std::path::PathBuf::from(sub_dir)
61                .join(module_name)
62                .with_extension(ext);
63
64            match std::fs::read(directory.join(&file)) {
65                Ok(content) if ext == "json" => {
66                    return serde_json::from_slice(&content).map_err(std::io::Error::other);
67                },
68                Ok(content) if ext == "yaml" => {
69                    return serde_yaml_ng::from_slice(&content).map_err(std::io::Error::other);
70                },
71                Ok(_) => unreachable!(),
72
73                Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
74                Err(err) => return Err(err),
75            }
76        }
77
78        Err(std::io::ErrorKind::NotFound.into())
79    }
80
81    #[cfg(feature = "std")]
82    pub fn read_variables(
83        &self,
84        profile: Option<&str>,
85    ) -> Result<std::collections::BTreeMap<String, String>, ReadVarError> {
86        self.config
87            .as_ref()
88            .map(|c| c.variables.as_slice())
89            .unwrap_or_default()
90            .iter()
91            .map(|var| Ok((var.name.clone(), self.variable(&var.name, profile)?)))
92            .collect()
93    }
94
95    #[cfg(feature = "std")]
96    pub fn variable(&self, key: &str, profile: Option<&str>) -> Result<String, ReadVarError> {
97        let Some(var) = self
98            .config
99            .as_ref()
100            .and_then(|conf| conf.variables.iter().find(|var| var.name == key))
101        else {
102            return Err(ReadVarError::UnknownVar(key.into()));
103        };
104
105        if let Some(value) = var
106            .environment
107            .as_deref()
108            .and_then(|env_name| std::env::var(env_name).ok())
109        {
110            return Ok(value);
111        }
112
113        let profile = profile.unwrap_or("default");
114        let path = asimov_env::paths::asimov_root()
115            .join("configs")
116            .join(profile)
117            .join(&self.name)
118            .join(key);
119
120        std::fs::read_to_string(&path).or_else(|err| {
121            if err.kind() == std::io::ErrorKind::NotFound {
122                var.default_value
123                    .clone()
124                    .ok_or_else(|| ReadVarError::UnconfiguredVar(key.into()))
125            } else {
126                Err(ReadVarError::Io {
127                    name: key.into(),
128                    source: err,
129                })
130            }
131        })
132    }
133}
134
135#[derive(Clone, Debug, Default)]
136#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
137pub struct Provides {
138    pub programs: Vec<String>,
139}
140
141impl Provides {
142    pub fn is_empty(&self) -> bool {
143        self.programs.is_empty()
144    }
145}
146
147#[derive(Clone, Debug, Default)]
148#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
149pub struct Handles {
150    #[cfg_attr(
151        feature = "serde",
152        serde(default, skip_serializing_if = "Vec::is_empty")
153    )]
154    pub url_protocols: Vec<String>,
155
156    #[cfg_attr(
157        feature = "serde",
158        serde(default, skip_serializing_if = "Vec::is_empty")
159    )]
160    pub url_prefixes: Vec<String>,
161
162    #[cfg_attr(
163        feature = "serde",
164        serde(default, skip_serializing_if = "Vec::is_empty")
165    )]
166    pub url_patterns: Vec<String>,
167
168    #[cfg_attr(
169        feature = "serde",
170        serde(default, skip_serializing_if = "Vec::is_empty")
171    )]
172    pub file_extensions: Vec<String>,
173
174    #[cfg_attr(
175        feature = "serde",
176        serde(default, skip_serializing_if = "Vec::is_empty")
177    )]
178    pub content_types: Vec<String>,
179}
180
181impl Handles {
182    pub fn is_empty(&self) -> bool {
183        self.url_protocols.is_empty()
184            && self.url_prefixes.is_empty()
185            && self.url_patterns.is_empty()
186            && self.file_extensions.is_empty()
187            && self.content_types.is_empty()
188    }
189}
190
191#[derive(Clone, Debug, Default)]
192#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
193pub struct Configuration {
194    #[cfg_attr(
195        feature = "serde",
196        serde(default, skip_serializing_if = "Vec::is_empty")
197    )]
198    pub variables: Vec<ConfigurationVariable>,
199}
200
201#[derive(Clone, Debug, Default)]
202#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
203pub struct ConfigurationVariable {
204    /// The name of the variable. Configured variables are by default saved in
205    /// `~/.asimov/configs/$profile/$module/$name`.
206    pub name: String,
207
208    /// Optional description to provide information about the variable.
209    #[cfg_attr(
210        feature = "serde",
211        serde(default, alias = "desc", skip_serializing_if = "Option::is_none")
212    )]
213    pub description: Option<String>,
214
215    /// Optional name of an environment variable to check for a value before checking for a
216    /// configured or a default value.
217    #[cfg_attr(
218        feature = "serde",
219        serde(default, alias = "env", skip_serializing_if = "Option::is_none")
220    )]
221    pub environment: Option<String>,
222
223    /// Optional default value to use as a fallback. If a default value is present the user
224    /// configuration of the value is not required.
225    #[cfg_attr(
226        feature = "serde",
227        serde(default, alias = "default", skip_serializing_if = "Option::is_none")
228    )]
229    pub default_value: Option<String>,
230}