use crate::VERSION;
pub const VERSION_KEY: &str = "powerio_version";
const FROZEN_LINEAGE: (u64, u64) = (0, 9);
#[must_use]
pub fn supports(version: &str) -> bool {
let Some(document) = lineage(version) else {
return false;
};
reads(current_lineage(), document)
}
fn reads(build: (u64, u64), document: (u64, u64)) -> bool {
let ((build_major, build_minor), (major, minor)) = (build, document);
if major == build_major {
return major != 0 || minor == build_minor;
}
build_major == 1 && document == FROZEN_LINEAGE
}
#[must_use]
pub fn reject(document: &str, version: &str) -> String {
let states = if version.is_empty() {
format!("{document} states no `{VERSION_KEY}`, so it was written before powerio 0.9.0")
} else {
format!("{document} states `{VERSION_KEY}` {version}")
};
format!(
"{states}; this build reads {}; regenerate it with powerio {VERSION}",
lineage_label()
)
}
#[must_use]
pub fn lineage_label() -> String {
match current_lineage() {
(0, minor) => format!("0.{minor}.x"),
(1, _) => format!(
"major version 1 and {}.{}.x",
FROZEN_LINEAGE.0, FROZEN_LINEAGE.1
),
(major, _) => format!("major version {major}"),
}
}
#[must_use]
pub fn lineage_path() -> String {
match current_lineage() {
(0, minor) => format!("0.{minor}"),
(major, _) => major.to_string(),
}
}
fn current_lineage() -> (u64, u64) {
lineage(VERSION).expect("the crate version is valid semver")
}
fn lineage(version: &str) -> Option<(u64, u64)> {
let (rest, build) = match version.split_once('+') {
Some((rest, build)) => (rest, Some(build)),
None => (version, None),
};
let (core, pre) = match rest.split_once('-') {
Some((core, pre)) => (core, Some(pre)),
None => (rest, None),
};
if pre.is_some_and(|s| !valid_suffix(s)) || build.is_some_and(|s| !valid_suffix(s)) {
return None;
}
let mut parts = core.split('.');
let major = parts.next()?;
let minor = parts.next()?;
let patch = parts.next()?;
if parts.next().is_some() {
return None;
}
let major = parse_number(major)?;
let minor = parse_number(minor)?;
parse_number(patch)?;
Some((major, minor))
}
fn parse_number(s: &str) -> Option<u64> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) || (s.len() > 1 && s.starts_with('0'))
{
return None;
}
s.parse().ok()
}
fn valid_suffix(s: &str) -> bool {
!s.is_empty()
&& s.split('.').all(|part| {
!part.is_empty() && part.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lineage_parses_semver_suffixes() {
assert_eq!(lineage("1.2.3"), Some((1, 2)));
assert_eq!(lineage("1.0.0-rc.1"), Some((1, 0)));
assert_eq!(lineage("1.0.0+build-x"), Some((1, 0)));
assert_eq!(lineage("0.9.0"), Some((0, 9)));
}
#[test]
fn lineage_rejects_what_is_not_semver() {
for bad in [
"", "1", "1.2", "1.2.3.4", "01.2.3", "1.x.3", "1.2.3-", "1.2.3+",
] {
assert_eq!(lineage(bad), None, "{bad}");
}
}
#[test]
fn this_build_reads_its_own_version() {
assert!(supports(VERSION));
}
#[test]
fn a_zero_x_minor_is_its_own_lineage() {
let (major, minor) = current_lineage();
assert_eq!(major, 0, "update this test at 1.0.0");
assert!(supports(&format!("0.{minor}.0")));
assert!(supports(&format!("0.{minor}.99")));
assert!(!supports(&format!("0.{}.0", minor + 1)));
assert!(!supports(&format!("0.{}.0", minor - 1)));
assert!(!supports("1.0.0"));
}
#[test]
fn one_x_reads_the_lineage_it_froze() {
assert!(reads((1, 0), FROZEN_LINEAGE));
assert!(reads((1, 7), FROZEN_LINEAGE));
assert!(reads((1, 0), (1, 4)), "a 1.x build reads any 1.x document");
}
#[test]
fn the_frozen_lineage_is_never_behind_this_build() {
let (major, minor) = current_lineage();
if major == 0 {
assert!(
(major, minor) <= FROZEN_LINEAGE,
"0.{minor} ships past the frozen lineage 0.{}",
FROZEN_LINEAGE.1
);
}
}
#[test]
fn nothing_else_crosses_a_major_boundary() {
assert!(!reads((1, 0), (0, 8)), "only the frozen lineage crosses");
assert!(!reads((2, 0), FROZEN_LINEAGE), "2.0 froze nothing");
assert!(!reads((0, 9), (1, 0)), "0.9 cannot read the future");
assert!(!reads((0, 9), (0, 8)), "a 0.x minor is its own lineage");
}
#[test]
fn reject_names_the_document_and_both_versions() {
let message = reject(".pio.json", "0.2.1");
assert!(message.contains(".pio.json"), "{message}");
assert!(message.contains("0.2.1"), "{message}");
assert!(message.contains(VERSION), "{message}");
}
}