1use alloc::{collections::BTreeMap, string::String, vec::Vec};
4
5#[derive(Clone, Debug, Default, PartialEq, Eq)]
7#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
8pub struct ModuleManifest {
9 pub name: String,
11
12 #[cfg_attr(
14 feature = "serde",
15 serde(default, skip_serializing_if = "Option::is_none")
16 )]
17 pub label: Option<String>,
18
19 #[cfg_attr(
21 feature = "serde",
22 serde(default, skip_serializing_if = "Option::is_none")
23 )]
24 pub title: Option<String>,
25
26 #[cfg_attr(
28 feature = "serde",
29 serde(default, skip_serializing_if = "Option::is_none")
30 )]
31 pub summary: Option<String>,
32
33 #[cfg_attr(
35 feature = "serde",
36 serde(
37 default,
38 deserialize_with = "empty_vec_if_null",
39 skip_serializing_if = "Vec::is_empty"
40 )
41 )]
42 pub links: Vec<String>,
43
44 #[cfg_attr(
46 feature = "serde",
47 serde(
48 default,
49 deserialize_with = "empty_vec_if_null",
50 skip_serializing_if = "Vec::is_empty"
51 )
52 )]
53 pub tags: Vec<String>,
54
55 #[cfg_attr(
57 feature = "serde",
58 serde(default, skip_serializing_if = "Requires::is_empty")
59 )]
60 pub requires: Requires,
61
62 #[cfg_attr(
64 feature = "serde",
65 serde(default, skip_serializing_if = "Provides::is_empty")
66 )]
67 pub provides: Provides,
68
69 #[cfg_attr(
71 feature = "serde",
72 serde(default, skip_serializing_if = "Handles::is_empty")
73 )]
74 pub handles: Handles,
75
76 #[cfg_attr(
77 feature = "serde",
78 serde(
79 default,
80 alias = "configuration",
81 skip_serializing_if = "Option::is_none"
82 )
83 )]
84 pub config: Option<Configuration>,
85}
86
87#[cfg(feature = "std")]
88#[derive(Debug, thiserror::Error)]
89pub enum ReadVarError {
90 #[error("variable named `{0}` not found in module manifest")]
91 UnknownVar(String),
92
93 #[error("a value for variable `{0}` was not configured")]
94 UnconfiguredVar(String),
95
96 #[error("failed to read variable `{name}`: {source}")]
97 Io {
98 name: String,
99 #[source]
100 source: std::io::Error,
101 },
102}
103
104impl ModuleManifest {
105 #[cfg(all(feature = "std", feature = "serde"))]
106 pub fn read_manifest(module_name: &str) -> std::io::Result<Self> {
107 let directory = asimov_env::paths::asimov_root().join("modules");
108 let search_paths = [
109 ("installed", "json"),
110 ("installed", "yaml"), ("", "yaml"), ];
113
114 for (sub_dir, ext) in search_paths {
115 let file = std::path::PathBuf::from(sub_dir)
116 .join(module_name)
117 .with_extension(ext);
118
119 match std::fs::read(directory.join(&file)) {
120 Ok(content) if ext == "json" => {
121 return serde_json::from_slice(&content).map_err(std::io::Error::other);
122 },
123 Ok(content) if ext == "yaml" => {
124 return serde_yaml_ng::from_slice(&content).map_err(std::io::Error::other);
125 },
126 Ok(_) => unreachable!(),
127
128 Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
129 Err(err) => return Err(err),
130 }
131 }
132
133 Err(std::io::ErrorKind::NotFound.into())
134 }
135
136 #[cfg(feature = "std")]
137 pub fn read_variables(
138 &self,
139 profile: Option<&str>,
140 ) -> Result<alloc::collections::BTreeMap<String, String>, ReadVarError> {
141 self.config
142 .as_ref()
143 .map(|c| c.variables.as_slice())
144 .unwrap_or_default()
145 .iter()
146 .map(|var| Ok((var.name.clone(), self.variable(&var.name, profile)?)))
147 .collect()
148 }
149
150 #[cfg(feature = "std")]
151 pub fn variable(&self, key: &str, profile: Option<&str>) -> Result<String, ReadVarError> {
152 let Some(var) = self
153 .config
154 .as_ref()
155 .and_then(|conf| conf.variables.iter().find(|var| var.name == key))
156 else {
157 return Err(ReadVarError::UnknownVar(key.into()));
158 };
159
160 if let Some(value) = var
161 .environment
162 .as_deref()
163 .and_then(|env_name| std::env::var(env_name).ok())
164 {
165 return Ok(value);
166 }
167
168 let profile = profile.unwrap_or("default");
169 let path = asimov_env::paths::asimov_root()
170 .join("configs")
171 .join(profile)
172 .join(&self.name)
173 .join(key);
174
175 std::fs::read_to_string(&path).or_else(|err| {
176 if err.kind() == std::io::ErrorKind::NotFound {
177 var.default_value
178 .clone()
179 .ok_or_else(|| ReadVarError::UnconfiguredVar(key.into()))
180 } else {
181 Err(ReadVarError::Io {
182 name: key.into(),
183 source: err,
184 })
185 }
186 })
187 }
188}
189
190#[derive(Clone, Debug, Default, PartialEq, Eq)]
191#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
192pub struct Requires {
193 #[cfg_attr(
195 feature = "serde",
196 serde(
197 default,
198 deserialize_with = "empty_vec_if_null",
199 skip_serializing_if = "Vec::is_empty"
200 )
201 )]
202 pub modules: Vec<String>,
203
204 #[cfg_attr(
206 feature = "serde",
207 serde(
208 default,
209 deserialize_with = "empty_vec_if_null",
210 skip_serializing_if = "Vec::is_empty"
211 )
212 )]
213 pub platforms: Vec<String>,
214
215 #[cfg_attr(
217 feature = "serde",
218 serde(
219 default,
220 deserialize_with = "empty_vec_if_null",
221 skip_serializing_if = "Vec::is_empty"
222 )
223 )]
224 pub programs: Vec<String>,
225
226 #[cfg_attr(
228 feature = "serde",
229 serde(
230 default,
231 deserialize_with = "empty_vec_if_null",
232 skip_serializing_if = "Vec::is_empty"
233 )
234 )]
235 pub libraries: Vec<String>,
236
237 #[cfg_attr(
239 feature = "serde",
240 serde(default, skip_serializing_if = "BTreeMap::is_empty")
241 )]
242 pub models: BTreeMap<String, RequiredModel>,
243
244 #[cfg_attr(
246 feature = "serde",
247 serde(
248 default,
249 deserialize_with = "empty_vec_if_null",
250 skip_serializing_if = "Vec::is_empty"
251 )
252 )]
253 pub datasets: Vec<String>,
254
255 #[cfg_attr(
257 feature = "serde",
258 serde(
259 default,
260 deserialize_with = "empty_vec_if_null",
261 skip_serializing_if = "Vec::is_empty"
262 )
263 )]
264 pub ontologies: Vec<String>,
265
266 #[cfg_attr(
268 feature = "serde",
269 serde(
270 default,
271 deserialize_with = "empty_vec_if_null",
272 skip_serializing_if = "Vec::is_empty"
273 )
274 )]
275 pub classes: Vec<String>,
276
277 #[cfg_attr(
279 feature = "serde",
280 serde(
281 default,
282 deserialize_with = "empty_vec_if_null",
283 skip_serializing_if = "Vec::is_empty"
284 )
285 )]
286 pub datatypes: Vec<String>,
287}
288
289impl Requires {
290 pub fn is_empty(&self) -> bool {
291 self.modules.is_empty() && self.models.is_empty()
292 }
293}
294
295#[derive(Clone, Debug, PartialEq, Eq)]
296#[cfg_attr(
297 feature = "serde",
298 derive(serde::Deserialize, serde::Serialize),
299 serde(untagged)
300)]
301pub enum RequiredModel {
302 Url(String),
307
308 #[cfg_attr(
316 feature = "serde",
317 serde(deserialize_with = "ordered::deserialize_ordered")
318 )]
319 Choices(Vec<(String, String)>),
320}
321
322#[derive(Clone, Debug, Default, PartialEq, Eq)]
323#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
324pub struct Provides {
325 #[cfg_attr(
326 feature = "serde",
327 serde(
328 default,
329 deserialize_with = "empty_vec_if_null",
330 skip_serializing_if = "Vec::is_empty"
331 )
332 )]
333 pub programs: Vec<String>,
334}
335
336impl Provides {
337 pub fn is_empty(&self) -> bool {
338 self.programs.is_empty()
339 }
340}
341
342#[derive(Clone, Debug, Default, PartialEq, Eq)]
343#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
344pub struct Handles {
345 #[cfg_attr(
346 feature = "serde",
347 serde(
348 default,
349 deserialize_with = "empty_vec_if_null",
350 skip_serializing_if = "Vec::is_empty"
351 )
352 )]
353 pub url_protocols: Vec<String>,
354
355 #[cfg_attr(
356 feature = "serde",
357 serde(
358 default,
359 deserialize_with = "empty_vec_if_null",
360 skip_serializing_if = "Vec::is_empty"
361 )
362 )]
363 pub url_prefixes: Vec<String>,
364
365 #[cfg_attr(
366 feature = "serde",
367 serde(
368 default,
369 deserialize_with = "empty_vec_if_null",
370 skip_serializing_if = "Vec::is_empty"
371 )
372 )]
373 pub url_patterns: Vec<String>,
374
375 #[cfg_attr(
376 feature = "serde",
377 serde(
378 default,
379 deserialize_with = "empty_vec_if_null",
380 skip_serializing_if = "Vec::is_empty"
381 )
382 )]
383 pub file_extensions: Vec<String>,
384
385 #[cfg_attr(
386 feature = "serde",
387 serde(
388 default,
389 deserialize_with = "empty_vec_if_null",
390 skip_serializing_if = "Vec::is_empty"
391 )
392 )]
393 pub content_types: Vec<String>,
394}
395
396impl Handles {
397 pub fn is_empty(&self) -> bool {
398 self.url_protocols.is_empty()
399 && self.url_prefixes.is_empty()
400 && self.url_patterns.is_empty()
401 && self.file_extensions.is_empty()
402 && self.content_types.is_empty()
403 }
404}
405
406#[derive(Clone, Debug, Default, PartialEq, Eq)]
407#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
408pub struct Configuration {
409 #[cfg_attr(
410 feature = "serde",
411 serde(default, skip_serializing_if = "Vec::is_empty")
412 )]
413 pub variables: Vec<ConfigurationVariable>,
414}
415
416#[derive(Clone, Debug, Default, PartialEq, Eq)]
417#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
418pub struct ConfigurationVariable {
419 pub name: String,
422
423 #[cfg_attr(
425 feature = "serde",
426 serde(default, alias = "desc", skip_serializing_if = "Option::is_none")
427 )]
428 pub description: Option<String>,
429
430 #[cfg_attr(
433 feature = "serde",
434 serde(default, alias = "env", skip_serializing_if = "Option::is_none")
435 )]
436 pub environment: Option<String>,
437
438 #[cfg_attr(
441 feature = "serde",
442 serde(default, alias = "default", skip_serializing_if = "Option::is_none")
443 )]
444 pub default_value: Option<String>,
445
446 #[cfg_attr(
449 feature = "serde",
450 serde(default, skip_serializing_if = "core::ops::Not::not")
451 )]
452 pub secret: bool,
453
454 #[cfg_attr(
458 feature = "serde",
459 serde(default, skip_serializing_if = "core::ops::Not::not")
460 )]
461 pub optional: bool,
462}
463
464impl ConfigurationVariable {
465 pub fn is_required(&self) -> bool {
467 !self.optional && self.default_value.is_none()
468 }
469}
470
471#[cfg(feature = "serde")]
472fn empty_vec_if_null<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
473where
474 D: serde::Deserializer<'de>,
475 T: serde::Deserialize<'de>,
476{
477 use serde::Deserialize;
478 Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
479}
480
481#[cfg(feature = "serde")]
482mod ordered {
483 use super::*;
484 use alloc::fmt;
485 use serde::{
486 Deserializer,
487 de::{MapAccess, Visitor},
488 };
489
490 pub fn deserialize_ordered<'de, D>(deserializer: D) -> Result<Vec<(String, String)>, D::Error>
491 where
492 D: Deserializer<'de>,
493 {
494 struct OrderedVisitor;
495
496 impl<'de> Visitor<'de> for OrderedVisitor {
497 type Value = Vec<(String, String)>;
498
499 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
500 f.write_str("a map of string keys to string values (preserving order)")
501 }
502
503 fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
504 where
505 A: MapAccess<'de>,
506 {
507 let mut items = Vec::with_capacity(access.size_hint().unwrap_or(0));
508 while let Some((k, v)) = access.next_entry::<String, String>()? {
509 items.push((k, v));
510 }
511 Ok(items)
512 }
513 }
514
515 deserializer.deserialize_map(OrderedVisitor)
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 use alloc::vec;
523
524 #[test]
525 fn test_deser() {
526 let yaml = r#"
527name: example
528label: Example
529summary: Example Module
530links:
531 - https://github.com/asimov-platform/asimov.rs/tree/master/lib/asimov-module
532
533requires:
534 modules:
535 - other
536 models:
537 hf:first/model: first_url
538 hf:second/model:
539 small: small_url
540 medium: medium_url
541 large: large_url
542
543provides:
544 programs:
545 - asimov-example-module
546
547handles:
548 content_types:
549 - content_type
550 file_extensions:
551 - file_extension
552 url_patterns:
553 - pattern
554 url_prefixes:
555 - prefix
556 url_protocols:
557 - protocol
558
559config:
560 variables:
561 - name: api_key
562 description: "api key to authorize requests"
563 default_value: "foobar"
564 environment: API_KEY
565
566 - name: token
567 secret: true
568
569 - name: nickname
570 optional: true
571
572"#;
573
574 let dec: ModuleManifest = serde_yaml_ng::from_str(yaml).expect("deser should succeed");
575
576 assert_eq!(dec.name, "example");
577 assert_eq!(dec.label.as_deref(), Some("Example"));
578 assert_eq!(dec.summary.as_deref(), Some("Example Module"));
579
580 assert_eq!(
581 dec.links,
582 vec!["https://github.com/asimov-platform/asimov.rs/tree/master/lib/asimov-module"],
583 );
584
585 assert_eq!(dec.provides.programs.len(), 1);
586 assert_eq!(
587 dec.provides.programs.first().unwrap(),
588 "asimov-example-module",
589 );
590
591 assert_eq!(
592 dec.handles
593 .content_types
594 .first()
595 .expect("should have content_types"),
596 "content_type",
597 );
598
599 assert_eq!(
600 dec.handles
601 .file_extensions
602 .first()
603 .expect("should have file_extensions"),
604 "file_extension",
605 );
606
607 assert_eq!(
608 dec.handles
609 .url_patterns
610 .first()
611 .expect("should have url_patterns"),
612 "pattern",
613 );
614
615 assert_eq!(
616 dec.handles
617 .url_prefixes
618 .first()
619 .expect("should have url_prefixes"),
620 "prefix",
621 );
622
623 assert_eq!(
624 dec.handles
625 .url_protocols
626 .first()
627 .expect("should have url_protocols"),
628 "protocol",
629 );
630
631 let variables = dec.config.expect("should have config").variables;
632
633 assert_eq!(
634 variables.first(),
635 Some(&ConfigurationVariable {
636 name: "api_key".into(),
637 description: Some("api key to authorize requests".into()),
638 environment: Some("API_KEY".into()),
639 default_value: Some("foobar".into()),
640 secret: false,
641 optional: false,
642 }),
643 );
644 assert!(!variables[0].is_required());
645
646 assert!(variables[1].secret);
647 assert!(variables[1].is_required());
648
649 assert!(variables[2].optional);
650 assert!(!variables[2].is_required());
651
652 let requires = dec.requires;
653
654 assert_eq!(requires.modules.len(), 1);
655 assert_eq!(requires.modules.first().unwrap(), "other");
656
657 assert_eq!(requires.models.len(), 2);
658
659 assert_eq!(
660 requires.models["hf:first/model"],
661 RequiredModel::Url("first_url".into()),
662 );
663
664 assert_eq!(
665 requires.models["hf:second/model"],
666 RequiredModel::Choices(vec![
667 ("small".into(), "small_url".into()),
668 ("medium".into(), "medium_url".into()),
669 ("large".into(), "large_url".into())
670 ]),
671 );
672 }
673}