use crate::logs;
use crate::modules::execution::model_injection::{inject_task_metadata, inject_template_values};
use crate::modules::execution::output;
use crate::utils::{color_print, ColorEnum};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CiInfo {
pub provider: String,
pub repo_owner: String,
pub repo_name: String,
pub token_env_var: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct JobResults {
pub job_name: String,
pub execution_time: String,
pub results: Vec<TaskResult>,
}
impl JobResults {
pub fn log_results(&self) {
let log_path = logs::write_logs(self);
println!("> Log file written to: {}", log_path);
}
pub fn display_results(&self) {
output::display_execution_results(self);
}
pub fn check_results(&self) {
self.results.iter().for_each(|result| {
if result.result == PassFail::Fail {
std::process::exit(2)
}
});
}
}
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
pub enum PassFail {
Pass,
Fail,
}
impl std::fmt::Display for PassFail {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub enum DocsKind {
Markdown,
Text,
URL,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Docs {
pub name: String,
pub description: Option<String>,
pub kind: DocsKind,
pub path: String,
}
#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
pub struct TaskResult {
pub name: String,
pub command: String,
pub stage: i8,
pub result: PassFail,
pub elapsed_time: i64,
pub file_path: String,
}
#[derive(Debug, Clone)]
pub struct ValidationError {
pub message: String,
}
impl Default for ValidationError {
fn default() -> Self {
ValidationError {
message: String::from("Error: Roxfile syntax is invalid!"),
}
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for ValidationError {}
pub trait Validate {
fn validate(&self) -> Result<(), ValidationError>;
}
#[derive(Deserialize, Debug, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct Task {
pub name: String,
pub command: Option<String>,
pub description: Option<String>,
pub file_path: Option<String>,
pub uses: Option<String>,
pub values: Option<Vec<String>>,
pub hide: Option<bool>,
pub workdir: Option<String>,
}
impl Validate for Task {
fn validate(&self) -> Result<(), ValidationError> {
let task_fail_message = format!("> Task '{}' failed validation!", self.name);
if self.command.is_none() & self.uses.is_none() {
color_print(vec![task_fail_message], ColorEnum::Red);
return Err(ValidationError {
message: "A Task must implement either 'command' or 'uses'!".to_owned(),
});
}
if self.uses.is_some() & self.command.is_some() {
color_print(vec![task_fail_message], ColorEnum::Red);
return Err(ValidationError {
message: "A Task cannot implement both 'command' & 'uses'!".to_owned(),
});
}
if self.uses.is_some() & self.values.is_none() {
color_print(vec![task_fail_message], ColorEnum::Red);
return Err(ValidationError {
message: "A Task that implements 'uses' must also implement 'values'!".to_owned(),
});
}
if self.uses.is_none() & self.values.is_some() {
color_print(vec![task_fail_message], ColorEnum::Red);
return Err(ValidationError {
message: "A Task that implements 'values' must also implement 'uses'!".to_owned(),
});
}
Ok(())
}
}
#[derive(Deserialize, Debug, Default, Clone)]
#[serde(deny_unknown_fields)]
pub struct Template {
pub name: String,
pub command: String,
pub symbols: Vec<String>,
}
impl Validate for Template {
fn validate(&self) -> Result<(), ValidationError> {
let failure_message = format!("> Template '{}' failed validation!", self.name);
for symbol in &self.symbols {
let exists = self.command.contains(symbol);
if !exists {
color_print(vec![failure_message], ColorEnum::Red);
return Err(ValidationError {
message: "A Template's 'symbols' must all exist within its 'command'!"
.to_owned(),
});
}
}
Ok(())
}
}
#[derive(Deserialize, Debug, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct Pipeline {
pub name: String,
pub description: Option<String>,
pub stages: Vec<Vec<String>>,
}
#[derive(Deserialize, Debug, Default, Clone)]
#[serde(deny_unknown_fields)]
pub struct RoxFile {
pub ci: Option<CiInfo>,
pub docs: Option<Vec<Docs>>,
pub tasks: Vec<Task>,
pub pipelines: Option<Vec<Pipeline>>,
pub templates: Option<Vec<Template>>,
}
impl RoxFile {
pub fn build(file_path: &str) -> Result<Self> {
let file_string = std::fs::read_to_string(file_path)?;
let mut roxfile: RoxFile = serde_yaml::from_str(&file_string)?;
let _ = roxfile
.templates
.iter()
.flatten()
.try_for_each(|template| template.validate());
let template_map: HashMap<String, &Template> = std::collections::HashMap::from_iter(
roxfile
.templates
.as_deref()
.into_iter()
.flatten()
.map(|template| (template.name.to_owned(), template)),
);
let _ = roxfile.tasks.iter().try_for_each(|task| task.validate());
roxfile.tasks = inject_task_metadata(roxfile.tasks, file_path);
roxfile.tasks = roxfile
.tasks
.into_iter()
.map(|task| match task.uses.to_owned() {
Some(task_use) => {
inject_template_values(task, template_map.get(&task_use).unwrap())
}
None => task,
})
.collect();
Ok(roxfile)
}
}