1use 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#[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 pub fn check_results(&self) {
41 self.results.iter().for_each(|result| {
43 if result.result == PassFail::Fail {
44 std::process::exit(2)
45 }
46 });
47 }
48}
49
50#[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#[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
107pub trait Validate {
109 fn validate(&self) -> Result<(), ValidationError>;
110}
111
112#[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 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 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 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 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#[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 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#[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#[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 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 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 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}