use crate::model::Model;
use crate::value::Value;
use serde::Serialize;
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct FieldChange {
pub field: &'static str,
pub old: Option<String>,
pub new: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct RecordRef {
pub model: &'static str,
pub id: Option<String>,
}
impl RecordRef {
pub fn of<M: Model>(value: &M) -> Self {
let id = M::fields().iter().position(|f| f.key).and_then(|i| value.value_at(i)).and_then(|v| plain(&v));
RecordRef { model: M::NAME, id }
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct ExportRecord {
pub model: &'static str,
pub format: &'static str,
pub rows: u64,
pub query: serde_json::Value,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct FieldRef {
pub model: &'static str,
pub id: Option<String>,
pub field: &'static str,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct Trail {
pub record: Option<RecordRef>,
pub changes: Vec<FieldChange>,
pub revealed: Vec<FieldRef>,
pub reads: Vec<RecordRef>,
pub reason: Option<String>,
pub transition: Option<crate::workflow::TransitionRecord>,
pub export: Option<ExportRecord>,
#[serde(skip)]
pub(crate) given_reason: Option<(String, String)>,
}
pub fn plain(value: &Value) -> Option<String> {
Some(match value {
Value::Null => return None,
Value::Bool(b) => b.to_string(),
Value::Int(n) => n.to_string(),
Value::Decimal(d) => d.to_string(),
Value::Number(d) => d.to_string(),
Value::Text(s) => s.clone(),
Value::Money(m) => {
let sign = if m.minor < 0 { "-" } else { "" };
let abs = m.minor.unsigned_abs();
format!("{sign}{}.{:02} {}", abs / 100, abs % 100, format!("{:?}", m.currency).to_uppercase())
}
Value::Date(d) => d.to_string(),
Value::Time(t) => t.to_string(),
Value::DateTime(d) => d.to_string(),
Value::LocalDateTime(l) => l.to_string(),
Value::Choice(c) => c.to_string(),
})
}
pub fn diff<M: Model>(old: &M, new: &M) -> Vec<FieldChange> {
M::fields()
.iter()
.enumerate()
.filter(|(_, meta)| !meta.version)
.filter_map(|(i, meta)| {
let (a, b) = (old.value_at(i).unwrap_or(Value::Null), new.value_at(i).unwrap_or(Value::Null));
if a == b {
return None;
}
let shown = |v: &Value| plain(v).map(|s| crate::privacy::masked_text(meta, &s));
Some(FieldChange { field: meta.name, old: shown(&a), new: shown(&b) })
})
.collect()
}