use std::path::{Path, PathBuf};
use laterite_ags4_reference::dict::Dictionary;
use super::{Document, resolve_edition, validator_kind};
use crate::{Error, ErrorKind};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Change {
Added,
Removed,
Changed,
}
#[derive(Debug, Clone)]
pub struct CellChange {
heading: String,
ags_type: String,
baseline: Option<String>,
revision: Option<String>,
}
impl CellChange {
#[must_use]
pub fn heading(&self) -> &str {
&self.heading
}
#[must_use]
pub fn ags_type(&self) -> &str {
&self.ags_type
}
#[must_use]
pub fn baseline(&self) -> Option<&str> {
self.baseline.as_deref()
}
#[must_use]
pub fn revision(&self) -> Option<&str> {
self.revision.as_deref()
}
}
#[derive(Debug, Clone)]
pub struct RowChange {
change: Change,
key: Vec<String>,
line_baseline: Option<u32>,
line_revision: Option<u32>,
cells: Vec<CellChange>,
}
impl RowChange {
#[must_use]
pub fn change(&self) -> Change {
self.change
}
#[must_use]
pub fn key(&self) -> &[String] {
&self.key
}
#[must_use]
pub fn line_baseline(&self) -> Option<u32> {
self.line_baseline
}
#[must_use]
pub fn line_revision(&self) -> Option<u32> {
self.line_revision
}
#[must_use]
pub fn cells(&self) -> &[CellChange] {
&self.cells
}
}
#[derive(Debug, Clone)]
pub struct GroupChange {
code: String,
added: usize,
removed: usize,
changed: usize,
headings_added: Vec<String>,
headings_removed: Vec<String>,
keyed: bool,
key_headings: Vec<String>,
rows: Vec<RowChange>,
}
impl GroupChange {
#[must_use]
pub fn code(&self) -> &str {
&self.code
}
#[must_use]
pub fn added(&self) -> usize {
self.added
}
#[must_use]
pub fn removed(&self) -> usize {
self.removed
}
#[must_use]
pub fn changed(&self) -> usize {
self.changed
}
#[must_use]
pub fn headings_added(&self) -> &[String] {
&self.headings_added
}
#[must_use]
pub fn headings_removed(&self) -> &[String] {
&self.headings_removed
}
#[must_use]
pub fn keyed(&self) -> bool {
self.keyed
}
#[must_use]
pub fn key_headings(&self) -> &[String] {
&self.key_headings
}
#[must_use]
pub fn rows(&self) -> &[RowChange] {
&self.rows
}
}
#[derive(Debug, Clone)]
pub struct Delta {
groups: Vec<GroupChange>,
groups_added: Vec<String>,
groups_removed: Vec<String>,
added: usize,
removed: usize,
changed: usize,
}
impl Delta {
#[must_use]
pub fn groups(&self) -> &[GroupChange] {
&self.groups
}
#[must_use]
pub fn groups_added(&self) -> &[String] {
&self.groups_added
}
#[must_use]
pub fn groups_removed(&self) -> &[String] {
&self.groups_removed
}
#[must_use]
pub fn added(&self) -> usize {
self.added
}
#[must_use]
pub fn removed(&self) -> usize {
self.removed
}
#[must_use]
pub fn changed(&self) -> usize {
self.changed
}
#[must_use]
pub fn is_unchanged(&self) -> bool {
self.groups.is_empty() && self.groups_added.is_empty() && self.groups_removed.is_empty()
}
}
enum Side<'a> {
Path(PathBuf),
Bytes(Vec<u8>),
Document(&'a Document),
}
impl Side<'_> {
fn bytes(&self) -> Result<Vec<u8>, Error> {
match self {
Side::Path(p) => std::fs::read(p).map_err(|e| {
Error::with_source(ErrorKind::Io, format!("cannot read {}", p.display()), e)
}),
Side::Bytes(b) => Ok(b.clone()),
Side::Document(doc) => super::write(doc)
.mode(super::WriteMode::Report)
.to_bytes()
.map(super::Written::into_bytes),
}
}
fn describe(&self) -> String {
match self {
Side::Path(p) => format!("path {}", p.display()),
Side::Bytes(b) => format!("{} bytes", b.len()),
Side::Document(doc) => format!("document of {} groups", doc.len()),
}
}
}
pub struct Diff<'a> {
baseline: Side<'a>,
revision: Side<'a>,
edition: Option<String>,
encoding: Option<String>,
max_rows_per_group: Option<usize>,
}
fn pending<'a>(baseline: Side<'a>, revision: Side<'a>) -> Diff<'a> {
Diff {
baseline,
revision,
edition: None,
encoding: None,
max_rows_per_group: None,
}
}
#[must_use]
pub fn diff(baseline: impl AsRef<Path>, revision: impl AsRef<Path>) -> Diff<'static> {
pending(
Side::Path(baseline.as_ref().to_path_buf()),
Side::Path(revision.as_ref().to_path_buf()),
)
}
#[must_use]
pub fn diff_bytes(baseline: impl Into<Vec<u8>>, revision: impl Into<Vec<u8>>) -> Diff<'static> {
pending(Side::Bytes(baseline.into()), Side::Bytes(revision.into()))
}
#[must_use]
pub fn diff_documents<'a>(baseline: &'a Document, revision: &'a Document) -> Diff<'a> {
pending(Side::Document(baseline), Side::Document(revision))
}
impl<'a> Diff<'a> {
#[must_use]
pub fn edition(mut self, edition: impl Into<String>) -> Diff<'a> {
self.edition = Some(edition.into());
self
}
#[must_use]
pub fn encoding(mut self, label: impl Into<String>) -> Diff<'a> {
self.encoding = Some(label.into());
self
}
#[must_use]
pub fn max_rows_per_group(mut self, rows: usize) -> Diff<'a> {
self.max_rows_per_group = Some(rows);
self
}
pub fn run(self) -> Result<Delta, Error> {
let a = self.baseline.bytes()?;
let b = self.revision.bytes()?;
let enc = laterite_ags4_parse::resolve_encoding(self.encoding.as_deref())
.ok_or_else(|| super::bad_encoding(self.encoding.as_deref().unwrap_or_default()))?;
let parse = |bytes: &[u8], which: &str| {
laterite_ags4_parse::parse_bytes(bytes, enc)
.map_err(laterite_ags4_validator::ValidatorError::from)
.map_err(|e| {
Error::with_source(
validator_kind(e.kind()),
format!("cannot read the {which} as AGS4"),
e,
)
})
};
let parsed_a = parse(&a, "baseline")?;
let parsed_b = parse(&b, "revision")?;
let forced = self.edition.as_deref().map(resolve_edition).transpose()?;
let tran = laterite_ags4_validator::tran_ags_of(&parsed_b);
let edition = laterite_ags4_validator::resolve_dict_version(forced, tran.as_deref())
.map_or(laterite_ags4_reference::dict::FALLBACK, |(dv, _)| dv);
let dict = Dictionary::bundled(edition);
let delta =
laterite_ags4_diff::diff_parsed(&parsed_a, &parsed_b, &dict, self.max_rows_per_group);
Ok(Delta {
groups: delta.groups.into_iter().map(convert_group).collect(),
groups_added: delta.groups_added,
groups_removed: delta.groups_removed,
added: delta.total_added,
removed: delta.total_removed,
changed: delta.total_changed,
})
}
}
fn convert_group(g: laterite_ags4_diff::GroupDelta) -> GroupChange {
GroupChange {
code: g.code,
added: g.added,
removed: g.removed,
changed: g.changed,
headings_added: g.headings_added,
headings_removed: g.headings_removed,
keyed: g.keyed,
key_headings: g.key_headings,
rows: g
.rows
.into_iter()
.map(|r| RowChange {
change: match r.kind {
"added" => Change::Added,
"removed" => Change::Removed,
_ => Change::Changed,
},
key: r.key,
line_baseline: r.line_a,
line_revision: r.line_b,
cells: r
.cells
.into_iter()
.map(|c| CellChange {
heading: c.heading,
ags_type: c.ags_type,
baseline: c.a,
revision: c.b,
})
.collect(),
})
.collect(),
}
}
impl std::fmt::Debug for Diff<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Diff")
.field("baseline", &self.baseline.describe())
.field("revision", &self.revision.describe())
.field("edition", &self.edition)
.field("encoding", &self.encoding)
.field("max_rows_per_group", &self.max_rows_per_group)
.finish()
}
}