#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TranslationStatus {
NotStarted,
InProgress,
Ported,
Verified,
Skipped,
}
impl TranslationStatus {
pub fn parse_str(s: &str) -> Option<Self> {
match s.to_lowercase().replace('-', "_").as_str() {
"not_started" => Some(Self::NotStarted),
"in_progress" => Some(Self::InProgress),
"ported" => Some(Self::Ported),
"verified" => Some(Self::Verified),
"skipped" => Some(Self::Skipped),
_ => None,
}
}
pub fn label(&self) -> &str {
match self {
Self::NotStarted => "not_started",
Self::InProgress => "in_progress",
Self::Ported => "ported",
Self::Verified => "verified",
Self::Skipped => "skipped",
}
}
fn is_complete(&self) -> bool {
matches!(self, Self::Ported | Self::Verified | Self::Skipped)
}
}
impl std::fmt::Display for TranslationStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug)]
pub struct TranslationMapping {
pub source_symbol: String,
pub target_symbol: Option<String>,
pub status: TranslationStatus,
pub notes: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TranslationProgress {
pub total: usize,
pub not_started: usize,
pub in_progress: usize,
pub ported: usize,
pub verified: usize,
pub skipped: usize,
pub percent_complete: f32,
}
#[derive(Debug)]
pub struct TranslationMap {
pub source_context: String,
pub target_context: String,
mappings: Vec<TranslationMapping>,
}
impl TranslationMap {
pub fn new(source_context: String, target_context: String) -> Self {
Self {
source_context,
target_context,
mappings: Vec::new(),
}
}
pub fn record(
&mut self,
source: &str,
target: Option<&str>,
status: TranslationStatus,
notes: Option<String>,
) {
if let Some(existing) = self.mappings.iter_mut().find(|m| m.source_symbol == source) {
existing.target_symbol = target.map(|s| s.to_string());
existing.status = status;
existing.notes = notes;
return;
}
self.mappings.push(TranslationMapping {
source_symbol: source.to_string(),
target_symbol: target.map(|s| s.to_string()),
status,
notes,
});
}
pub fn status(&self, source: &str) -> Option<&TranslationMapping> {
self.mappings.iter().find(|m| m.source_symbol == source)
}
pub fn progress(&self) -> TranslationProgress {
let total = self.mappings.len();
let mut not_started = 0usize;
let mut in_progress = 0usize;
let mut ported = 0usize;
let mut verified = 0usize;
let mut skipped = 0usize;
for m in &self.mappings {
match m.status {
TranslationStatus::NotStarted => not_started += 1,
TranslationStatus::InProgress => in_progress += 1,
TranslationStatus::Ported => ported += 1,
TranslationStatus::Verified => verified += 1,
TranslationStatus::Skipped => skipped += 1,
}
}
let percent_complete = if total > 0 {
(ported + verified + skipped) as f32 / total as f32 * 100.0
} else {
0.0
};
TranslationProgress {
total,
not_started,
in_progress,
ported,
verified,
skipped,
percent_complete,
}
}
pub fn remaining(&self) -> Vec<&TranslationMapping> {
self.mappings
.iter()
.filter(|m| !m.status.is_complete())
.collect()
}
pub fn completed(&self) -> Vec<&TranslationMapping> {
self.mappings
.iter()
.filter(|m| m.status.is_complete())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_and_status() {
let mut tm = TranslationMap::new("ctx-1".into(), "ctx-2".into());
tm.record("foo", Some("foo_rs"), TranslationStatus::Ported, None);
let m = tm.status("foo").unwrap();
assert_eq!(m.target_symbol.as_deref(), Some("foo_rs"));
assert_eq!(m.status, TranslationStatus::Ported);
}
#[test]
fn record_updates_existing() {
let mut tm = TranslationMap::new("ctx-1".into(), "ctx-2".into());
tm.record("bar", None, TranslationStatus::NotStarted, None);
tm.record(
"bar",
Some("bar_rs"),
TranslationStatus::InProgress,
Some("WIP".into()),
);
assert_eq!(tm.mappings.len(), 1, "should update, not duplicate");
let m = tm.status("bar").unwrap();
assert_eq!(m.status, TranslationStatus::InProgress);
assert_eq!(m.notes.as_deref(), Some("WIP"));
}
#[test]
fn progress_calculation() {
let mut tm = TranslationMap::new("a".into(), "b".into());
tm.record("s1", None, TranslationStatus::NotStarted, None);
tm.record("s2", None, TranslationStatus::InProgress, None);
tm.record("s3", Some("t3"), TranslationStatus::Ported, None);
tm.record("s4", Some("t4"), TranslationStatus::Verified, None);
tm.record("s5", None, TranslationStatus::Skipped, None);
let p = tm.progress();
assert_eq!(p.total, 5);
assert_eq!(p.not_started, 1);
assert_eq!(p.in_progress, 1);
assert_eq!(p.ported, 1);
assert_eq!(p.verified, 1);
assert_eq!(p.skipped, 1);
assert!((p.percent_complete - 60.0).abs() < 0.01);
}
#[test]
fn progress_empty() {
let tm = TranslationMap::new("a".into(), "b".into());
let p = tm.progress();
assert_eq!(p.total, 0);
assert!((p.percent_complete - 0.0).abs() < 0.01);
}
#[test]
fn remaining_and_completed() {
let mut tm = TranslationMap::new("a".into(), "b".into());
tm.record("s1", None, TranslationStatus::NotStarted, None);
tm.record("s2", None, TranslationStatus::InProgress, None);
tm.record("s3", Some("t3"), TranslationStatus::Ported, None);
tm.record("s4", Some("t4"), TranslationStatus::Verified, None);
tm.record("s5", None, TranslationStatus::Skipped, None);
let rem: Vec<_> = tm
.remaining()
.iter()
.map(|m| m.source_symbol.as_str())
.collect();
assert_eq!(rem, vec!["s1", "s2"]);
let done: Vec<_> = tm
.completed()
.iter()
.map(|m| m.source_symbol.as_str())
.collect();
assert_eq!(done, vec!["s3", "s4", "s5"]);
}
#[test]
fn translation_status_roundtrip() {
for label in &[
"not_started",
"in_progress",
"ported",
"verified",
"skipped",
] {
let status = TranslationStatus::parse_str(label).unwrap();
assert_eq!(status.label(), *label);
}
assert_eq!(
TranslationStatus::parse_str("not-started"),
Some(TranslationStatus::NotStarted)
);
assert_eq!(
TranslationStatus::parse_str("in-progress"),
Some(TranslationStatus::InProgress)
);
assert!(TranslationStatus::parse_str("bogus").is_none());
}
#[test]
fn status_returns_none_for_unknown() {
let tm = TranslationMap::new("a".into(), "b".into());
assert!(tm.status("nonexistent").is_none());
}
}