#![allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum FormatRisk {
Preferred,
Stable,
AtRisk,
Endangered,
Obsolete,
}
impl FormatRisk {
#[must_use]
pub const fn priority(&self) -> u8 {
match self {
Self::Preferred => 0,
Self::Stable => 1,
Self::AtRisk => 2,
Self::Endangered => 3,
Self::Obsolete => 4,
}
}
#[must_use]
pub const fn label(&self) -> &str {
match self {
Self::Preferred => "preferred",
Self::Stable => "stable",
Self::AtRisk => "at_risk",
Self::Endangered => "endangered",
Self::Obsolete => "obsolete",
}
}
}
#[derive(Clone, Debug)]
pub struct FileFormat {
pub name: String,
pub extension: String,
pub mime_type: String,
pub risk: FormatRisk,
}
impl FileFormat {
#[must_use]
pub fn new(
name: impl Into<String>,
extension: impl Into<String>,
mime_type: impl Into<String>,
risk: FormatRisk,
) -> Self {
Self {
name: name.into(),
extension: extension.into(),
mime_type: mime_type.into(),
risk,
}
}
#[must_use]
pub fn dpx() -> Self {
Self::new("DPX", "dpx", "image/x-dpx", FormatRisk::Preferred)
}
#[must_use]
pub fn prores_4444() -> Self {
Self::new("ProRes 4444", "mov", "video/quicktime", FormatRisk::Stable)
}
#[must_use]
pub fn avid_dnxhd() -> Self {
Self::new("Avid DNxHD", "mxf", "application/mxf", FormatRisk::Stable)
}
#[must_use]
pub fn h264_mp4() -> Self {
Self::new("H.264/MP4", "mp4", "video/mp4", FormatRisk::Stable)
}
#[must_use]
pub fn mpeg2() -> Self {
Self::new("MPEG-2", "mpg", "video/mpeg", FormatRisk::AtRisk)
}
#[must_use]
pub fn dv() -> Self {
Self::new("DV", "dv", "video/x-dv", FormatRisk::Endangered)
}
#[must_use]
pub fn betacam() -> Self {
Self::new(
"Betacam SP",
"betacam",
"application/octet-stream",
FormatRisk::Obsolete,
)
}
}
#[derive(Clone, Debug)]
pub struct MigrationPath {
pub source: FileFormat,
pub target: FileFormat,
pub quality_loss: bool,
pub reversible: bool,
pub notes: String,
}
pub struct MigrationPlanner;
impl MigrationPlanner {
#[must_use]
pub fn plan_migration(source: &FileFormat) -> Option<MigrationPath> {
match source.risk {
FormatRisk::Preferred | FormatRisk::Stable => None,
FormatRisk::AtRisk => Some(MigrationPath {
source: source.clone(),
target: FileFormat::prores_4444(),
quality_loss: false,
reversible: false,
notes: "Migrate to ProRes 4444 for better long-term tool support.".to_string(),
}),
FormatRisk::Endangered => Some(MigrationPath {
source: source.clone(),
target: FileFormat::dpx(),
quality_loss: false,
reversible: false,
notes: "Urgent: migrate to DPX image sequence for lossless preservation."
.to_string(),
}),
FormatRisk::Obsolete => Some(MigrationPath {
source: source.clone(),
target: FileFormat::dpx(),
quality_loss: false,
reversible: false,
notes: "Critical: format is obsolete — immediate migration to DPX required."
.to_string(),
}),
}
}
#[must_use]
pub fn estimate_cost_gb(format: &FileFormat, size_gb: f64) -> f64 {
let cpu_hours_per_gb = match format.risk {
FormatRisk::Preferred => 0.1,
FormatRisk::Stable => 0.2,
FormatRisk::AtRisk => 0.5,
FormatRisk::Endangered => 1.5,
FormatRisk::Obsolete => 4.0,
};
cpu_hours_per_gb * size_gb
}
}
#[derive(Clone, Debug)]
pub struct MigrationBatch {
pub items: Vec<(String, MigrationPath)>,
pub total_size_gb: f64,
pub estimated_hours: f64,
}
impl MigrationBatch {
#[must_use]
pub fn new() -> Self {
Self {
items: Vec::new(),
total_size_gb: 0.0,
estimated_hours: 0.0,
}
}
pub fn add(&mut self, asset_id: impl Into<String>, path: MigrationPath, size_gb: f64) {
let cost = MigrationPlanner::estimate_cost_gb(&path.source, size_gb);
self.estimated_hours += cost;
self.total_size_gb += size_gb;
self.items.push((asset_id.into(), path));
}
#[must_use]
pub fn len(&self) -> usize {
self.items.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
impl Default for MigrationBatch {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_risk_priority_ordering() {
assert!(FormatRisk::Obsolete.priority() > FormatRisk::Preferred.priority());
assert!(FormatRisk::Endangered.priority() > FormatRisk::AtRisk.priority());
}
#[test]
fn test_format_risk_labels_non_empty() {
for risk in [
FormatRisk::Preferred,
FormatRisk::Stable,
FormatRisk::AtRisk,
FormatRisk::Endangered,
FormatRisk::Obsolete,
] {
assert!(!risk.label().is_empty());
}
}
#[test]
fn test_dpx_is_preferred() {
assert_eq!(FileFormat::dpx().risk, FormatRisk::Preferred);
}
#[test]
fn test_betacam_is_obsolete() {
assert_eq!(FileFormat::betacam().risk, FormatRisk::Obsolete);
}
#[test]
fn test_dv_is_endangered() {
assert_eq!(FileFormat::dv().risk, FormatRisk::Endangered);
}
#[test]
fn test_mpeg2_is_at_risk() {
assert_eq!(FileFormat::mpeg2().risk, FormatRisk::AtRisk);
}
#[test]
fn test_plan_migration_preferred_none() {
let dpx = FileFormat::dpx();
assert!(MigrationPlanner::plan_migration(&dpx).is_none());
}
#[test]
fn test_plan_migration_at_risk_some() {
let mpeg2 = FileFormat::mpeg2();
let path = MigrationPlanner::plan_migration(&mpeg2);
assert!(path.is_some());
let path = path.unwrap();
assert_eq!(path.target.risk, FormatRisk::Stable);
}
#[test]
fn test_plan_migration_obsolete_to_dpx() {
let betacam = FileFormat::betacam();
let path = MigrationPlanner::plan_migration(&betacam).unwrap();
assert_eq!(path.target.extension, "dpx");
}
#[test]
fn test_estimate_cost_gb_preferred_cheap() {
let dpx = FileFormat::dpx();
let cost = MigrationPlanner::estimate_cost_gb(&dpx, 10.0);
assert!(cost <= 1.0, "cost = {cost}");
}
#[test]
fn test_estimate_cost_gb_obsolete_expensive() {
let betacam = FileFormat::betacam();
let cost = MigrationPlanner::estimate_cost_gb(&betacam, 10.0);
assert!(cost > 10.0, "cost = {cost}");
}
#[test]
fn test_migration_batch_add() {
let mut batch = MigrationBatch::new();
let dv = FileFormat::dv();
let path = MigrationPlanner::plan_migration(&dv).unwrap();
batch.add("asset-001", path, 5.0);
assert_eq!(batch.len(), 1);
assert!((batch.total_size_gb - 5.0).abs() < 1e-9);
assert!(batch.estimated_hours > 0.0);
}
#[test]
fn test_migration_batch_is_empty() {
let batch = MigrationBatch::new();
assert!(batch.is_empty());
}
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum MigrationRisk {
Low,
Medium,
High,
Critical,
}
impl MigrationRisk {
#[must_use]
pub const fn score(self) -> u32 {
match self {
Self::Low => 1,
Self::Medium => 2,
Self::High => 3,
Self::Critical => 4,
}
}
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FormatSupport {
FullySupported,
LegacySupport,
DeprecatedSoon,
Unsupported,
}
impl FormatSupport {
#[must_use]
pub const fn migration_risk(self) -> MigrationRisk {
match self {
Self::FullySupported => MigrationRisk::Low,
Self::LegacySupport => MigrationRisk::Medium,
Self::DeprecatedSoon => MigrationRisk::High,
Self::Unsupported => MigrationRisk::Critical,
}
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct MigrationTask {
pub source_format: String,
pub target_format: String,
pub file_count: u32,
pub estimated_hours: f32,
}
impl MigrationTask {
#[must_use]
pub fn is_large(&self) -> bool {
self.file_count > 1_000
}
}
#[allow(dead_code)]
#[derive(Default, Debug)]
pub struct MigrationPlan {
pub tasks: Vec<MigrationTask>,
pub description: String,
}
impl MigrationPlan {
#[must_use]
pub fn new(description: impl Into<String>) -> Self {
Self {
tasks: Vec::new(),
description: description.into(),
}
}
pub fn add(&mut self, task: MigrationTask) {
self.tasks.push(task);
}
#[must_use]
pub fn total_files(&self) -> u32 {
self.tasks.iter().map(|t| t.file_count).sum()
}
#[must_use]
pub fn total_hours(&self) -> f32 {
self.tasks.iter().map(|t| t.estimated_hours).sum()
}
#[must_use]
pub fn high_risk_tasks(&self) -> Vec<&MigrationTask> {
self.tasks
.iter()
.filter(|t| t.is_large() || t.estimated_hours > 24.0)
.collect()
}
}
#[cfg(test)]
mod migration_plan_tests {
use super::*;
fn small_task(src: &str, tgt: &str, n: u32, h: f32) -> MigrationTask {
MigrationTask {
source_format: src.to_string(),
target_format: tgt.to_string(),
file_count: n,
estimated_hours: h,
}
}
#[test]
fn test_migration_risk_score_ordering() {
assert!(MigrationRisk::Critical.score() > MigrationRisk::High.score());
assert!(MigrationRisk::High.score() > MigrationRisk::Medium.score());
assert!(MigrationRisk::Medium.score() > MigrationRisk::Low.score());
}
#[test]
fn test_migration_risk_score_values() {
assert_eq!(MigrationRisk::Low.score(), 1);
assert_eq!(MigrationRisk::Critical.score(), 4);
}
#[test]
fn test_format_support_fully_supported_low_risk() {
assert_eq!(
FormatSupport::FullySupported.migration_risk(),
MigrationRisk::Low
);
}
#[test]
fn test_format_support_unsupported_critical_risk() {
assert_eq!(
FormatSupport::Unsupported.migration_risk(),
MigrationRisk::Critical
);
}
#[test]
fn test_format_support_deprecated_high_risk() {
assert_eq!(
FormatSupport::DeprecatedSoon.migration_risk(),
MigrationRisk::High
);
}
#[test]
fn test_migration_task_is_large_false() {
let t = small_task("DV", "DPX", 500, 5.0);
assert!(!t.is_large());
}
#[test]
fn test_migration_task_is_large_true() {
let t = small_task("DV", "DPX", 2_000, 40.0);
assert!(t.is_large());
}
#[test]
fn test_migration_plan_total_files() {
let mut plan = MigrationPlan::new("Q1 migration");
plan.add(small_task("A", "B", 100, 2.0));
plan.add(small_task("C", "D", 200, 4.0));
assert_eq!(plan.total_files(), 300);
}
#[test]
fn test_migration_plan_total_hours() {
let mut plan = MigrationPlan::new("test");
plan.add(small_task("A", "B", 10, 1.5));
plan.add(small_task("C", "D", 10, 2.5));
assert!((plan.total_hours() - 4.0).abs() < 1e-6);
}
#[test]
fn test_migration_plan_high_risk_by_file_count() {
let mut plan = MigrationPlan::new("big batch");
plan.add(small_task("DV", "DPX", 5_000, 10.0));
plan.add(small_task("MP4", "MOV", 50, 1.0));
let hr = plan.high_risk_tasks();
assert_eq!(hr.len(), 1);
assert_eq!(hr[0].source_format, "DV");
}
#[test]
fn test_migration_plan_high_risk_by_hours() {
let mut plan = MigrationPlan::new("long job");
plan.add(small_task("old", "new", 100, 30.0));
assert_eq!(plan.high_risk_tasks().len(), 1);
}
#[test]
fn test_migration_plan_no_high_risk() {
let mut plan = MigrationPlan::new("easy");
plan.add(small_task("A", "B", 50, 2.0));
assert!(plan.high_risk_tasks().is_empty());
}
}