1use 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"), ("", "yaml"), ];
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 pub name: String,
207
208 #[cfg_attr(
210 feature = "serde",
211 serde(default, alias = "desc", skip_serializing_if = "Option::is_none")
212 )]
213 pub description: Option<String>,
214
215 #[cfg_attr(
218 feature = "serde",
219 serde(default, alias = "env", skip_serializing_if = "Option::is_none")
220 )]
221 pub environment: Option<String>,
222
223 #[cfg_attr(
226 feature = "serde",
227 serde(default, alias = "default", skip_serializing_if = "Option::is_none")
228 )]
229 pub default_value: Option<String>,
230}