1use alloc::{collections::BTreeMap, string::String, vec::Vec};
4
5#[derive(Clone, Debug, Default, PartialEq, Eq)]
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 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
32 pub requires: Option<Requires>,
33}
34
35#[cfg(feature = "std")]
36#[derive(Debug, thiserror::Error)]
37pub enum ReadVarError {
38 #[error("variable named `{0}` not found in module manifest")]
39 UnknownVar(String),
40
41 #[error("a value for variable `{0}` was not configured")]
42 UnconfiguredVar(String),
43
44 #[error("failed to read variable `{name}`: {source}")]
45 Io {
46 name: String,
47 #[source]
48 source: std::io::Error,
49 },
50}
51
52impl ModuleManifest {
53 #[cfg(all(feature = "std", feature = "serde"))]
54 pub fn read_manifest(module_name: &str) -> std::io::Result<Self> {
55 let directory = asimov_env::paths::asimov_root().join("modules");
56 let search_paths = [
57 ("installed", "json"),
58 ("installed", "yaml"), ("", "yaml"), ];
61
62 for (sub_dir, ext) in search_paths {
63 let file = std::path::PathBuf::from(sub_dir)
64 .join(module_name)
65 .with_extension(ext);
66
67 match std::fs::read(directory.join(&file)) {
68 Ok(content) if ext == "json" => {
69 return serde_json::from_slice(&content).map_err(std::io::Error::other);
70 },
71 Ok(content) if ext == "yaml" => {
72 return serde_yaml_ng::from_slice(&content).map_err(std::io::Error::other);
73 },
74 Ok(_) => unreachable!(),
75
76 Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
77 Err(err) => return Err(err),
78 }
79 }
80
81 Err(std::io::ErrorKind::NotFound.into())
82 }
83
84 #[cfg(feature = "std")]
85 pub fn read_variables(
86 &self,
87 profile: Option<&str>,
88 ) -> Result<std::collections::BTreeMap<String, String>, ReadVarError> {
89 self.config
90 .as_ref()
91 .map(|c| c.variables.as_slice())
92 .unwrap_or_default()
93 .iter()
94 .map(|var| Ok((var.name.clone(), self.variable(&var.name, profile)?)))
95 .collect()
96 }
97
98 #[cfg(feature = "std")]
99 pub fn variable(&self, key: &str, profile: Option<&str>) -> Result<String, ReadVarError> {
100 let Some(var) = self
101 .config
102 .as_ref()
103 .and_then(|conf| conf.variables.iter().find(|var| var.name == key))
104 else {
105 return Err(ReadVarError::UnknownVar(key.into()));
106 };
107
108 if let Some(value) = var
109 .environment
110 .as_deref()
111 .and_then(|env_name| std::env::var(env_name).ok())
112 {
113 return Ok(value);
114 }
115
116 let profile = profile.unwrap_or("default");
117 let path = asimov_env::paths::asimov_root()
118 .join("configs")
119 .join(profile)
120 .join(&self.name)
121 .join(key);
122
123 std::fs::read_to_string(&path).or_else(|err| {
124 if err.kind() == std::io::ErrorKind::NotFound {
125 var.default_value
126 .clone()
127 .ok_or_else(|| ReadVarError::UnconfiguredVar(key.into()))
128 } else {
129 Err(ReadVarError::Io {
130 name: key.into(),
131 source: err,
132 })
133 }
134 })
135 }
136}
137
138#[derive(Clone, Debug, Default, PartialEq, Eq)]
139#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
140pub struct Provides {
141 pub programs: Vec<String>,
142}
143
144impl Provides {
145 pub fn is_empty(&self) -> bool {
146 self.programs.is_empty()
147 }
148}
149
150#[cfg(feature = "serde")]
151fn empty_vec_if_null<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
152where
153 D: serde::Deserializer<'de>,
154 T: serde::Deserialize<'de>,
155{
156 use serde::Deserialize;
157 Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
158}
159
160#[derive(Clone, Debug, Default, PartialEq, Eq)]
161#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
162pub struct Handles {
163 #[cfg_attr(
164 feature = "serde",
165 serde(
166 default,
167 deserialize_with = "empty_vec_if_null",
168 skip_serializing_if = "Vec::is_empty"
169 )
170 )]
171 pub url_protocols: Vec<String>,
172
173 #[cfg_attr(
174 feature = "serde",
175 serde(
176 default,
177 deserialize_with = "empty_vec_if_null",
178 skip_serializing_if = "Vec::is_empty"
179 )
180 )]
181 pub url_prefixes: Vec<String>,
182
183 #[cfg_attr(
184 feature = "serde",
185 serde(
186 default,
187 deserialize_with = "empty_vec_if_null",
188 skip_serializing_if = "Vec::is_empty"
189 )
190 )]
191 pub url_patterns: Vec<String>,
192
193 #[cfg_attr(
194 feature = "serde",
195 serde(
196 default,
197 deserialize_with = "empty_vec_if_null",
198 skip_serializing_if = "Vec::is_empty"
199 )
200 )]
201 pub file_extensions: Vec<String>,
202
203 #[cfg_attr(
204 feature = "serde",
205 serde(
206 default,
207 deserialize_with = "empty_vec_if_null",
208 skip_serializing_if = "Vec::is_empty"
209 )
210 )]
211 pub content_types: Vec<String>,
212}
213
214impl Handles {
215 pub fn is_empty(&self) -> bool {
216 self.url_protocols.is_empty()
217 && self.url_prefixes.is_empty()
218 && self.url_patterns.is_empty()
219 && self.file_extensions.is_empty()
220 && self.content_types.is_empty()
221 }
222}
223
224#[derive(Clone, Debug, Default, PartialEq, Eq)]
225#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
226pub struct Configuration {
227 #[cfg_attr(
228 feature = "serde",
229 serde(default, skip_serializing_if = "Vec::is_empty")
230 )]
231 pub variables: Vec<ConfigurationVariable>,
232}
233
234#[derive(Clone, Debug, Default, PartialEq, Eq)]
235#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
236pub struct ConfigurationVariable {
237 pub name: String,
240
241 #[cfg_attr(
243 feature = "serde",
244 serde(default, alias = "desc", skip_serializing_if = "Option::is_none")
245 )]
246 pub description: Option<String>,
247
248 #[cfg_attr(
251 feature = "serde",
252 serde(default, alias = "env", skip_serializing_if = "Option::is_none")
253 )]
254 pub environment: Option<String>,
255
256 #[cfg_attr(
259 feature = "serde",
260 serde(default, alias = "default", skip_serializing_if = "Option::is_none")
261 )]
262 pub default_value: Option<String>,
263}
264
265#[derive(Clone, Debug, Default, PartialEq, Eq)]
266#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
267pub struct Requires {
268 #[cfg_attr(
270 feature = "serde",
271 serde(
272 default,
273 deserialize_with = "empty_vec_if_null",
274 skip_serializing_if = "Vec::is_empty"
275 )
276 )]
277 pub modules: Vec<String>,
278
279 #[cfg_attr(
280 feature = "serde",
281 serde(default, skip_serializing_if = "BTreeMap::is_empty")
282 )]
283 pub models: BTreeMap<String, RequiredModel>,
284}
285
286#[derive(Clone, Debug, PartialEq, Eq)]
287#[cfg_attr(
288 feature = "serde",
289 derive(serde::Deserialize, serde::Serialize),
290 serde(untagged)
291)]
292pub enum RequiredModel {
293 Url(String),
298
299 #[cfg_attr(
307 feature = "serde",
308 serde(deserialize_with = "ordered::deserialize_ordered")
309 )]
310 Choices(Vec<(String, String)>),
311}
312
313#[cfg(feature = "serde")]
314mod ordered {
315 use super::*;
316 use serde::{
317 Deserializer,
318 de::{MapAccess, Visitor},
319 };
320 use std::fmt;
321
322 pub fn deserialize_ordered<'de, D>(deserializer: D) -> Result<Vec<(String, String)>, D::Error>
323 where
324 D: Deserializer<'de>,
325 {
326 struct OrderedVisitor;
327
328 impl<'de> Visitor<'de> for OrderedVisitor {
329 type Value = Vec<(String, String)>;
330
331 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
332 f.write_str("a map of string keys to string values (preserving order)")
333 }
334
335 fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
336 where
337 A: MapAccess<'de>,
338 {
339 let mut items = Vec::with_capacity(access.size_hint().unwrap_or(0));
340 while let Some((k, v)) = access.next_entry::<String, String>()? {
341 items.push((k, v));
342 }
343 Ok(items)
344 }
345 }
346
347 deserializer.deserialize_map(OrderedVisitor)
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use std::vec;
355
356 #[test]
357 fn test_deser() {
358 let yaml = r#"
359name: example
360label: Example
361summary: Example Module
362links:
363 - https://github.com/asimov-platform/asimov.rs/tree/master/lib/asimov-module
364
365provides:
366 programs:
367 - asimov-example-module
368
369handles:
370 content_types:
371 - content_type
372 file_extensions:
373 - file_extension
374 url_patterns:
375 - pattern
376 url_prefixes:
377 - prefix
378 url_protocols:
379 - protocol
380
381config:
382 variables:
383 - name: api_key
384 description: "api key to authorize requests"
385 default_value: "foobar"
386 environment: API_KEY
387
388requires:
389 modules:
390 - other
391 models:
392 hf:first/model: first_url
393 hf:second/model:
394 small: small_url
395 medium: medium_url
396 large: large_url
397"#;
398
399 let dec: ModuleManifest = serde_yaml_ng::from_str(yaml).expect("deser should succeed");
400
401 assert_eq!("example", dec.name);
402 assert_eq!("Example", dec.label);
403 assert_eq!("Example Module", dec.summary);
404
405 assert_eq!(
406 vec!["https://github.com/asimov-platform/asimov.rs/tree/master/lib/asimov-module"],
407 dec.links
408 );
409
410 assert_eq!(1, dec.provides.programs.len());
411 assert_eq!(
412 "asimov-example-module",
413 dec.provides.programs.first().unwrap()
414 );
415
416 assert_eq!(
417 "content_type",
418 dec.handles
419 .content_types
420 .first()
421 .expect("should have content_types")
422 );
423
424 assert_eq!(
425 "file_extension",
426 dec.handles
427 .file_extensions
428 .first()
429 .expect("should have file_extensions")
430 );
431
432 assert_eq!(
433 "pattern",
434 dec.handles
435 .url_patterns
436 .first()
437 .expect("should have url_patterns")
438 );
439
440 assert_eq!(
441 "prefix",
442 dec.handles
443 .url_prefixes
444 .first()
445 .expect("should have url_prefixes")
446 );
447
448 assert_eq!(
449 "protocol",
450 dec.handles
451 .url_protocols
452 .first()
453 .expect("should have url_protocols")
454 );
455
456 assert_eq!(
457 Some(&ConfigurationVariable {
458 name: "api_key".into(),
459 description: Some("api key to authorize requests".into()),
460 environment: Some("API_KEY".into()),
461 default_value: Some("foobar".into())
462 }),
463 dec.config.expect("should have config").variables.first()
464 );
465
466 let requires = dec.requires.expect("should have requires");
467
468 assert_eq!(1, requires.modules.len());
469 assert_eq!("other", requires.modules.first().unwrap());
470
471 assert_eq!(2, requires.models.len());
472
473 assert_eq!(
474 RequiredModel::Url("first_url".into()),
475 requires.models["hf:first/model"]
476 );
477
478 assert_eq!(
479 RequiredModel::Choices(vec![
480 ("small".into(), "small_url".into()),
481 ("medium".into(), "medium_url".into()),
482 ("large".into(), "large_url".into())
483 ]),
484 requires.models["hf:second/model"]
485 );
486 }
487}