use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{Check, CheckId, Context, Registry, StatusCode, TestableType};
use std::collections::HashMap;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Override {
pub code: String,
pub status: StatusCode,
pub reason: String,
}
impl Override {
pub fn new(code: &str, status: StatusCode, reason: &str) -> Self {
Override {
code: code.to_string(),
status,
reason: reason.to_string(),
}
}
}
#[derive(Serialize, Deserialize, Default)]
pub struct Profile {
pub sections: IndexMap<String, Vec<CheckId>>,
#[serde(default)]
include_profiles: Vec<String>,
#[serde(default)]
exclude_checks: Vec<CheckId>,
#[serde(default)]
pub overrides: HashMap<CheckId, Vec<Override>>,
#[serde(default)]
configuration_defaults: HashMap<CheckId, HashMap<String, serde_json::Value>>,
#[cfg(feature = "python")]
#[serde(default)]
pub check_definitions: Vec<String>,
}
impl Profile {
pub fn from_toml(toml: &str) -> Result<Profile, toml::de::Error> {
toml::from_str(toml)
}
pub fn validate(&mut self, registry: &Registry) -> Result<(), String> {
for included_profile_str in self.include_profiles.iter() {
if let Some(profile) = registry.profiles.get(included_profile_str) {
for section in profile.sections.keys().rev() {
if !self.sections.contains_key(section) {
self.sections.insert_before(0, section.clone(), vec![]);
}
}
for (section, checks) in &profile.sections {
#[allow(clippy::unwrap_used)] let existing_checks = self.sections.get_mut(section).unwrap();
for check in checks {
if !existing_checks.contains(check) {
existing_checks.push(check.clone());
}
}
}
} else {
return Err(format!("Unknown profile: {included_profile_str}"));
}
}
let mut missing_checks = vec![];
for section in self.sections.values() {
for check_id in section {
if !registry.checks.contains_key(check_id) {
missing_checks.push(check_id.clone());
}
}
}
for check in registry.checks.values() {
if !registry.filetypes.contains_key(check.applies_to) {
return Err(format!(
"Check {} applies to unknown filetype {}",
check.id, check.applies_to
));
}
}
Ok(())
}
pub fn check_order<'t, 'r>(
&self,
include_checks: &[String],
exclude_checks: &[String],
registry: &'r Registry<'r>,
general_context: Context,
configuration: &HashMap<CheckId, serde_json::Value>,
testables: &'t [TestableType],
) -> Vec<(String, &'t TestableType<'t>, &'r Check<'r>, Context)> {
let testable_and_cache = testables
.iter()
.map(|t| (t, general_context.with_new_cache()));
let mut order = vec![];
let mut sections_and_checks = vec![];
for (section_name, check_ids) in self.sections.iter() {
for check_id in check_ids.iter() {
if !included_excluded(check_id, include_checks, exclude_checks) {
continue;
}
if self.exclude_checks.contains(check_id) {
continue;
}
if registry.checks.contains_key(check_id) {
sections_and_checks.push((section_name, check_id))
} else {
log::warn!("Unknown check: {check_id}");
}
}
}
for (testable, context) in testable_and_cache {
for (section_name, check_id) in sections_and_checks.iter() {
#[allow(clippy::unwrap_used)] let check = registry.checks.get(check_id.as_str()).unwrap();
if check.applies(testable, registry)
&& per_file_included_excluded(check_id, configuration, testable)
{
let specialized_context = context.specialize(check, configuration, self);
order.push((
section_name.to_string(),
testable,
check,
specialized_context,
));
}
}
}
order
}
pub fn defaults(&self, check_id: &str) -> HashMap<String, Value> {
self.configuration_defaults
.get(check_id)
.unwrap_or(&HashMap::new())
.clone()
}
}
fn included_excluded(
checkname: &str,
include_checks: &[String],
exclude_checks: &[String],
) -> bool {
if !include_checks.is_empty() && !include_checks.iter().any(|id| checkname.contains(id)) {
return false;
}
if exclude_checks.iter().any(|id| checkname.contains(id)) {
return false;
}
true
}
fn per_file_included_excluded(
check_id: &str,
configuration: &HashMap<CheckId, serde_json::Value>,
testable: &TestableType,
) -> bool {
if let Some(explicit_files) = configuration
.get(check_id)
.and_then(|x| x.get("explicit_files"))
.and_then(|x| x.as_array())
{
let explicit_file_names: Vec<&str> =
explicit_files.iter().flat_map(|x| x.as_str()).collect();
return match testable {
TestableType::Single(testable) => testable
.basename()
.is_some_and(|b| explicit_file_names.iter().any(|s| s == &b)),
TestableType::Collection(_) => true,
};
}
if let Some(skips) = configuration
.get(check_id)
.and_then(|x| x.get("exclude_files"))
.and_then(|x| x.as_array())
{
if skips.is_empty() {
return true;
}
let is_skipped = match testable {
TestableType::Single(testable) => testable
.basename()
.is_some_and(|b| skips.iter().any(|s| s.as_str() == Some(&b))),
TestableType::Collection(_) => false,
};
if is_skipped {
return false;
}
}
true
}
pub struct ProfileBuilder<'a> {
profile: Profile,
current_section: Option<String>,
checks_to_register: Vec<Check<'a>>,
}
impl<'a> ProfileBuilder<'a> {
pub fn new() -> Self {
ProfileBuilder {
checks_to_register: vec![],
profile: Profile::default(),
current_section: None,
}
}
pub fn add_section(mut self, name: &str) -> Self {
self.current_section = Some(name.to_string());
if !self.profile.sections.contains_key(name) {
self.profile.sections.insert(name.to_string(), vec![]);
} else {
log::warn!("Section {name} already exists");
}
self
}
pub fn add_and_register_check(mut self, check: Check<'static>) -> Self {
let check_id = check.id.to_string();
if let Some(section) = &self.current_section {
self.checks_to_register.push(check);
#[allow(clippy::unwrap_used)] self.profile
.sections
.get_mut(section)
.unwrap()
.push(check_id);
} else {
panic!("No section to add check to");
}
self
}
pub fn include_profile(mut self, profile: &str) -> Self {
self.profile.include_profiles.push(profile.to_string());
self
}
pub fn exclude_check(mut self, check: &str) -> Self {
self.profile.exclude_checks.push(check.to_string());
self
}
pub fn with_overrides(mut self, check_id: &str, overrides: Vec<Override>) -> Self {
self.profile
.overrides
.insert(check_id.to_string(), overrides);
self
}
pub fn with_configuration_defaults(
mut self,
check_id: &str,
configuration_defaults: HashMap<String, serde_json::Value>,
) -> Self {
self.profile
.configuration_defaults
.insert(check_id.to_string(), configuration_defaults);
self
}
pub fn build(self, name: &str, registry: &mut Registry<'a>) -> Result<(), String> {
for check in self.checks_to_register {
registry.register_check(check);
}
registry.register_profile(name, self.profile)
}
}
impl Default for ProfileBuilder<'_> {
fn default() -> Self {
Self::new()
}
}