asimov_module/models/
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 path = asimov_env::paths::asimov_root()
53            .join("modules")
54            .join(std::format!("{module_name}.yaml"));
55        let content = std::fs::read(&path)?;
56        serde_yml::from_slice(&content).map_err(std::io::Error::other)
57    }
58
59    #[cfg(feature = "std")]
60    pub fn read_variables(
61        &self,
62        profile: Option<&str>,
63    ) -> Result<std::collections::BTreeMap<String, String>, ReadVarError> {
64        self.config
65            .as_ref()
66            .map(|c| c.variables.as_slice())
67            .unwrap_or_default()
68            .iter()
69            .map(|var| Ok((var.name.clone(), self.variable(&var.name, profile)?)))
70            .collect()
71    }
72
73    #[cfg(feature = "std")]
74    pub fn variable(&self, key: &str, profile: Option<&str>) -> Result<String, ReadVarError> {
75        let Some(var) = self
76            .config
77            .as_ref()
78            .and_then(|conf| conf.variables.iter().find(|var| var.name == key))
79        else {
80            return Err(ReadVarError::UnknownVar(key.into()));
81        };
82
83        if let Some(value) = var
84            .environment
85            .as_deref()
86            .and_then(|env_name| std::env::var(env_name).ok())
87        {
88            return Ok(value);
89        }
90
91        let profile = profile.unwrap_or("default");
92        let path = asimov_env::paths::asimov_root()
93            .join("configs")
94            .join(profile)
95            .join(&self.name)
96            .join(key);
97
98        std::fs::read_to_string(&path).or_else(|err| {
99            if err.kind() == std::io::ErrorKind::NotFound {
100                var.default_value
101                    .clone()
102                    .ok_or_else(|| ReadVarError::UnconfiguredVar(key.into()))
103            } else {
104                Err(ReadVarError::Io {
105                    name: key.into(),
106                    source: err,
107                })
108            }
109        })
110    }
111}
112
113#[derive(Clone, Debug, Default)]
114#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
115pub struct Provides {
116    pub programs: Vec<String>,
117}
118
119impl Provides {
120    pub fn is_empty(&self) -> bool {
121        self.programs.is_empty()
122    }
123}
124
125#[derive(Clone, Debug, Default)]
126#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
127pub struct Handles {
128    #[cfg_attr(
129        feature = "serde",
130        serde(default, skip_serializing_if = "Vec::is_empty")
131    )]
132    pub url_protocols: Vec<String>,
133
134    #[cfg_attr(
135        feature = "serde",
136        serde(default, skip_serializing_if = "Vec::is_empty")
137    )]
138    pub url_prefixes: Vec<String>,
139
140    #[cfg_attr(
141        feature = "serde",
142        serde(default, skip_serializing_if = "Vec::is_empty")
143    )]
144    pub url_patterns: Vec<String>,
145
146    #[cfg_attr(
147        feature = "serde",
148        serde(default, skip_serializing_if = "Vec::is_empty")
149    )]
150    pub file_extensions: Vec<String>,
151
152    #[cfg_attr(
153        feature = "serde",
154        serde(default, skip_serializing_if = "Vec::is_empty")
155    )]
156    pub content_types: Vec<String>,
157}
158
159impl Handles {
160    pub fn is_empty(&self) -> bool {
161        self.url_protocols.is_empty()
162            && self.url_prefixes.is_empty()
163            && self.url_patterns.is_empty()
164            && self.file_extensions.is_empty()
165            && self.content_types.is_empty()
166    }
167}
168
169#[derive(Clone, Debug, Default)]
170#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
171pub struct Configuration {
172    #[cfg_attr(
173        feature = "serde",
174        serde(default, skip_serializing_if = "Vec::is_empty")
175    )]
176    pub variables: Vec<ConfigurationVariable>,
177}
178
179#[derive(Clone, Debug, Default)]
180#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
181pub struct ConfigurationVariable {
182    /// The name of the variable. Configured variables are by default saved in
183    /// `~/.asimov/configs/$profile/$module/$name`.
184    pub name: String,
185
186    /// Optional description to provide information about the variable.
187    #[cfg_attr(
188        feature = "serde",
189        serde(default, alias = "desc", skip_serializing_if = "Option::is_none")
190    )]
191    pub description: Option<String>,
192
193    /// Optional name of an environment variable to check for a value before checking for a
194    /// configured or a default value.
195    #[cfg_attr(
196        feature = "serde",
197        serde(default, alias = "env", skip_serializing_if = "Option::is_none")
198    )]
199    pub environment: Option<String>,
200
201    /// Optional default value to use as a fallback. If a default value is present the user
202    /// configuration of the value is not required.
203    #[cfg_attr(
204        feature = "serde",
205        serde(default, alias = "default", skip_serializing_if = "Option::is_none")
206    )]
207    pub default_value: Option<String>,
208}