rox/
models.rs

1//! Contains the Structs for the Schema of the Roxfile
2//! as well as the validation logic.
3use crate::logs;
4use crate::modules::execution::model_injection::{inject_task_metadata, inject_template_values};
5use crate::modules::execution::output;
6use crate::utils::{color_print, ColorEnum};
7use anyhow::Result;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::error::Error;
11use std::fmt;
12
13#[derive(Serialize, Deserialize, Debug, Clone)]
14pub struct CiInfo {
15    pub provider: String,
16    pub repo_owner: String,
17    pub repo_name: String,
18    pub token_env_var: String,
19}
20
21/// Format for completed executions
22#[derive(Serialize, Deserialize, Debug)]
23pub struct JobResults {
24    pub job_name: String,
25    pub execution_time: String,
26    pub results: Vec<TaskResult>,
27}
28
29impl JobResults {
30    pub fn log_results(&self) {
31        let log_path = logs::write_logs(self);
32        println!("> Log file written to: {}", log_path);
33    }
34
35    pub fn display_results(&self) {
36        output::display_execution_results(self);
37    }
38
39    /// Triggers a non-zero exit if any job failed, otherwise passes.
40    pub fn check_results(&self) {
41        // TODO: Figure out a way to get this info without looping again
42        self.results.iter().for_each(|result| {
43            if result.result == PassFail::Fail {
44                std::process::exit(2)
45            }
46        });
47    }
48}
49
50/// Enum for task command status
51#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
52pub enum PassFail {
53    Pass,
54    Fail,
55}
56impl std::fmt::Display for PassFail {
57    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
58        write!(f, "{:?}", self)
59    }
60}
61
62#[derive(Deserialize, Debug, Clone)]
63#[serde(rename_all = "lowercase")]
64pub enum DocsKind {
65    Markdown,
66    Text,
67    URL,
68}
69
70#[derive(Deserialize, Debug, Clone)]
71pub struct Docs {
72    pub name: String,
73    pub description: Option<String>,
74    pub kind: DocsKind,
75    pub path: String,
76}
77
78#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
79pub struct TaskResult {
80    pub name: String,
81    pub command: String,
82    pub stage: i8,
83    pub result: PassFail,
84    pub elapsed_time: i64,
85    pub file_path: String,
86}
87
88// Create a custom Error type for Validation
89#[derive(Debug, Clone)]
90pub struct ValidationError {
91    pub message: String,
92}
93impl Default for ValidationError {
94    fn default() -> Self {
95        ValidationError {
96            message: String::from("Error: Roxfile syntax is invalid!"),
97        }
98    }
99}
100impl fmt::Display for ValidationError {
101    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
102        write!(f, "{}", self.message)
103    }
104}
105impl Error for ValidationError {}
106
107// Trait for granular schema validation
108pub trait Validate {
109    fn validate(&self) -> Result<(), ValidationError>;
110}
111
112/// Schema for Tasks in the Roxfile
113///
114/// Tasks are discrete units of execution
115/// that send commands to the shell.
116#[derive(Deserialize, Debug, Clone, Default)]
117#[serde(deny_unknown_fields)]
118pub struct Task {
119    pub name: String,
120    pub command: Option<String>,
121    pub description: Option<String>,
122    pub file_path: Option<String>,
123    pub uses: Option<String>,
124    pub values: Option<Vec<String>>,
125    pub hide: Option<bool>,
126    pub workdir: Option<String>,
127}
128
129impl Validate for Task {
130    fn validate(&self) -> Result<(), ValidationError> {
131        let task_fail_message = format!("> Task '{}' failed validation!", self.name);
132
133        // Command and Uses cannot both be none
134        if self.command.is_none() & self.uses.is_none() {
135            color_print(vec![task_fail_message], ColorEnum::Red);
136            return Err(ValidationError {
137                message: "A Task must implement either 'command' or 'uses'!".to_owned(),
138            });
139        }
140
141        // Command and Uses cannot both be Some
142        if self.uses.is_some() & self.command.is_some() {
143            color_print(vec![task_fail_message], ColorEnum::Red);
144            return Err(ValidationError {
145                message: "A Task cannot implement both 'command' & 'uses'!".to_owned(),
146            });
147        }
148
149        // If Uses is Some, Values must also be Some
150        if self.uses.is_some() & self.values.is_none() {
151            color_print(vec![task_fail_message], ColorEnum::Red);
152            return Err(ValidationError {
153                message: "A Task that implements 'uses' must also implement 'values'!".to_owned(),
154            });
155        }
156
157        // If Uses is None, Values must also be None
158        if self.uses.is_none() & self.values.is_some() {
159            color_print(vec![task_fail_message], ColorEnum::Red);
160            return Err(ValidationError {
161                message: "A Task that implements 'values' must also implement 'uses'!".to_owned(),
162            });
163        }
164
165        Ok(())
166    }
167}
168
169/// Schema for Templates
170///
171/// Templates are injectable commands that
172/// can be used by tasks.
173#[derive(Deserialize, Debug, Default, Clone)]
174#[serde(deny_unknown_fields)]
175pub struct Template {
176    pub name: String,
177    pub command: String,
178    pub symbols: Vec<String>,
179}
180impl Validate for Template {
181    fn validate(&self) -> Result<(), ValidationError> {
182        let failure_message = format!("> Template '{}' failed validation!", self.name);
183
184        // All of the 'Symbol' items must exist within the 'Command'
185        for symbol in &self.symbols {
186            let exists = self.command.contains(symbol);
187            if !exists {
188                color_print(vec![failure_message], ColorEnum::Red);
189                return Err(ValidationError {
190                    message: "A Template's 'symbols' must all exist within its 'command'!"
191                        .to_owned(),
192                });
193            }
194        }
195
196        Ok(())
197    }
198}
199
200/// Schema for Pipelines
201///
202/// Pipelines are collections of tasks.
203#[derive(Deserialize, Debug, Clone, Default)]
204#[serde(deny_unknown_fields)]
205pub struct Pipeline {
206    pub name: String,
207    pub description: Option<String>,
208    pub stages: Vec<Vec<String>>,
209}
210
211/// The top-level structure of the Roxfile
212#[derive(Deserialize, Debug, Default, Clone)]
213#[serde(deny_unknown_fields)]
214pub struct RoxFile {
215    pub ci: Option<CiInfo>,
216    pub docs: Option<Vec<Docs>>,
217    pub tasks: Vec<Task>,
218    pub pipelines: Option<Vec<Pipeline>>,
219    pub templates: Option<Vec<Template>>,
220}
221
222impl RoxFile {
223    /// Create a new instance of RoxFile from a file path and
224    /// run all additional validation and metadata injection.
225    pub fn build(file_path: &str) -> Result<Self> {
226        let file_string = std::fs::read_to_string(file_path)?;
227        let mut roxfile: RoxFile = serde_yaml::from_str(&file_string)?;
228
229        // Templates
230        let _ = roxfile
231            .templates
232            .iter()
233            .flatten()
234            .try_for_each(|template| template.validate());
235        let template_map: HashMap<String, &Template> = std::collections::HashMap::from_iter(
236            roxfile
237                .templates
238                .as_deref()
239                .into_iter()
240                .flatten()
241                .map(|template| (template.name.to_owned(), template)),
242        );
243
244        // Tasks
245        let _ = roxfile.tasks.iter().try_for_each(|task| task.validate());
246        roxfile.tasks = inject_task_metadata(roxfile.tasks, file_path);
247        roxfile.tasks = roxfile
248            .tasks
249            .into_iter()
250            .map(|task| match task.uses.to_owned() {
251                Some(task_use) => {
252                    inject_template_values(task, template_map.get(&task_use).unwrap())
253                }
254                None => task,
255            })
256            .collect();
257
258        Ok(roxfile)
259    }
260}