use core::fmt;
use serde::Serialize;
use crate::output::Render;
use crate::style::Presentation;
use crate::vocabulary::Role;
use super::toolkit::{Paint, paint};
#[derive(Clone, Serialize)]
pub struct CopyChange {
pub key: String,
pub change: &'static str,
pub old: Option<serde_json::Value>,
pub new: Option<serde_json::Value>,
}
impl fmt::Debug for CopyChange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let slot = |value: &Option<serde_json::Value>| {
if value.is_some() { "<value>" } else { "none" }
};
write!(
f,
"CopyChange {{ key: {:?}, change: {:?}, old: {}, new: {} }}",
self.key,
self.change,
slot(&self.old),
slot(&self.new),
)
}
}
pub(crate) fn render_value(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => s.clone(),
other => serde_json::to_string(other).unwrap_or_else(|_| "<value>".to_string()),
}
}
#[derive(Debug, Clone, Serialize)]
pub struct CopyReport {
pub from: String,
pub to: String,
pub path: String,
pub written: bool,
pub dry_run: bool,
pub pending: Vec<String>,
pub changes: Vec<CopyChange>,
}
impl Render for CopyReport {
fn headers() -> &'static [&'static str] {
&["KEY", "CHANGE", "OLD", "NEW"]
}
fn rows(&self) -> Vec<Vec<String>> {
self.changes
.iter()
.map(|change| {
vec![
change.key.clone(),
change.change.to_string(),
change
.old
.as_ref()
.map_or_else(|| "-".to_string(), render_value),
change
.new
.as_ref()
.map_or_else(|| "-".to_string(), render_value),
]
})
.collect()
}
fn rows_for(&self, presentation: Presentation, status_word: bool) -> Vec<Vec<String>> {
let rows = self.rows();
paint(
rows,
Self::headers(),
presentation,
status_word,
|header, cell, _index| match (header, cell) {
("CHANGE", "removed") => Paint::Role(Role::Bark),
("CHANGE", "added") => Paint::Role(Role::Meadow),
_ => Paint::Default,
},
)
}
#[track_caller]
fn json_key_for(header: &str) -> &'static str {
match header {
"KEY" => "key",
"CHANGE" => "change",
"OLD" => "old",
"NEW" => "new",
other => panic!("CopyReport::headers() does not include {other:?}"),
}
}
const JSON_ONLY: &'static [&'static str] = &[];
const PRIORITIES: &'static [u8] = &[0, 0, 6, 7];
}
#[cfg(test)]
mod tests {
use super::super::tests::assert_no_drift;
use super::*;
fn report() -> CopyReport {
CopyReport {
from: "shep".to_string(),
to: "shep-kelpie".to_string(),
path: "dogs.kelpie.rulings".to_string(),
written: true,
dry_run: false,
pending: Vec::new(),
changes: vec![
CopyChange {
key: "panel".to_string(),
change: "changed",
old: Some(serde_json::Value::String("issue".to_string())),
new: Some(serde_json::Value::String("panel".to_string())),
},
CopyChange {
key: "git.checkout".to_string(),
change: "added",
old: None,
new: Some(serde_json::json!("./worktree")),
},
CopyChange {
key: "legacy".to_string(),
change: "removed",
old: Some(serde_json::json!(4)),
new: None,
},
],
}
}
#[test]
fn a_copy_report_does_not_drift() {
assert_no_drift(&report(), |j| &j["changes"][0], &["OLD", "NEW"]);
}
#[test]
fn a_copy_report_renders_one_row_per_change() {
let report = report();
let json = serde_json::to_value(&report).unwrap();
assert_eq!(json["changes"].as_array().unwrap().len(), 3);
assert_eq!(report.rows().len(), 3);
}
#[test]
fn a_copy_changes_debug_never_prints_a_value() {
let change = CopyChange {
key: "token".to_string(),
change: "changed",
old: Some(serde_json::Value::String("hunter2".to_string())),
new: Some(serde_json::Value::String("rotated".to_string())),
};
assert_eq!(
format!("{change:?}"),
r#"CopyChange { key: "token", change: "changed", old: <value>, new: <value> }"#
);
let absent = CopyChange {
key: "token".to_string(),
change: "added",
old: None,
new: Some(serde_json::json!("hunter2")),
};
assert_eq!(
format!("{absent:?}"),
r#"CopyChange { key: "token", change: "added", old: none, new: <value> }"#
);
assert!(
!format!("{:?}", report()).contains("hunter2"),
"a report prints its changes through this impl"
);
}
#[test]
fn a_value_renders_as_a_bare_string_or_as_compact_json() {
assert_eq!(render_value(&serde_json::json!("panel")), "panel");
assert_eq!(render_value(&serde_json::json!(true)), "true");
assert_eq!(render_value(&serde_json::json!(null)), "null");
assert_eq!(render_value(&serde_json::json!([1, 2])), "[1,2]");
assert_eq!(render_value(&serde_json::json!({ "a": 1 })), r#"{"a":1}"#);
}
}