mistletoe_api/v1alpha1/
mistresult.rs

1use anyhow::anyhow;
2use indexmap::IndexMap;
3use serde::{Serialize, Deserialize};
4
5/// This is the type that's returned from the package to the engine.
6/// 
7/// The error case can be any error (thanks to "anyhow"), and the success case is a
8/// [MistOutput]. It can be serialized with [serialize_result] and deserialized with
9/// [deserialize_result] (this is because we don't own the [Result] type).
10pub type MistResult = anyhow::Result<MistOutput>;
11
12/// Serialized the result to a YAML string.
13pub fn serialize_result(result: MistResult) -> Result<String, serde_yaml::Error> {
14    serde_yaml::to_string(&MistResultLayout::from(result))
15}
16
17/// Deserialized the result from a YAML string.
18pub fn deserialize_result(result_str: &str) -> MistResult {
19    serde_yaml::from_str::<MistResultLayout>(result_str)?.into()
20}
21
22/// This is the successful output of a package.
23#[derive(Clone, PartialEq, Debug)]
24pub struct MistOutput {
25    message: Option<String>,
26
27    /// This is the map of output files.
28    /// 
29    /// Each key is a relative path in the output directory that the content will be
30    /// rendered to, and the keys are the content.
31    files: IndexMap<String, String>,
32}
33
34impl MistOutput {
35    /// Creates a new output object.
36    pub fn new() -> Self {
37        Self {
38            message: None,
39            files: IndexMap::new(),
40        }
41    }
42
43    /// Sets the optional message in the output that the package can print out, in case
44    /// there's additional info the package wishes to provide to the end user.
45    pub fn set_message(&mut self, message: String) {
46        self.message = Some(message);
47    }
48
49
50    /// Sets the optional message in the output that the package can print out, in case
51    /// there's additional info the package wishes to provide to the end user.
52    /// 
53    /// This is the same as `set_message` but can be used in chaining.
54    pub fn with_message(mut self, message: String) -> Self {
55        self.set_message(message);
56        self
57    }
58
59    /// Adds a file to the output that will be rendered to the output directory.
60    pub fn set_file(&mut self, filename: String, content: String) {
61        self.files.insert(filename, content);
62    }
63
64    /// Adds a file to the output that will be rendered to the output directory.
65    /// 
66    /// This is the same as `add_file` but can be used in chaining.
67    pub fn with_file(mut self, filename: String, content: String) -> Self {
68        self.set_file(filename, content);
69        self
70    }
71
72    /// Retrieves the attached message on this object.
73    pub fn get_message(&self) -> &Option<String> {
74        &self.message
75    }
76
77    /// Retrieves the map of files stored by this object.
78    pub fn get_files(&self) -> &IndexMap<String, String> {
79        &self.files
80    }
81}
82
83#[derive(Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85struct MistResultLayout {
86    api_version: String,
87    kind: String,
88    data: MistResultLayoutData,
89}
90
91#[derive(Serialize, Deserialize)]
92struct MistResultLayoutData {
93    result: String,
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    message: Option<String>,
96    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
97    files: IndexMap<String, String>,
98}
99
100impl From<MistResult> for MistResultLayout {
101    fn from(result: MistResult) -> Self {
102        Self {
103            api_version: "mistletoe.dev/v1alpha1".to_string(),
104            kind: "MistResult".to_string(),
105            data: match result {
106                Ok(output) => MistResultLayoutData {
107                    result: "Ok".to_string(),
108                    message: output.message,
109                    files: output.files,
110                },
111                Err(e) => MistResultLayoutData {
112                    result: "Err".to_string(),
113                    message: Some(e.to_string()),
114                    files: IndexMap::new(),
115                },
116            }
117        }
118    }
119}
120
121impl Into<MistResult> for MistResultLayout {
122    fn into(self) -> MistResult {
123        match self.data.result.as_str() {
124            "Ok" => MistResult::Ok(MistOutput {
125                message: self.data.message,
126                files: self.data.files,
127            }),
128            "Err" => MistResult::Err(match self.data.message {
129                Some(message) => anyhow!(message),
130                None => anyhow!("package failed without a message"),
131            }),
132            s => MistResult::Err(anyhow!("package result format error: `data.result` must either be \"Ok\" or \"Err\", found {}", s)),
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use indoc::indoc;
141
142    #[test]
143    fn test_mistresult_ok() {
144        let expected_yaml = indoc!{"
145            apiVersion: mistletoe.dev/v1alpha1
146            kind: MistResult
147            data:
148              result: Ok
149              message: 'warning: nothing went wrong'
150              files:
151                namespace.yaml: |
152                  apiVersion: v1
153                  kind: Namespace
154                  metadata:
155                    name: my-namespace"};
156
157        let mistoutput = MistOutput::new()
158            .with_message("warning: nothing went wrong".to_string())
159            .with_file("namespace.yaml".to_string(), indoc!("
160                apiVersion: v1
161                kind: Namespace
162                metadata:
163                  name: my-namespace")
164                .to_string());
165
166        let yaml = serialize_result(Ok(mistoutput.clone())).unwrap();
167        assert_eq!(expected_yaml, yaml);
168
169        let mistresult_parsed = deserialize_result(&yaml).unwrap();
170        assert_eq!(mistoutput, mistresult_parsed);
171    }
172
173    #[test]
174    fn test_mistresult_err() {
175        let expected_yaml: &str = indoc!{"
176            apiVersion: mistletoe.dev/v1alpha1
177            kind: MistResult
178            data:
179              result: Err
180              message: 'error: something went wrong'"};
181
182        let err_string = "error: something went wrong";
183        let yaml = serialize_result(Err(anyhow!(err_string.to_string()))).unwrap();
184        assert_eq!(expected_yaml, yaml);
185
186        let mistresult_parsed = deserialize_result(&yaml);
187        assert_eq!(err_string, mistresult_parsed.err().unwrap().to_string());
188    }
189}