use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::audit::normalized::{NormalizedReport, RiskLevel};
use crate::audit::AuditReport;
use crate::output::builder::build_view_model;
use crate::output::report_model::ReportConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StudioAuditResponse {
pub url: String,
pub timestamp: DateTime<Utc>,
pub accessibility_score: u32,
pub overall_score: u32,
pub grade: String,
pub certificate: String,
pub risk_level: String,
pub risk_summary: String,
pub legal_flags: usize,
pub blocking_issues: usize,
pub critical_issues: usize,
pub high_issues: usize,
pub medium_issues: usize,
pub low_issues: usize,
pub total_issues: usize,
pub module_scores: Vec<StudioModuleScore>,
pub findings: Vec<StudioFindingPreview>,
pub nodes_analyzed: usize,
pub execution_time_ms: u64,
pub executive_summary: StudioExecutiveSummary,
pub artifacts: StudioReportArtifacts,
pub json_report: String,
pub cli_version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StudioExecutiveSummary {
pub executive_lead: String,
pub verdict: String,
pub benchmark_context: String,
pub key_points: Vec<String>,
pub top_actions: Vec<String>,
pub overall_impact: Vec<(String, String)>,
pub positive_aspects: Vec<String>,
pub next_step: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StudioReportArtifacts {
pub audit_json_path: Option<String>,
pub studio_snapshot_path: Option<String>,
pub pdf_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StudioModuleScore {
pub name: String,
pub score: u32,
pub grade: String,
pub weight_pct: u32,
pub contributes_to_overall: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StudioFindingPreview {
pub rule_id: String,
pub title: String,
pub severity: String,
pub occurrences: usize,
pub user_impact: String,
pub wcag_criterion: String,
}
impl StudioAuditResponse {
pub fn from_normalized(
normalized: &NormalizedReport,
_report: &AuditReport,
json_report: String,
) -> Self {
let vm = build_view_model(normalized, &ReportConfig::default());
let module_scores: Vec<StudioModuleScore> = normalized
.module_scores
.iter()
.map(|m| StudioModuleScore {
name: m.name.clone(),
score: m.score,
grade: m.grade.clone(),
weight_pct: m.weight_pct,
contributes_to_overall: m.contributes_to_overall,
})
.collect();
let findings: Vec<StudioFindingPreview> = normalized
.findings
.iter()
.map(|f| StudioFindingPreview {
rule_id: f.rule_id.clone(),
title: f.title.clone(),
severity: format!("{:?}", f.severity).to_lowercase(),
occurrences: f.occurrence_count,
user_impact: f.user_impact.clone(),
wcag_criterion: f.wcag_criterion.clone(),
})
.collect();
Self {
url: normalized.url.clone(),
timestamp: normalized.timestamp,
accessibility_score: normalized.score,
overall_score: normalized.overall_score,
grade: normalized.grade.clone(),
certificate: normalized.certificate.clone(),
risk_level: risk_level_string(normalized.risk.level),
risk_summary: normalized.risk.summary.clone(),
legal_flags: normalized.risk.legal_flags,
blocking_issues: normalized.risk.blocking_issues,
critical_issues: normalized.severity_counts.critical,
high_issues: normalized.severity_counts.high,
medium_issues: normalized.severity_counts.medium,
low_issues: normalized.severity_counts.low,
total_issues: normalized.severity_counts.total,
module_scores,
findings,
nodes_analyzed: normalized.nodes_analyzed,
execution_time_ms: normalized.duration_ms,
executive_summary: StudioExecutiveSummary {
executive_lead: vm.summary.executive_lead,
verdict: vm.summary.verdict,
benchmark_context: vm.summary.benchmark_context,
key_points: vm.executive.key_points,
top_actions: vm.summary.top_actions,
overall_impact: vm.summary.overall_impact,
positive_aspects: vm.summary.positive_aspects,
next_step: vm.executive.next_steps_callout_body,
},
artifacts: StudioReportArtifacts::default(),
json_report,
cli_version: env!("CARGO_PKG_VERSION").to_string(),
}
}
pub fn with_artifacts(mut self, artifacts: StudioReportArtifacts) -> Self {
self.artifacts = artifacts;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StudioHistoryEntry {
pub id: String,
pub url: String,
pub timestamp: DateTime<Utc>,
#[serde(default)]
pub accessibility_score: u32,
pub overall_score: u32,
#[serde(default)]
pub grade: String,
#[serde(default)]
pub certificate: String,
#[serde(default)]
pub risk_level: String,
#[serde(default)]
pub total_issues: usize,
#[serde(default)]
pub critical_issues: usize,
#[serde(default)]
pub high_issues: usize,
#[serde(default)]
pub execution_time_ms: u64,
#[serde(default)]
pub module_scores: Vec<StudioModuleScore>,
#[serde(default)]
pub artifacts: StudioReportArtifacts,
}
impl StudioHistoryEntry {
pub fn from_response(response: &StudioAuditResponse) -> Self {
Self {
id: format!("{}", response.timestamp.timestamp_millis()),
url: response.url.clone(),
timestamp: response.timestamp,
accessibility_score: response.accessibility_score,
overall_score: response.overall_score,
grade: response.grade.clone(),
certificate: response.certificate.clone(),
risk_level: response.risk_level.clone(),
total_issues: response.total_issues,
critical_issues: response.critical_issues,
high_issues: response.high_issues,
execution_time_ms: response.execution_time_ms,
module_scores: response.module_scores.clone(),
artifacts: response.artifacts.clone(),
}
}
pub fn into_response(self, json_report: String) -> StudioAuditResponse {
StudioAuditResponse {
url: self.url,
timestamp: self.timestamp,
accessibility_score: self.accessibility_score,
overall_score: self.overall_score,
grade: self.grade,
certificate: self.certificate,
risk_level: self.risk_level,
risk_summary: String::new(),
legal_flags: 0,
blocking_issues: 0,
critical_issues: self.critical_issues,
high_issues: self.high_issues,
medium_issues: 0,
low_issues: 0,
total_issues: self.total_issues,
module_scores: self.module_scores,
findings: vec![],
nodes_analyzed: 0,
execution_time_ms: self.execution_time_ms,
executive_summary: StudioExecutiveSummary::default(),
artifacts: self.artifacts,
json_report,
cli_version: env!("CARGO_PKG_VERSION").to_string(),
}
}
}
fn risk_level_string(level: RiskLevel) -> String {
match level {
RiskLevel::Low => "low".to_string(),
RiskLevel::Medium => "medium".to_string(),
RiskLevel::High => "high".to_string(),
RiskLevel::Critical => "critical".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::audit::normalize;
use crate::cli::WcagLevel;
use crate::wcag::WcagResults;
#[test]
fn test_studio_response_from_empty_report() {
let report = AuditReport::new(
"https://example.com".to_string(),
WcagLevel::AA,
WcagResults::new(),
500,
);
let normalized = normalize(&report);
let response = StudioAuditResponse::from_normalized(&normalized, &report, "{}".to_string());
assert_eq!(response.url, "https://example.com");
assert_eq!(response.accessibility_score, 100);
assert_eq!(response.risk_level, "low");
assert_eq!(response.total_issues, 0);
assert!(!response.grade.is_empty());
assert!(response.executive_summary.key_points.len() <= 3);
}
#[test]
fn test_history_entry_from_response() {
let report = AuditReport::new(
"https://example.com".to_string(),
WcagLevel::AA,
WcagResults::new(),
500,
);
let normalized = normalize(&report);
let response = StudioAuditResponse::from_normalized(&normalized, &report, "{}".to_string());
let entry = StudioHistoryEntry::from_response(&response);
assert_eq!(entry.url, response.url);
assert_eq!(entry.accessibility_score, response.accessibility_score);
assert_eq!(entry.overall_score, response.overall_score);
assert_eq!(entry.risk_level, response.risk_level);
}
#[test]
fn test_module_scores_propagated() {
let report = AuditReport::new(
"https://example.com".to_string(),
WcagLevel::AA,
WcagResults::new(),
500,
);
let normalized = normalize(&report);
let response = StudioAuditResponse::from_normalized(&normalized, &report, "{}".to_string());
assert!(
response
.module_scores
.iter()
.any(|m| m.name == "Accessibility"),
"Accessibility module must be in studio response"
);
}
#[test]
fn test_studio_response_is_serializable() {
let report = AuditReport::new(
"https://example.com".to_string(),
WcagLevel::AA,
WcagResults::new(),
500,
);
let normalized = normalize(&report);
let response = StudioAuditResponse::from_normalized(&normalized, &report, "{}".to_string());
let json = serde_json::to_string(&response).expect("serialization must work");
assert!(json.contains("example.com"));
let parsed: StudioAuditResponse =
serde_json::from_str(&json).expect("deserialization must work");
assert_eq!(parsed.url, response.url);
assert_eq!(parsed.accessibility_score, response.accessibility_score);
}
}