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<std::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
447#[cfg(feature = "serde")]
448fn empty_vec_if_null<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
449where
450 D: serde::Deserializer<'de>,
451 T: serde::Deserialize<'de>,
452{
453 use serde::Deserialize;
454 Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
455}
456
457#[cfg(feature = "serde")]
458mod ordered {
459 use super::*;
460 use serde::{
461 Deserializer,
462 de::{MapAccess, Visitor},
463 };
464 use std::fmt;
465
466 pub fn deserialize_ordered<'de, D>(deserializer: D) -> Result<Vec<(String, String)>, D::Error>
467 where
468 D: Deserializer<'de>,
469 {
470 struct OrderedVisitor;
471
472 impl<'de> Visitor<'de> for OrderedVisitor {
473 type Value = Vec<(String, String)>;
474
475 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
476 f.write_str("a map of string keys to string values (preserving order)")
477 }
478
479 fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
480 where
481 A: MapAccess<'de>,
482 {
483 let mut items = Vec::with_capacity(access.size_hint().unwrap_or(0));
484 while let Some((k, v)) = access.next_entry::<String, String>()? {
485 items.push((k, v));
486 }
487 Ok(items)
488 }
489 }
490
491 deserializer.deserialize_map(OrderedVisitor)
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use std::vec;
499
500 #[test]
501 fn test_deser() {
502 let yaml = r#"
503name: example
504label: Example
505summary: Example Module
506links:
507 - https://github.com/asimov-platform/asimov.rs/tree/master/lib/asimov-module
508
509requires:
510 modules:
511 - other
512 models:
513 hf:first/model: first_url
514 hf:second/model:
515 small: small_url
516 medium: medium_url
517 large: large_url
518
519provides:
520 programs:
521 - asimov-example-module
522
523handles:
524 content_types:
525 - content_type
526 file_extensions:
527 - file_extension
528 url_patterns:
529 - pattern
530 url_prefixes:
531 - prefix
532 url_protocols:
533 - protocol
534
535config:
536 variables:
537 - name: api_key
538 description: "api key to authorize requests"
539 default_value: "foobar"
540 environment: API_KEY
541
542"#;
543
544 let dec: ModuleManifest = serde_yaml_ng::from_str(yaml).expect("deser should succeed");
545
546 assert_eq!(dec.name, "example");
547 assert_eq!(dec.label.as_deref(), Some("Example"));
548 assert_eq!(dec.summary.as_deref(), Some("Example Module"));
549
550 assert_eq!(
551 dec.links,
552 vec!["https://github.com/asimov-platform/asimov.rs/tree/master/lib/asimov-module"],
553 );
554
555 assert_eq!(dec.provides.programs.len(), 1);
556 assert_eq!(
557 dec.provides.programs.first().unwrap(),
558 "asimov-example-module",
559 );
560
561 assert_eq!(
562 dec.handles
563 .content_types
564 .first()
565 .expect("should have content_types"),
566 "content_type",
567 );
568
569 assert_eq!(
570 dec.handles
571 .file_extensions
572 .first()
573 .expect("should have file_extensions"),
574 "file_extension",
575 );
576
577 assert_eq!(
578 dec.handles
579 .url_patterns
580 .first()
581 .expect("should have url_patterns"),
582 "pattern",
583 );
584
585 assert_eq!(
586 dec.handles
587 .url_prefixes
588 .first()
589 .expect("should have url_prefixes"),
590 "prefix",
591 );
592
593 assert_eq!(
594 dec.handles
595 .url_protocols
596 .first()
597 .expect("should have url_protocols"),
598 "protocol",
599 );
600
601 assert_eq!(
602 dec.config.expect("should have config").variables.first(),
603 Some(&ConfigurationVariable {
604 name: "api_key".into(),
605 description: Some("api key to authorize requests".into()),
606 environment: Some("API_KEY".into()),
607 default_value: Some("foobar".into())
608 }),
609 );
610
611 let requires = dec.requires;
612
613 assert_eq!(requires.modules.len(), 1);
614 assert_eq!(requires.modules.first().unwrap(), "other");
615
616 assert_eq!(requires.models.len(), 2);
617
618 assert_eq!(
619 requires.models["hf:first/model"],
620 RequiredModel::Url("first_url".into()),
621 );
622
623 assert_eq!(
624 requires.models["hf:second/model"],
625 RequiredModel::Choices(vec![
626 ("small".into(), "small_url".into()),
627 ("medium".into(), "medium_url".into()),
628 ("large".into(), "large_url".into())
629 ]),
630 );
631 }
632}