mod session;
pub mod test_harness;
#[cfg(test)]
mod tests;
use std::error::Error;
use std::fmt;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
pub use session::EditionSession;
pub use session::EditionSessionExt;
use vortex_session::registry::Id;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EditionId {
pub family: &'static str,
pub year: u16,
pub month: u8,
pub version: u8,
}
impl EditionId {
pub const fn new(family: &'static str, year: u16, month: u8, version: u8) -> Self {
Self {
family,
year,
month,
version,
}
}
pub fn is_at_or_before(&self, other: &EditionId) -> bool {
self.family == other.family
&& (self.year, self.month, self.version) <= (other.year, other.month, other.version)
}
pub fn validate(&self) -> Result<(), EditionError> {
if self.family.is_empty() || !self.family.chars().all(|c| c.is_ascii_lowercase()) {
return Err(EditionError::new(format!(
"edition {self} must have a non-empty lowercase family, e.g. `core`"
)));
}
if !(1000..=9999).contains(&self.year) {
return Err(EditionError::new(format!(
"edition {self} must have a four-digit year"
)));
}
if !(1..=12).contains(&self.month) {
return Err(EditionError::new(format!(
"edition {self} must have a month in 01-12"
)));
}
Ok(())
}
}
impl Display for EditionId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"{}{}.{:02}.{}",
self.family, self.year, self.month, self.version
)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Edition {
pub id: EditionId,
pub min_vortex_version: Option<&'static str>,
}
impl Edition {
pub fn is_draft(&self) -> bool {
self.min_vortex_version.is_none()
}
}
#[derive(Clone, Copy, Debug)]
pub struct EditionInclusion {
pub encoding_id: Id,
pub since: EditionId,
pub required_vortex_release: Option<&'static str>,
}
pub trait AsEncodingId: Debug + Send + Sync {
fn encoding_id(&self) -> Id;
}
impl AsEncodingId for str {
#[expect(
clippy::disallowed_methods,
reason = "interning a dynamic encoding id at declaration time"
)]
fn encoding_id(&self) -> Id {
Id::new(self)
}
}
impl AsEncodingId for Id {
fn encoding_id(&self) -> Id {
*self
}
}
impl AsEncodingId for &'static str {
fn encoding_id(&self) -> Id {
(**self).encoding_id()
}
}
#[derive(Clone, Copy, Debug)]
pub struct EditionDeclaration {
pub edition: Edition,
pub added: &'static [&'static dyn AsEncodingId],
}
impl EditionInclusion {
pub fn new<E: AsEncodingId + ?Sized>(encoding: &E, since: EditionId) -> Self {
Self {
encoding_id: encoding.encoding_id(),
since,
required_vortex_release: None,
}
}
pub fn validate(&self) -> Result<(), EditionError> {
let id = self.encoding_id.as_str();
let well_formed = !id.starts_with('.')
&& !id.ends_with('.')
&& id.contains('.')
&& id
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || "._-".contains(c));
if !well_formed {
return Err(EditionError::new(format!(
"invalid encoding id {id:?}: expected lowercase `namespace.name`, e.g. \
`vortex.alp`"
)));
}
if let Some(release) = self.required_vortex_release
&& parse_release(release).is_none()
{
return Err(EditionError::new(format!(
"encoding {id} declares malformed required_vortex_release {release:?}"
)));
}
Ok(())
}
}
pub(crate) fn parse_release(release: &str) -> Option<Vec<u64>> {
let parts: Vec<u64> = release
.split('.')
.map(|part| part.parse::<u64>().ok())
.collect::<Option<_>>()?;
(parts.len() == 3).then_some(parts)
}
#[derive(Debug)]
pub struct EditionError(String);
impl EditionError {
pub fn new(msg: impl Into<String>) -> Self {
Self(msg.into())
}
}
impl Display for EditionError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl Error for EditionError {}