mod allow;
mod compare;
mod report;
mod snapshot;
pub use allow::{Allowance, Allowlist, AllowlistError};
pub use compare::diff;
pub use report::{Added, Basis, Reason, Removed, Renamed, Report, Unused};
pub use snapshot::{Input, Side, Snapshot};
use std::{fmt, path::Path};
use tilewright_source::Diagnostic;
pub const FORMAT: &str = "tilewright.diff/1";
pub const ALLOWLIST_FORMAT: &str = "tilewright.diff-allow/1";
pub const DEFAULT_MAXIMUM_IDS: usize = 1_000_000;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Limits {
pub maximum_ids: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
maximum_ids: DEFAULT_MAXIMUM_IDS,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Kind {
Anchor,
Definition,
Object,
Plane,
RenderLayer,
Stairwell,
Zone,
}
impl Kind {
pub const ALL: [Self; 7] = [
Self::Anchor,
Self::Definition,
Self::Object,
Self::Plane,
Self::RenderLayer,
Self::Stairwell,
Self::Zone,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Anchor => "anchor",
Self::Definition => "definition",
Self::Object => "object",
Self::Plane => "plane",
Self::RenderLayer => "render_layer",
Self::Stairwell => "stairwell",
Self::Zone => "zone",
}
}
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
Self::ALL.into_iter().find(|kind| kind.as_str() == text)
}
#[must_use]
pub const fn pairs_by_content(self) -> bool {
!matches!(self, Self::Object)
}
#[must_use]
pub const fn in_plane(self) -> bool {
matches!(self, Self::Anchor | Self::Stairwell | Self::Zone)
}
#[must_use]
pub fn is_id(self, id: &str) -> bool {
if self.in_plane() || self == Self::Definition {
id.split_once('/')
.is_some_and(|(scope, local)| is_identifier(scope) && is_identifier(local))
} else {
is_identifier(id)
}
}
pub(crate) fn scope(self, id: &str) -> &str {
if self.in_plane() || self == Self::Definition {
id.split_once('/').map_or("", |(scope, _)| scope)
} else {
""
}
}
}
impl fmt::Display for Kind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
pub(crate) fn is_identifier(text: &str) -> bool {
let bytes = text.as_bytes();
(1..=128).contains(&bytes.len())
&& (bytes[0].is_ascii_lowercase() || bytes[0].is_ascii_digit())
&& bytes
.iter()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"_.-".contains(byte))
}
#[derive(Debug)]
pub enum Error {
Source(Vec<Diagnostic>),
Pack(tilewright_pack::Error),
TooManyIds(usize),
Invalid(String),
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Source(diagnostics) => {
write!(formatter, "{} problem(s) in the source", diagnostics.len())
}
Self::Pack(error) => error.fmt(formatter),
Self::TooManyIds(maximum) => {
write!(formatter, "more than {maximum} IDs, the ID limit")
}
Self::Invalid(message) => formatter.write_str(message),
}
}
}
impl std::error::Error for Error {}
pub fn load(path: &Path, limits: &Limits) -> Result<Snapshot, Error> {
if std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file()) {
let pack = tilewright_pack::Pack::open(path).map_err(Error::Pack)?;
Snapshot::from_pack(&pack, limits)
} else {
let world = tilewright_source::load(path).map_err(Error::Source)?;
Snapshot::from_world(&world, limits)
}
}
#[cfg(test)]
mod tests;