use acorde_core::Score;
use serde::{Deserialize, Serialize};
pub const REPORT_SCHEMA_VERSION: u32 = 1;
fn default_report_schema_version() -> u32 {
REPORT_SCHEMA_VERSION
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiagnosticSeverity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Diagnostic {
pub code: String,
pub severity: DiagnosticSeverity,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_location: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preserved_value: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub loss_reason: Option<String>,
}
impl Diagnostic {
pub fn info(code: impl Into<String>, preserved_value: impl Into<String>) -> Self {
Self {
code: code.into(),
severity: DiagnosticSeverity::Info,
source_location: None,
preserved_value: Some(preserved_value.into()),
loss_reason: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportReport {
#[serde(default = "default_report_schema_version")]
pub schema_version: u32,
#[serde(default)]
pub format: String,
pub score: Score,
#[serde(default)]
pub diagnostics: Vec<Diagnostic>,
}
impl ImportReport {
pub fn new(score: Score) -> Self {
Self {
schema_version: REPORT_SCHEMA_VERSION,
format: "unknown".to_string(),
score,
diagnostics: Vec::new(),
}
}
pub fn for_format(score: Score, format: impl Into<String>) -> Self {
Self {
format: format.into(),
..Self::new(score)
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExportReport<T> {
#[serde(default = "default_report_schema_version")]
pub schema_version: u32,
#[serde(default)]
pub format: String,
pub output: T,
#[serde(default)]
pub diagnostics: Vec<Diagnostic>,
}
impl<T> ExportReport<T> {
pub fn new(output: T) -> Self {
Self {
schema_version: REPORT_SCHEMA_VERSION,
format: "unknown".to_string(),
output,
diagnostics: Vec::new(),
}
}
pub fn for_format(output: T, format: impl Into<String>) -> Self {
Self {
format: format.into(),
..Self::new(output)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diagnostic_round_trips_with_optional_fields() {
let mut diagnostic = Diagnostic::info("musicxml.title", "A score");
diagnostic.source_location = Some("/score-work/title".to_string());
let json = serde_json::to_string(&diagnostic).expect("diagnostic serializes");
let restored: Diagnostic = serde_json::from_str(&json).expect("diagnostic parses");
assert_eq!(restored, diagnostic);
}
#[test]
fn reports_default_to_no_loss() {
let report = ImportReport::new(Score::default());
assert_eq!(report.schema_version, REPORT_SCHEMA_VERSION);
assert_eq!(report.format, "unknown");
assert!(report.diagnostics.is_empty());
let output = ExportReport::new("output");
assert_eq!(output.schema_version, REPORT_SCHEMA_VERSION);
assert_eq!(output.format, "unknown");
assert!(output.diagnostics.is_empty());
}
#[test]
fn legacy_export_report_defaults_to_current_schema() {
let restored: ExportReport<String> =
serde_json::from_str(r#"{"format":"musicxml","output":"output","diagnostics":[]}"#)
.expect("legacy report parses");
assert_eq!(restored.schema_version, REPORT_SCHEMA_VERSION);
}
}