wascc_host/
manifest.rs

1use std::collections::HashMap;
2
3#[derive(Debug, Clone)]
4#[cfg_attr(feature = "manifest", derive(serde::Serialize, serde::Deserialize))]
5pub struct HostManifest {
6    #[serde(default)]
7    #[serde(skip_serializing_if = "HashMap::is_empty")]
8    pub labels: HashMap<String, String>,
9    pub actors: Vec<String>,
10    pub capabilities: Vec<Capability>,
11    pub bindings: Vec<BindingEntry>,
12}
13
14#[derive(Debug, Clone)]
15#[cfg_attr(feature = "manifest", derive(serde::Serialize, serde::Deserialize))]
16pub struct Capability {
17    pub path: String,
18    pub binding_name: Option<String>,
19}
20
21#[derive(Debug, Clone)]
22#[cfg_attr(feature = "manifest", derive(serde::Serialize, serde::Deserialize))]
23pub struct BindingEntry {
24    pub actor: String,
25    pub capability: String,
26    pub binding: Option<String>,
27    pub values: Option<HashMap<String, String>>,
28}
29
30#[cfg(feature = "manifest")]
31use std::{fs::File, io::Read, path::Path};
32#[cfg(feature = "manifest")]
33impl HostManifest {
34    /// Creates an instance of a host manifest from a file path. The de-serialization
35    /// type will be chosen based on the file path extension, selecting YAML for .yaml
36    /// or .yml files, and JSON for all other file extensions. If the path has no extension, the
37    /// de-serialization type chosen will be YAML.
38    pub fn from_path(
39        path: impl AsRef<Path>,
40        expand_env: bool,
41    ) -> std::result::Result<HostManifest, Box<dyn std::error::Error + Send + Sync>> {
42        let mut contents = String::new();
43        let mut file = File::open(path.as_ref())?;
44        file.read_to_string(&mut contents)?;
45        if expand_env {
46            contents = Self::expand_env(&contents);
47        }
48        match path.as_ref().extension() {
49            Some(e) => {
50                let e = e.to_str().unwrap().to_lowercase(); // convert away from the FFI str
51                if e == "yaml" || e == "yml" {
52                    serde_yaml::from_str::<HostManifest>(&contents).map_err(|e| e.into())
53                } else {
54                    serde_json::from_str::<HostManifest>(&contents).map_err(|e| e.into())
55                }
56            }
57            None => serde_yaml::from_str::<HostManifest>(&contents).map_err(|e| e.into()),
58        }
59    }
60
61    fn expand_env(contents: &str) -> String {
62        let mut options = envmnt::ExpandOptions::new();
63        options.default_to_empty = false; // If environment variable not found, leave unexpanded.
64        options.expansion_type = Some(envmnt::ExpansionType::UnixBracketsWithDefaults); // ${VAR:DEFAULT}
65
66        envmnt::expand(contents, Some(options))
67    }
68}
69
70#[cfg(feature = "manifest")]
71#[cfg(test)]
72mod test {
73    use super::{BindingEntry, Capability};
74    use std::collections::HashMap;
75
76    #[test]
77    fn round_trip() {
78        let manifest = super::HostManifest {
79            labels: HashMap::new(),
80            actors: vec!["a".to_string(), "b".to_string(), "c".to_string()],
81            capabilities: vec![
82                Capability {
83                    path: "one".to_string(),
84                    binding_name: Some("default".to_string()),
85                },
86                Capability {
87                    path: "two".to_string(),
88                    binding_name: Some("default".to_string()),
89                },
90            ],
91            bindings: vec![BindingEntry {
92                actor: "a".to_string(),
93                binding: Some("default".to_string()),
94                capability: "wascc:one".to_string(),
95                values: Some(gen_values()),
96            }],
97        };
98        let yaml = serde_yaml::to_string(&manifest).unwrap();
99        assert_eq!(yaml, "---\nactors:\n  - a\n  - b\n  - c\ncapabilities:\n  - path: one\n    binding_name: default\n  - path: two\n    binding_name: default\nbindings:\n  - actor: a\n    capability: \"wascc:one\"\n    binding: default\n    values:\n      ROOT: /tmp");
100    }
101
102    #[test]
103    fn round_trip_with_labels() {
104        let manifest = super::HostManifest {
105            labels: {
106                let mut hm = HashMap::new();
107                hm.insert("test".to_string(), "value".to_string());
108                hm
109            },
110            actors: vec!["a".to_string(), "b".to_string(), "c".to_string()],
111            capabilities: vec![
112                Capability {
113                    path: "one".to_string(),
114                    binding_name: Some("default".to_string()),
115                },
116                Capability {
117                    path: "two".to_string(),
118                    binding_name: Some("default".to_string()),
119                },
120            ],
121            bindings: vec![BindingEntry {
122                actor: "a".to_string(),
123                binding: Some("default".to_string()),
124                capability: "wascc:one".to_string(),
125                values: Some(gen_values()),
126            }],
127        };
128        let yaml = serde_yaml::to_string(&manifest).unwrap();
129        assert_eq!(yaml, "---\nlabels:\n  test: value\nactors:\n  - a\n  - b\n  - c\ncapabilities:\n  - path: one\n    binding_name: default\n  - path: two\n    binding_name: default\nbindings:\n  - actor: a\n    capability: \"wascc:one\"\n    binding: default\n    values:\n      ROOT: /tmp");
130    }
131
132    #[test]
133    fn env_expansion() {
134        let values = vec![
135            "echo Test",
136            "echo $TEST_EXPAND_ENV_TEMP",
137            "echo ${TEST_EXPAND_ENV_TEMP}",
138            "echo ${TEST_EXPAND_ENV_TMP}",
139            "echo ${TEST_EXPAND_ENV_TEMP:/etc}",
140            "echo ${TEST_EXPAND_ENV_TMP:/etc}",
141        ];
142        let expected = vec![
143            "echo Test",
144            "echo $TEST_EXPAND_ENV_TEMP",
145            "echo /tmp",
146            "echo ${TEST_EXPAND_ENV_TMP}",
147            "echo /tmp",
148            "echo /etc",
149        ];
150
151        envmnt::set("TEST_EXPAND_ENV_TEMP", "/tmp");
152        for (got, expected) in values
153            .iter()
154            .map(|v| super::HostManifest::expand_env(v))
155            .zip(expected.iter())
156        {
157            assert_eq!(*expected, got);
158        }
159        envmnt::remove("TEST_EXPAND_ENV_TEMP");
160    }
161
162    fn gen_values() -> HashMap<String, String> {
163        let mut hm = HashMap::new();
164        hm.insert("ROOT".to_string(), "/tmp".to_string());
165
166        hm
167    }
168}