use semver::{Version, VersionReq};
use serde::Deserialize;
use std::error::Error;
use std::fmt;
use std::str::FromStr;
use crate::utils::{color_print, ColorEnum};
#[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 VersionRequirement {
pub command: String,
pub minimum_version: Option<String>,
pub maximum_version: Option<String>,
pub split: Option<bool>, }
impl Validate for VersionRequirement {
fn validate(&self) -> Result<(), ValidationError> {
let failure_message = format!(
"> Version Requirement '{}' failed validation!",
self.command
);
let versions: Vec<&String> = vec![&self.minimum_version, &self.maximum_version]
.into_iter()
.flatten()
.collect();
for version in versions.iter() {
if Version::from_str(version).is_err() {
color_print(vec![failure_message], ColorEnum::Red);
return Err(ValidationError {
message: "Mininum and Maximum versions must be valid semantic version!"
.to_owned(),
});
}
}
if self.maximum_version.is_some() && self.maximum_version.is_some() {
let valid_version_constraints =
VersionReq::from_str(&format!("> {}", self.minimum_version.as_ref().unwrap()))
.unwrap()
.matches(&Version::from_str(self.maximum_version.as_ref().unwrap()).unwrap());
if !valid_version_constraints {
color_print(vec![failure_message], ColorEnum::Red);
return Err(ValidationError {
message: "The Minimum version cannot be larger than the Maximum version!"
.to_owned(),
});
}
}
if self.split.is_some() && versions.is_empty() {
color_print(vec![failure_message], ColorEnum::Red);
return Err(ValidationError {
message: "If 'split' is defined, either a 'minimum_version' or a 'maximum_version' is also required!"
.to_owned(),
});
}
Ok(())
}
}
#[derive(Deserialize, Debug, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct FileRequirement {
pub path: String,
pub create_if_not_exists: Option<bool>,
}
#[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 version_requirements: Option<Vec<VersionRequirement>>,
pub file_requirements: Option<Vec<FileRequirement>>,
pub tasks: Vec<Task>,
pub pipelines: Option<Vec<Pipeline>>,
pub templates: Option<Vec<Template>>,
pub additional_files: Option<Vec<String>>,
}
impl Validate for RoxFile {
fn validate(&self) -> Result<(), ValidationError> {
for task in &self.tasks {
task.validate()?
}
if let Some(templates) = &self.templates {
for template in templates {
template.validate()?
}
}
if let Some(version_requirements) = &self.version_requirements {
for requirement in version_requirements {
requirement.validate()?
}
}
Ok(())
}
}