use std::path::{Path, PathBuf};
use apimock_routing::RuleSet;
use crate::view::{Diagnostic, NodeValidation, Severity, ValidationIssue, ValidationReport};
use super::Workspace;
use super::id_index::NodeAddress;
impl Workspace {
pub(super) fn collect_diagnostics(&self) -> Vec<Diagnostic> {
let mut out: Vec<Diagnostic> = Vec::new();
for (rs_idx, rule_set) in self.config.service.rule_sets.iter().enumerate() {
for (rule_idx, rule) in rule_set.rules.iter().enumerate() {
let nv = respond_node_validation(&rule.respond, rule_set, rule_idx, rs_idx);
if nv.ok {
continue;
}
let resp_id = self.ids.id_for(NodeAddress::Respond {
rule_set: rs_idx,
rule: rule_idx,
});
for issue in nv.issues {
out.push(Diagnostic {
node_id: resp_id,
file: Some(PathBuf::from(rule_set.file_path.as_str())),
severity: issue.severity,
message: issue.message,
});
}
}
}
if !Path::new(self.config.service.fallback_respond_dir.as_str()).exists() {
out.push(Diagnostic {
node_id: self.ids.id_for(NodeAddress::FallbackRespondDir),
file: Some(self.root_path.clone()),
severity: Severity::Error,
message: format!(
"fallback_respond_dir does not exist: {}",
self.config.service.fallback_respond_dir
),
});
}
out
}
pub fn validate(&self) -> ValidationReport {
let diagnostics = self.collect_diagnostics();
let is_valid = !diagnostics
.iter()
.any(|d| matches!(d.severity, Severity::Error));
ValidationReport {
diagnostics,
is_valid,
}
}
}
pub(super) fn respond_node_validation(
respond: &apimock_routing::Respond,
rule_set: &RuleSet,
rule_idx: usize,
rs_idx: usize,
) -> NodeValidation {
let mut issues: Vec<ValidationIssue> = Vec::new();
let any = respond.file_path.is_some()
|| respond.text.is_some()
|| respond.json.is_some()
|| respond.status.is_some();
if !any {
issues.push(ValidationIssue {
severity: Severity::Error,
message: "response requires at least one of file_path, text, json, or status"
.to_owned(),
});
}
let body_sources_set = [
respond.file_path.is_some(),
respond.text.is_some(),
respond.json.is_some(),
]
.into_iter()
.filter(|&set| set)
.count();
if body_sources_set > 1 {
issues.push(ValidationIssue {
severity: Severity::Error,
message: "file_path, text and json are mutually exclusive; only one may be set"
.to_owned(),
});
}
if respond.file_path.is_some() && respond.status.is_some() {
issues.push(ValidationIssue {
severity: Severity::Error,
message: "status cannot be combined with file_path (only with text or json)".to_owned(),
});
}
if let Some(json_str) = respond.json.as_ref()
&& let Err(e) = json5::from_str::<serde_json::Value>(json_str)
{
issues.push(ValidationIssue {
severity: Severity::Error,
message: format!(
"invalid json (rule #{} in rule set #{}): {}",
rule_idx + 1,
rs_idx + 1,
e
),
});
}
if let Some(file_path) = respond.file_path.as_ref() {
let dir_prefix = rule_set.dir_prefix();
let p = Path::new(dir_prefix.as_str()).join(file_path);
if !p.exists() {
issues.push(ValidationIssue {
severity: Severity::Error,
message: format!(
"file not found: {} (rule #{} in rule set #{})",
display_path(&p),
rule_idx + 1,
rs_idx + 1,
),
});
} else {
let is_json_like = p
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase())
.is_some_and(|e| e == "json" || e == "json5");
if is_json_like {
match std::fs::read_to_string(&p) {
Ok(content) => {
if let Err(e) = json5::from_str::<serde_json::Value>(content.as_str()) {
issues.push(ValidationIssue {
severity: Severity::Error,
message: format!(
"`{}` is not valid JSON (rule #{} in rule set #{}): {}",
display_path(&p),
rule_idx + 1,
rs_idx + 1,
e
),
});
}
}
Err(e) => {
issues.push(ValidationIssue {
severity: Severity::Error,
message: format!(
"failed to read `{}` (rule #{} in rule set #{}): {}",
display_path(&p),
rule_idx + 1,
rs_idx + 1,
e
),
});
}
}
}
}
}
NodeValidation {
ok: issues.is_empty(),
issues,
}
}
fn display_path(p: &Path) -> String {
use std::path::Component;
let cleaned: PathBuf = p
.components()
.filter(|c| !matches!(c, Component::CurDir))
.collect();
if cleaned.as_os_str().is_empty() {
".".to_owned()
} else {
cleaned.to_string_lossy().into_owned()
}
}