use std::path::{Path, PathBuf};
use laterite_ags4_emit::GroupInput;
use super::{Document, Finding, WriteMode, Written, emit_groups, resolve_edition};
use crate::{Error, ErrorKind};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum Cell {
Null,
Text(String),
Int(i64),
Float(f64),
Bool(bool),
}
impl From<&str> for Cell {
fn from(s: &str) -> Cell {
Cell::Text(s.to_string())
}
}
impl From<String> for Cell {
fn from(s: String) -> Cell {
Cell::Text(s)
}
}
impl From<&String> for Cell {
fn from(s: &String) -> Cell {
Cell::Text(s.clone())
}
}
impl From<i64> for Cell {
fn from(v: i64) -> Cell {
Cell::Int(v)
}
}
impl From<i32> for Cell {
fn from(v: i32) -> Cell {
Cell::Int(i64::from(v))
}
}
impl From<f64> for Cell {
fn from(v: f64) -> Cell {
Cell::Float(v)
}
}
impl From<bool> for Cell {
fn from(v: bool) -> Cell {
Cell::Bool(v)
}
}
impl<T: Into<Cell>> From<Option<T>> for Cell {
fn from(v: Option<T>) -> Cell {
v.map_or(Cell::Null, Into::into)
}
}
impl Cell {
fn into_engine(self) -> laterite_ags4_emit::Cell {
match self {
Cell::Null => laterite_ags4_emit::Cell::Null,
Cell::Text(s) => laterite_ags4_emit::Cell::Text(s),
Cell::Int(v) => laterite_ags4_emit::Cell::Int(v),
Cell::Float(v) => laterite_ags4_emit::Cell::from(v),
Cell::Bool(v) => laterite_ags4_emit::Cell::Bool(v),
}
}
}
#[derive(Debug, Clone)]
pub struct GroupData {
code: String,
headings: Vec<String>,
units: Option<Vec<String>>,
types: Option<Vec<String>>,
rows: Vec<Vec<Cell>>,
}
impl GroupData {
pub fn new<I, S>(code: impl Into<String>, headings: I) -> GroupData
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
GroupData {
code: code.into(),
headings: headings.into_iter().map(Into::into).collect(),
units: None,
types: None,
rows: Vec::new(),
}
}
#[must_use]
pub fn units<I, S>(mut self, units: I) -> GroupData
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.units = Some(units.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn types<I, S>(mut self, types: I) -> GroupData
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.types = Some(types.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn row<I, C>(mut self, cells: I) -> GroupData
where
I: IntoIterator<Item = C>,
C: Into<Cell>,
{
self.rows.push(cells.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn code(&self) -> &str {
&self.code
}
#[must_use]
pub fn len(&self) -> usize {
self.rows.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
fn into_engine(self) -> Result<GroupInput, Error> {
let width = self.headings.len();
for (i, row) in self.rows.iter().enumerate() {
if row.len() != width {
return Err(Error::new(
ErrorKind::InvalidArgument,
format!(
"{}: row {i} has {} cells but the group declares {width} headings",
self.code,
row.len()
),
));
}
}
for (what, meta) in [
("units", self.units.as_ref()),
("types", self.types.as_ref()),
] {
if let Some(m) = meta {
if m.len() != width {
return Err(Error::new(
ErrorKind::InvalidArgument,
format!(
"{}: {what} has {} entries but the group declares {width} headings",
self.code,
m.len()
),
));
}
}
}
Ok(GroupInput {
code: self.code,
headings: self.headings,
units: self.units,
types: self.types,
rows: self
.rows
.into_iter()
.map(|r| r.into_iter().map(Cell::into_engine).collect())
.collect(),
})
}
}
pub struct Build {
groups: Vec<GroupData>,
mode: WriteMode,
edition: Option<String>,
synthesise_metadata: bool,
tran: Option<laterite_ags4_emit::TranStamp>,
}
#[must_use]
pub fn build(groups: Vec<GroupData>) -> Build {
Build {
groups,
mode: WriteMode::default(),
edition: None,
synthesise_metadata: true,
tran: None,
}
}
#[must_use]
pub fn build_document(doc: &Document) -> Build {
build(document_groups(doc))
}
fn document_groups(doc: &Document) -> Vec<GroupData> {
doc.groups()
.iter()
.map(|g| {
let headings = g.headings();
let mut data = GroupData::new(g.code(), headings.iter().copied())
.units(g.units().iter().copied())
.types(g.types().iter().copied());
for row in g.rows() {
data = data.row(
headings
.iter()
.map(|h| Cell::Text(row.cell(h).unwrap_or("").to_string())),
);
}
data
})
.collect()
}
impl Build {
#[must_use]
pub fn mode(mut self, mode: WriteMode) -> Build {
self.mode = mode;
self
}
#[must_use]
pub fn edition(mut self, edition: impl Into<String>) -> Build {
self.edition = Some(edition.into());
self
}
#[must_use]
pub fn synthesise_metadata(mut self, yes: bool) -> Build {
self.synthesise_metadata = yes;
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>,
) -> Build {
self.tran = Some(laterite_ags4_emit::TranStamp::new(
issue_number,
date,
producer,
recipient,
status,
));
self
}
pub fn run(self) -> Result<Written, Error> {
let groups = self
.groups
.into_iter()
.map(GroupData::into_engine)
.collect::<Result<Vec<_>, _>>()?;
emit_groups(
&groups,
self.mode,
self.edition.as_deref(),
self.synthesise_metadata,
self.tran,
)
}
pub fn to_path(self, path: impl AsRef<Path>) -> Result<BuildSaved, Error> {
let dest = path.as_ref();
let written = self.run()?;
staged_write(dest, written.bytes())?;
Ok(BuildSaved {
path: dest.to_path_buf(),
findings: written.findings,
fixes_applied: written.fixes_applied,
})
}
}
pub struct BuildSaved {
path: PathBuf,
findings: Vec<Finding>,
fixes_applied: usize,
}
impl BuildSaved {
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn findings(&self) -> &[Finding] {
&self.findings
}
#[must_use]
pub fn fixes_applied(&self) -> usize {
self.fixes_applied
}
}
fn staged_write(dest: &Path, bytes: &[u8]) -> Result<(), Error> {
let io_err = |e: std::io::Error| {
Error::with_source(ErrorKind::Io, format!("cannot write {}", dest.display()), e)
};
let dir = staging_dir(dest);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.subsec_nanos());
let tmp = dir.join(format!(
".laterite-build-{}-{nanos}.tmp",
std::process::id()
));
std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)
.and_then(|mut f| std::io::Write::write_all(&mut f, bytes))
.and_then(|()| std::fs::rename(&tmp, dest))
.map_err(|e| {
let _ = std::fs::remove_file(&tmp);
io_err(e)
})
}
fn staging_dir(dest: &Path) -> &Path {
match dest.parent() {
Some(p) if !p.as_os_str().is_empty() => p,
_ => Path::new("."),
}
}
pub struct BuildUnchecked {
groups: Vec<GroupData>,
edition: Option<String>,
}
#[must_use]
pub fn build_unchecked(groups: Vec<GroupData>) -> BuildUnchecked {
BuildUnchecked {
groups,
edition: None,
}
}
#[must_use]
pub fn build_unchecked_document(doc: &Document) -> BuildUnchecked {
build_unchecked(document_groups(doc))
}
impl BuildUnchecked {
#[must_use]
pub fn edition(mut self, edition: impl Into<String>) -> BuildUnchecked {
self.edition = Some(edition.into());
self
}
pub fn run(self) -> Result<Vec<u8>, Error> {
let edition = match self.edition.as_deref() {
Some(label) => resolve_edition(label)?,
None => laterite_ags4_reference::dict::FALLBACK,
};
let groups = self
.groups
.into_iter()
.map(GroupData::into_engine)
.collect::<Result<Vec<_>, _>>()?;
laterite_ags4_emit::emit_ags4_unchecked(groups, edition)
.map_err(|e| Error::with_source(ErrorKind::Emit, "cannot write as AGS4", e))
}
pub fn to_path(self, path: impl AsRef<Path>) -> Result<PathBuf, Error> {
let dest = path.as_ref().to_path_buf();
let bytes = self.run()?;
staged_write(&dest, &bytes)?;
Ok(dest)
}
}
impl std::fmt::Debug for Build {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Build")
.field(
"groups",
&self
.groups
.iter()
.map(|g| format!("{} x{}", g.code, g.rows.len()))
.collect::<Vec<_>>(),
)
.field("mode", &self.mode)
.field("edition", &self.edition)
.field("synthesise_metadata", &self.synthesise_metadata)
.field("transmission", &self.tran.is_some())
.finish()
}
}
impl std::fmt::Debug for BuildUnchecked {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BuildUnchecked")
.field(
"groups",
&self
.groups
.iter()
.map(|g| format!("{} x{}", g.code, g.rows.len()))
.collect::<Vec<_>>(),
)
.field("edition", &self.edition)
.finish()
}
}
impl std::fmt::Debug for BuildSaved {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BuildSaved")
.field("path", &self.path)
.field("findings", &self.findings.len())
.field("fixes_applied", &self.fixes_applied)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::staging_dir;
use std::path::Path;
#[test]
fn the_staging_dir_is_the_destinations_own() {
assert_eq!(
staging_dir(Path::new("/a/b/out.ags")),
Path::new("/a/b"),
"staging anywhere else forfeits rename atomicity"
);
assert_eq!(staging_dir(Path::new("out.ags")), Path::new("."));
assert_eq!(staging_dir(Path::new("./out.ags")), Path::new("."));
}
}