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;
pub use session::EnabledEditions;
use vortex_session::registry::Id;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
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, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ComponentKind {
Array,
Layout,
DType,
Aggregate,
}
impl Display for ComponentKind {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Array => "array",
Self::Layout => "layout",
Self::DType => "dtype",
Self::Aggregate => "aggregate",
})
}
}
#[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 kind: ComponentKind,
pub component_id: Id,
pub since: EditionId,
pub required_vortex_release: Option<&'static str>,
}
pub trait AsComponentId: Debug + Send + Sync {
fn component_id(&self) -> Id;
}
impl AsComponentId for str {
#[expect(
clippy::disallowed_methods,
reason = "interning a dynamic component id at declaration time"
)]
fn component_id(&self) -> Id {
Id::new(self)
}
}
impl AsComponentId for Id {
fn component_id(&self) -> Id {
*self
}
}
impl AsComponentId for &'static str {
fn component_id(&self) -> Id {
(**self).component_id()
}
}
#[derive(Clone, Copy, Debug)]
pub struct EditionMember {
pub kind: ComponentKind,
pub component: &'static dyn AsComponentId,
}
impl EditionMember {
pub const fn array(component: &'static dyn AsComponentId) -> Self {
Self {
kind: ComponentKind::Array,
component,
}
}
pub const fn layout(component: &'static dyn AsComponentId) -> Self {
Self {
kind: ComponentKind::Layout,
component,
}
}
pub const fn dtype(component: &'static dyn AsComponentId) -> Self {
Self {
kind: ComponentKind::DType,
component,
}
}
pub const fn aggregate(component: &'static dyn AsComponentId) -> Self {
Self {
kind: ComponentKind::Aggregate,
component,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct EditionDeclaration {
pub edition: Edition,
pub added: &'static [EditionMember],
}
impl EditionInclusion {
pub fn new<C: AsComponentId + ?Sized>(
kind: ComponentKind,
component: &C,
since: EditionId,
) -> Self {
Self {
kind,
component_id: component.component_id(),
since,
required_vortex_release: None,
}
}
pub fn array<C: AsComponentId + ?Sized>(encoding: &C, since: EditionId) -> Self {
Self::new(ComponentKind::Array, encoding, since)
}
pub fn dtype<C: AsComponentId + ?Sized>(dtype: &C, since: EditionId) -> Self {
Self::new(ComponentKind::DType, dtype, since)
}
pub fn validate(&self) -> Result<(), EditionError> {
let id = self.component_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 {} id {id:?}: expected lowercase `namespace.name`, e.g. `vortex.alp`",
self.kind
)));
}
if let Some(release) = self.required_vortex_release
&& parse_release(release).is_none()
{
return Err(EditionError::new(format!(
"{} {id} declares malformed required_vortex_release {release:?}",
self.kind
)));
}
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 {}