mod document;
mod report;
use std::path::{Path, PathBuf};
use laterite_ags4_core::ags4_codec::{DuplicateHeadings, ReadOptions, read_ags4_bytes_with};
use laterite_ags4_emit::{EmitMode, EmitOpts, GroupInput, TranStamp, emit_ags4};
use laterite_ags4_reference::dict::DictVersion;
use laterite_ags4_validator::parse::parse_bytes;
use laterite_ags4_validator::{CheckOptions, WorldScope, check_file, check_parsed_with_dict};
pub use document::{Document, Group, Row, Rows};
pub use report::{Finding, Report, Severity};
use crate::{Error, ErrorKind};
#[must_use]
pub fn editions() -> Vec<&'static str> {
DictVersion::ALL.iter().map(|v| v.as_str()).collect()
}
fn resolve_edition(label: &str) -> Result<DictVersion, Error> {
DictVersion::from_edition(label).ok_or_else(|| {
Error::new(
ErrorKind::BadDictionary,
format!(
"unknown AGS4 edition `{label}` — this build has {}",
editions().join(", ")
),
)
})
}
fn bad_encoding(label: &str) -> Error {
Error::new(
ErrorKind::InvalidArgument,
format!("unknown encoding label `{label}` (WHATWG names, e.g. `windows-1252`)"),
)
}
fn convert(findings: laterite_ags4_validator::Findings) -> Vec<Finding> {
findings
.into_iter()
.flat_map(|(rule, group_findings)| {
group_findings.into_iter().map(move |f| Finding {
rule: rule.clone(),
group: f.group,
description: f.desc,
line: f.line,
severity: match f.severity {
laterite_ags4_validator::findings::Severity::Error => Severity::Error,
laterite_ags4_validator::findings::Severity::Warning => Severity::Warning,
laterite_ags4_validator::findings::Severity::Fyi => Severity::Fyi,
},
})
})
.collect()
}
enum Source {
Path(PathBuf),
Bytes(Vec<u8>),
}
pub struct Read {
source: Source,
encoding: Option<String>,
recover_duplicate_headings: bool,
}
pub fn read(path: impl AsRef<Path>) -> Read {
Read {
source: Source::Path(path.as_ref().to_path_buf()),
encoding: None,
recover_duplicate_headings: false,
}
}
pub fn read_bytes(bytes: impl Into<Vec<u8>>) -> Read {
Read {
source: Source::Bytes(bytes.into()),
encoding: None,
recover_duplicate_headings: false,
}
}
impl Read {
#[must_use]
pub fn encoding(mut self, label: impl Into<String>) -> Read {
self.encoding = Some(label.into());
self
}
#[must_use]
pub fn recover_duplicate_headings(mut self, yes: bool) -> Read {
self.recover_duplicate_headings = yes;
self
}
pub fn run(self) -> Result<Document, Error> {
let raw = match &self.source {
Source::Path(p) => std::fs::read(p).map_err(|e| {
Error::with_source(ErrorKind::Io, format!("cannot read {}", p.display()), e)
})?,
Source::Bytes(b) => b.clone(),
};
let bytes = match &self.encoding {
None => raw,
Some(label) => {
let enc = laterite_ags4_parse::resolve_encoding(Some(label))
.ok_or_else(|| bad_encoding(label))?;
enc.decode(&raw).0.into_owned().into_bytes()
}
};
let opts = ReadOptions {
duplicate_headings: if self.recover_duplicate_headings {
DuplicateHeadings::Recover
} else {
DuplicateHeadings::Error
},
};
read_ags4_bytes_with(&bytes, opts)
.map(Document::new)
.map_err(|e| Error::with_source(ErrorKind::NotAgs4, "cannot read as AGS4", e))
}
}
pub struct Validate {
source: Source,
warnings: bool,
fyi: bool,
edition: Option<String>,
encoding: Option<String>,
check_files: bool,
}
pub fn validate(path: impl AsRef<Path>) -> Validate {
Validate {
source: Source::Path(path.as_ref().to_path_buf()),
warnings: false,
fyi: false,
edition: None,
encoding: None,
check_files: false,
}
}
pub fn validate_bytes(bytes: impl Into<Vec<u8>>) -> Validate {
Validate {
source: Source::Bytes(bytes.into()),
warnings: false,
fyi: false,
edition: None,
encoding: None,
check_files: false,
}
}
impl Validate {
#[must_use]
pub fn warnings(mut self, yes: bool) -> Validate {
self.warnings = yes;
self
}
#[must_use]
pub fn fyi(mut self, yes: bool) -> Validate {
self.fyi = yes;
self
}
#[must_use]
pub fn edition(mut self, edition: impl Into<String>) -> Validate {
self.edition = Some(edition.into());
self
}
#[must_use]
pub fn encoding(mut self, label: impl Into<String>) -> Validate {
self.encoding = Some(label.into());
self
}
#[must_use]
pub fn check_files(mut self, yes: bool) -> Validate {
self.check_files = yes;
self
}
pub fn run(self) -> Result<Report, Error> {
let opts = CheckOptions {
dict_version: self.edition.as_deref().map(resolve_edition).transpose()?,
custom_dict: None,
include_warnings: self.warnings,
include_fyi: self.fyi,
check_files: self.check_files,
encoding: laterite_ags4_parse::resolve_encoding(self.encoding.as_deref())
.ok_or_else(|| bad_encoding(self.encoding.as_deref().unwrap_or_default()))?,
};
let result = match &self.source {
Source::Path(p) => check_file(p, &opts),
Source::Bytes(b) => parse_bytes(b, opts.encoding).and_then(|parsed| {
check_parsed_with_dict(&parsed, &opts, &WorldScope::None).map(|(f, _, _)| f)
}),
};
let findings = result.map_err(|e| {
let kind = match e.kind() {
"io" | "not_found" => ErrorKind::Io,
"not_ags4" => ErrorKind::NotAgs4,
"bad_dict" | "unsupported_edition" => ErrorKind::BadDictionary,
"world_check_requires_source" => ErrorKind::InvalidArgument,
_ => ErrorKind::Other,
};
let what = match &self.source {
Source::Path(p) => format!("cannot validate {}", p.display()),
Source::Bytes(b) => format!("cannot validate {} bytes", b.len()),
};
Error::with_source(kind, what, e)
})?;
Ok(Report {
findings: convert(findings),
})
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WriteMode {
#[default]
AutoFix,
Report,
Strict,
}
pub struct Write<'a> {
doc: &'a Document,
mode: WriteMode,
edition: Option<String>,
synthesise_metadata: bool,
tran: Option<TranStamp>,
}
#[must_use]
pub fn write(doc: &Document) -> Write<'_> {
Write {
doc,
mode: WriteMode::default(),
edition: None,
synthesise_metadata: true,
tran: None,
}
}
impl<'a> Write<'a> {
#[must_use]
pub fn mode(mut self, mode: WriteMode) -> Write<'a> {
self.mode = mode;
self
}
#[must_use]
pub fn edition(mut self, edition: impl Into<String>) -> Write<'a> {
self.edition = Some(edition.into());
self
}
#[must_use]
pub fn synthesise_metadata(mut self, yes: bool) -> Write<'a> {
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>,
) -> Write<'a> {
self.tran = Some(TranStamp::new(
issue_number,
date,
producer,
recipient,
status,
));
self
}
fn emit(self) -> Result<Written, Error> {
let groups: Vec<GroupInput> = self
.doc
.groups()
.iter()
.map(|g| {
let headings = g.headings();
GroupInput {
code: g.code().to_string(),
headings: headings.iter().map(|h| (*h).to_string()).collect(),
units: Some(g.units().iter().map(|u| (*u).to_string()).collect()),
types: Some(g.types().iter().map(|t| (*t).to_string()).collect()),
rows: g
.rows()
.map(|r| {
headings
.iter()
.map(|h| serde_json::Value::from(r.cell(h).unwrap_or("")))
.collect()
})
.collect(),
}
})
.collect();
let edition = match &self.edition {
Some(label) => resolve_edition(label)?,
None => laterite_ags4_reference::dict::FALLBACK,
};
let opts = EmitOpts {
mode: match self.mode {
WriteMode::AutoFix => EmitMode::AutoFix,
WriteMode::Report => EmitMode::Report,
WriteMode::Strict => EmitMode::Strict,
},
edition,
tran: self.tran,
synthesise_metadata: self.synthesise_metadata,
};
let result = emit_ags4(&groups, &opts)
.map_err(|e| Error::with_source(ErrorKind::Emit, "cannot write as AGS4", e))?;
Ok(Written {
fixes_applied: result.fixes_applied,
findings: convert(result.findings),
bytes: result.bytes,
})
}
pub fn to_bytes(self) -> Result<Written, Error> {
self.emit()
}
pub fn to_path(self, path: impl AsRef<Path>) -> Result<Written, Error> {
let path = path.as_ref();
let written = self.emit()?;
std::fs::write(path, &written.bytes).map_err(|e| {
Error::with_source(ErrorKind::Io, format!("cannot write {}", path.display()), e)
})?;
Ok(written)
}
}
pub struct Written {
bytes: Vec<u8>,
findings: Vec<Finding>,
fixes_applied: usize,
}
impl Written {
#[must_use]
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn into_bytes(self) -> Vec<u8> {
self.bytes
}
#[must_use]
pub fn findings(&self) -> &[Finding] {
&self.findings
}
#[must_use]
pub fn fixes_applied(&self) -> usize {
self.fixes_applied
}
}
impl std::fmt::Debug for Read {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Read")
.field(
"source",
&match &self.source {
Source::Path(p) => format!("path {}", p.display()),
Source::Bytes(b) => format!("{} bytes", b.len()),
},
)
.field("encoding", &self.encoding)
.field(
"recover_duplicate_headings",
&self.recover_duplicate_headings,
)
.finish()
}
}
impl std::fmt::Debug for Validate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Validate")
.field(
"source",
&match &self.source {
Source::Path(p) => format!("path {}", p.display()),
Source::Bytes(b) => format!("{} bytes", b.len()),
},
)
.field("warnings", &self.warnings)
.field("fyi", &self.fyi)
.field("edition", &self.edition)
.field("encoding", &self.encoding)
.field("check_files", &self.check_files)
.finish()
}
}
impl std::fmt::Debug for Write<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Write")
.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 Written {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Written")
.field("bytes", &self.bytes.len())
.field("findings", &self.findings.len())
.field("fixes_applied", &self.fixes_applied)
.finish()
}
}