use std::path::Path;
use serde::Deserialize;
use serde::de::DeserializeOwned;
use super::error::{PlanPersistenceError, PlanPersistenceResult};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlanDocumentKind {
ExecutablePlan,
DryRunReport { source_command: Option<String> },
Unrecognized,
}
#[derive(Deserialize)]
struct DocumentProbe {
#[serde(default)]
id: Option<serde_json::Value>,
#[serde(default)]
dry_run: Option<bool>,
#[serde(default)]
command: Option<String>,
}
pub(crate) fn deserialize_plan_document<T: DeserializeOwned>(
path: &Path,
bytes: &[u8],
expected_command: &str,
) -> PlanPersistenceResult<T> {
match serde_json::from_slice::<T>(bytes) {
Ok(document) => Ok(document),
Err(decode_error) => Err(diagnose_load_failure(
path,
bytes,
expected_command,
decode_error,
)),
}
}
fn diagnose_load_failure(
path: &Path,
bytes: &[u8],
expected_command: &str,
decode_error: serde_json::Error,
) -> PlanPersistenceError {
match classify_plan_document(bytes) {
PlanDocumentKind::DryRunReport { source_command } => PlanPersistenceError::DryRunReport {
path: path.to_path_buf(),
source_command: source_command.unwrap_or_else(|| expected_command.to_string()),
expected_command: expected_command.to_string(),
},
PlanDocumentKind::Unrecognized => PlanPersistenceError::UnrecognizedDocument {
path: path.to_path_buf(),
},
PlanDocumentKind::ExecutablePlan => PlanPersistenceError::Decode {
path: path.to_path_buf(),
message: decode_error.to_string(),
},
}
}
pub(crate) fn classify_plan_document(bytes: &[u8]) -> PlanDocumentKind {
let Ok(probe) = serde_json::from_slice::<DocumentProbe>(bytes) else {
return PlanDocumentKind::Unrecognized;
};
if probe.id.is_some() {
PlanDocumentKind::ExecutablePlan
} else if probe.dry_run.is_some() || probe.command.is_some() {
PlanDocumentKind::DryRunReport {
source_command: probe.command,
}
} else {
PlanDocumentKind::Unrecognized
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn executable_plan_is_detected_by_top_level_id() {
let bytes = br#"{"schema_version":1,"id":"abc123","created_at":{}}"#;
assert_eq!(
classify_plan_document(bytes),
PlanDocumentKind::ExecutablePlan
);
}
#[test]
fn dry_run_report_is_detected_without_id_and_carries_its_command() {
let bytes = br#"{"schema_version":1,"command":"plan","dry_run":true,"entries":[]}"#;
assert_eq!(
classify_plan_document(bytes),
PlanDocumentKind::DryRunReport {
source_command: Some("plan".to_string()),
}
);
}
#[test]
fn cargo_home_report_is_detected_by_command_without_id() {
let bytes = br#"{"schema_version":1,"command":"cargo-home plan","dry_run":true}"#;
assert_eq!(
classify_plan_document(bytes),
PlanDocumentKind::DryRunReport {
source_command: Some("cargo-home plan".to_string()),
}
);
}
#[test]
fn foreign_object_is_unrecognized() {
assert_eq!(
classify_plan_document(br#"{"hello":"world"}"#),
PlanDocumentKind::Unrecognized
);
}
#[test]
fn non_object_json_is_unrecognized() {
assert_eq!(
classify_plan_document(b"[1, 2, 3]"),
PlanDocumentKind::Unrecognized
);
}
#[test]
fn invalid_json_is_unrecognized() {
assert_eq!(
classify_plan_document(b"not json at all"),
PlanDocumentKind::Unrecognized
);
}
}