use std::path::{Path, PathBuf};
use laterite_ags4_merge::{MergeError, MergeOpts, MissingTranMode, TypeClashMode, merge_parsed};
use super::{Document, WriteMode, resolve_edition, validator_kind};
use crate::{Error, ErrorKind};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TypeClash {
#[default]
Refuse,
Widen,
Promote,
}
impl TypeClash {
fn to_engine(self) -> TypeClashMode {
match self {
TypeClash::Refuse => TypeClashMode::Error,
TypeClash::Widen => TypeClashMode::Widen,
TypeClash::Promote => TypeClashMode::Promote,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MissingTran {
#[default]
Reconcile,
Refuse,
}
impl MissingTran {
fn to_engine(self) -> MissingTranMode {
match self {
MissingTran::Reconcile => MissingTranMode::Reconcile,
MissingTran::Refuse => MissingTranMode::Error,
}
}
}
#[derive(Debug, Clone)]
pub struct Note {
kind: String,
group: Option<String>,
heading: Option<String>,
message: String,
}
impl Note {
#[must_use]
pub fn kind(&self) -> &str {
&self.kind
}
#[must_use]
pub fn group(&self) -> Option<&str> {
self.group.as_deref()
}
#[must_use]
pub fn heading(&self) -> Option<&str> {
self.heading.as_deref()
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
#[derive(Debug, Clone)]
pub struct Revision {
group: String,
key: Vec<String>,
changed: Vec<String>,
winner: usize,
}
impl Revision {
#[must_use]
pub fn group(&self) -> &str {
&self.group
}
#[must_use]
pub fn key(&self) -> &[String] {
&self.key
}
#[must_use]
pub fn changed(&self) -> &[String] {
&self.changed
}
#[must_use]
pub fn winner(&self) -> usize {
self.winner
}
}
pub struct Merged {
text: String,
notes: Vec<Note>,
revisions: Vec<Revision>,
}
impl Merged {
#[must_use]
pub fn bytes(&self) -> &[u8] {
self.text.as_bytes()
}
#[must_use]
pub fn into_bytes(self) -> Vec<u8> {
self.text.into_bytes()
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub fn into_text(self) -> String {
self.text
}
#[must_use]
pub fn notes(&self) -> &[Note] {
&self.notes
}
#[must_use]
pub fn revisions(&self) -> &[Revision] {
&self.revisions
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), Error> {
let path = path.as_ref();
std::fs::write(path, self.bytes()).map_err(|e| {
Error::with_source(ErrorKind::Io, format!("cannot write {}", path.display()), e)
})
}
}
enum Source<'a> {
Path(PathBuf),
Bytes(Vec<u8>),
Document(&'a Document),
}
impl Source<'_> {
fn bytes(&self) -> Result<Vec<u8>, Error> {
match self {
Source::Path(p) => std::fs::read(p).map_err(|e| {
Error::with_source(ErrorKind::Io, format!("cannot read {}", p.display()), e)
}),
Source::Bytes(b) => Ok(b.clone()),
Source::Document(doc) => super::write(doc)
.mode(WriteMode::Report)
.to_bytes()
.map(super::Written::into_bytes),
}
}
fn describe(&self) -> String {
match self {
Source::Path(p) => format!("path {}", p.display()),
Source::Bytes(b) => format!("{} bytes", b.len()),
Source::Document(doc) => format!("document of {} groups", doc.len()),
}
}
}
pub struct Merge<'a> {
sources: Vec<Source<'a>>,
on_type_clash: TypeClash,
on_missing_tran: MissingTran,
edition: Option<String>,
encoding: Option<String>,
mode: WriteMode,
tran: Option<laterite_ags4_emit::TranStamp>,
}
fn pending(sources: Vec<Source<'_>>) -> Merge<'_> {
Merge {
sources,
on_type_clash: TypeClash::default(),
on_missing_tran: MissingTran::default(),
edition: None,
encoding: None,
mode: WriteMode::default(),
tran: None,
}
}
#[must_use]
pub fn merge<I, P>(sources: I) -> Merge<'static>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
pending(
sources
.into_iter()
.map(|p| Source::Path(p.as_ref().to_path_buf()))
.collect(),
)
}
#[must_use]
pub fn merge_bytes<I, B>(sources: I) -> Merge<'static>
where
I: IntoIterator<Item = B>,
B: Into<Vec<u8>>,
{
pending(
sources
.into_iter()
.map(|b| Source::Bytes(b.into()))
.collect(),
)
}
#[must_use]
pub fn merge_documents<'a, I>(sources: I) -> Merge<'a>
where
I: IntoIterator<Item = &'a Document>,
{
pending(sources.into_iter().map(Source::Document).collect())
}
impl<'a> Merge<'a> {
#[must_use]
pub fn on_type_clash(mut self, mode: TypeClash) -> Merge<'a> {
self.on_type_clash = mode;
self
}
#[must_use]
pub fn on_missing_tran(mut self, mode: MissingTran) -> Merge<'a> {
self.on_missing_tran = mode;
self
}
#[must_use]
pub fn edition(mut self, edition: impl Into<String>) -> Merge<'a> {
self.edition = Some(edition.into());
self
}
#[must_use]
pub fn encoding(mut self, label: impl Into<String>) -> Merge<'a> {
self.encoding = Some(label.into());
self
}
#[must_use]
pub fn mode(mut self, mode: WriteMode) -> Merge<'a> {
self.mode = mode;
self
}
#[must_use]
pub fn transmission(
mut self,
issue_number: impl Into<String>,
date: impl Into<String>,
producer: impl Into<String>,
recipient: impl Into<String>,
status: impl Into<String>,
) -> Merge<'a> {
self.tran = Some(laterite_ags4_emit::TranStamp::new(
issue_number,
date,
producer,
recipient,
status,
));
self
}
pub fn run(self) -> Result<Merged, Error> {
if self.sources.len() < 2 {
return Err(Error::new(
ErrorKind::InvalidArgument,
format!(
"merge needs at least two files, and was given {}",
self.sources.len()
),
));
}
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 mut parsed = Vec::with_capacity(self.sources.len());
for (i, source) in self.sources.iter().enumerate() {
let bytes = source.bytes()?;
parsed.push(
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 source {i} as AGS4"),
e,
)
})?,
);
}
let opts = MergeOpts {
on_type_clash: self.on_type_clash.to_engine(),
on_missing_tran: self.on_missing_tran.to_engine(),
edition: match &self.edition {
Some(label) => resolve_edition(label)?,
None => laterite_ags4_reference::dict::FALLBACK,
},
emit_mode: match self.mode {
WriteMode::AutoFix => laterite_ags4_emit::EmitMode::AutoFix,
WriteMode::Report => laterite_ags4_emit::EmitMode::Report,
WriteMode::Strict => laterite_ags4_emit::EmitMode::Strict,
},
tran: self.tran,
};
let result = merge_parsed(&parsed, &opts).map_err(|e| {
let kind = match &e {
MergeError::TypeConflict { .. } => ErrorKind::TypeConflict,
MergeError::UnitConflict { .. } => ErrorKind::UnitConflict,
MergeError::MissingTran => ErrorKind::MissingTran,
MergeError::Emit(_) => ErrorKind::Emit,
};
Error::new(kind, e.to_string())
})?;
Ok(Merged {
text: String::from_utf8(result.bytes).map_err(|e| {
Error::with_source(
ErrorKind::Emit,
"the merge produced bytes that are not UTF-8",
e,
)
})?,
notes: result
.warnings
.into_iter()
.map(|w| Note {
kind: w.kind.to_string(),
group: w.group,
heading: w.heading,
message: w.message,
})
.collect(),
revisions: result
.revisions
.into_iter()
.map(|r| Revision {
group: r.group,
key: r.key,
changed: r.changed,
winner: r.winner_file,
})
.collect(),
})
}
}
impl std::fmt::Debug for Merge<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Merge")
.field(
"sources",
&self
.sources
.iter()
.map(Source::describe)
.collect::<Vec<_>>(),
)
.field("on_type_clash", &self.on_type_clash)
.field("on_missing_tran", &self.on_missing_tran)
.field("edition", &self.edition)
.field("encoding", &self.encoding)
.field("mode", &self.mode)
.field("transmission", &self.tran.is_some())
.finish()
}
}
impl std::fmt::Debug for Merged {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Merged")
.field("bytes", &self.text.len())
.field("notes", &self.notes.len())
.field("revisions", &self.revisions.len())
.finish_non_exhaustive()
}
}