use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Kind {
Pair,
Consumer,
Donor,
}
impl Kind {
pub fn as_str(self) -> &'static str {
match self {
Kind::Pair => "pair",
Kind::Consumer => "consumer",
Kind::Donor => "donor",
}
}
pub fn parse(text: &str) -> Option<Self> {
match text {
"pair" => Some(Kind::Pair),
"consumer" => Some(Kind::Consumer),
"donor" => Some(Kind::Donor),
_ => None,
}
}
pub fn symmetric(self) -> bool {
matches!(self, Kind::Pair)
}
pub fn flipped(self) -> Self {
match self {
Kind::Pair => Kind::Pair,
Kind::Consumer => Kind::Donor,
Kind::Donor => Kind::Consumer,
}
}
}
impl std::fmt::Display for Kind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.pad(self.as_str())
}
}
#[derive(Debug, Clone, Serialize)]
pub struct End {
pub project: String,
pub version: Option<String>,
pub shipped: Option<bool>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Link {
pub id: i64,
pub kind: Kind,
pub near: End,
pub far: End,
pub note: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Drift {
ShippedAlone,
RunAhead,
}
impl Drift {
fn rank(self) -> u8 {
match self {
Drift::ShippedAlone => 0,
Drift::RunAhead => 1,
}
}
}
impl Ord for Drift {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.rank().cmp(&other.rank())
}
}
impl PartialOrd for Drift {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Parted {
pub drift: Drift,
pub ahead: End,
pub behind: End,
pub versions: Option<u32>,
pub note: Option<String>,
}
pub const AHEAD_VERSIONS: u32 = 2;
#[derive(Debug, Clone)]
pub struct Pair {
pub link: Link,
pub near_ahead: u32,
pub far_ahead: u32,
}
pub fn parted(pairs: &[Pair]) -> Vec<Parted> {
let mut out = Vec::new();
for pair in pairs {
if pair.link.kind != Kind::Pair {
continue;
}
let near = &pair.link.near;
let far = &pair.link.far;
match (near.shipped, far.shipped) {
(Some(true), Some(false)) => out.push(Parted {
drift: Drift::ShippedAlone,
ahead: near.clone(),
behind: far.clone(),
versions: None,
note: pair.link.note.clone(),
}),
(Some(false), Some(true)) => out.push(Parted {
drift: Drift::ShippedAlone,
ahead: far.clone(),
behind: near.clone(),
versions: None,
note: pair.link.note.clone(),
}),
(Some(false), Some(false)) => {
let (ahead, behind, by) = if pair.near_ahead >= pair.far_ahead {
(near, far, pair.near_ahead - pair.far_ahead)
} else {
(far, near, pair.far_ahead - pair.near_ahead)
};
if by >= AHEAD_VERSIONS {
out.push(Parted {
drift: Drift::RunAhead,
ahead: ahead.clone(),
behind: behind.clone(),
versions: Some(by),
note: pair.link.note.clone(),
});
}
}
_ => {}
}
}
out.sort_by(|a, b| {
a.drift
.cmp(&b.drift)
.then_with(|| b.versions.cmp(&a.versions))
.then_with(|| a.ahead.project.cmp(&b.ahead.project))
});
out
}
#[cfg(test)]
mod tests {
use super::*;
fn end(project: &str, version: &str, shipped: bool) -> End {
End {
project: project.into(),
version: Some(version.into()),
shipped: Some(shipped),
}
}
fn bare(project: &str) -> End {
End {
project: project.into(),
version: None,
shipped: None,
}
}
fn pair(near: End, far: End, near_ahead: u32, far_ahead: u32) -> Pair {
Pair {
link: Link {
id: 1,
kind: Kind::Pair,
near,
far,
note: None,
},
near_ahead,
far_ahead,
}
}
#[test]
fn a_half_that_shipped_without_the_other_is_the_loudest_signal() {
let drifted = parted(&[pair(end("kasl", "v1.12.0", true), end("kasl-server", "v0.22.0", false), 0, 0)]);
assert_eq!(drifted.len(), 1);
assert_eq!(drifted[0].drift, Drift::ShippedAlone);
assert_eq!(drifted[0].ahead.project, "kasl");
assert_eq!(drifted[0].behind.project, "kasl-server");
}
#[test]
fn the_side_a_pair_was_recorded_from_does_not_change_what_it_says() {
let drifted = parted(&[pair(end("kasl-server", "v0.22.0", false), end("kasl", "v1.12.0", true), 0, 0)]);
assert_eq!(drifted[0].ahead.project, "kasl");
assert_eq!(drifted[0].behind.project, "kasl-server");
}
#[test]
fn a_pair_that_shipped_both_halves_is_the_pair_working() {
assert!(parted(&[pair(end("kasl", "v1.12.0", true), end("kasl-server", "v0.22.0", true), 0, 0)]).is_empty());
}
#[test]
fn one_version_ahead_is_not_yet_drift() {
assert!(parted(&[pair(end("a", "v0.3.0", false), end("b", "v0.1.0", false), 1, 0)]).is_empty());
}
#[test]
fn two_versions_ahead_is_reported_with_the_number() {
let drifted = parted(&[pair(end("a", "v0.3.0", false), end("b", "v0.1.0", false), 2, 0)]);
assert_eq!(drifted.len(), 1);
assert_eq!(drifted[0].drift, Drift::RunAhead);
assert_eq!(drifted[0].ahead.project, "a");
assert_eq!(drifted[0].versions, Some(2));
}
#[test]
fn the_far_end_can_be_the_one_that_ran_ahead() {
let drifted = parted(&[pair(end("a", "v0.1.0", false), end("b", "v0.4.0", false), 0, 3)]);
assert_eq!(drifted[0].ahead.project, "b");
assert_eq!(drifted[0].versions, Some(3));
}
#[test]
fn a_consumer_running_ahead_is_not_reported() {
let mut p = pair(end("lyrid", "v0.12.0", false), end("dowel", "v0.26.0", false), 5, 0);
p.link.kind = Kind::Consumer;
assert!(parted(&[p]).is_empty());
let mut p = pair(end("lyrid", "v0.12.0", true), end("dowel", "v0.26.0", false), 0, 0);
p.link.kind = Kind::Consumer;
assert!(parted(&[p]).is_empty());
}
#[test]
fn a_link_without_versions_says_nothing_about_drift() {
assert!(parted(&[pair(bare("a"), bare("b"), 0, 0)]).is_empty());
}
#[test]
fn one_anchored_end_is_not_a_comparison() {
assert!(parted(&[pair(end("a", "v0.3.0", true), bare("b"), 0, 0)]).is_empty());
}
#[test]
fn the_shipped_half_outranks_the_widest_gap() {
let drifted = parted(&[
pair(end("a", "v0.9.0", false), end("b", "v0.1.0", false), 8, 0),
pair(end("c", "v1.0.0", true), end("d", "v0.1.0", false), 0, 0),
]);
assert_eq!(drifted[0].ahead.project, "c");
assert_eq!(drifted[1].ahead.project, "a");
}
#[test]
fn a_kind_pads_to_the_width_it_is_given() {
assert_eq!(format!("{:<9}|", Kind::Pair), "pair |");
assert_eq!(format!("{:<9}|", Kind::Consumer), "consumer |");
}
#[test]
fn a_directed_kind_reads_as_its_opposite_from_the_far_end() {
assert_eq!(Kind::Consumer.flipped(), Kind::Donor);
assert_eq!(Kind::Donor.flipped(), Kind::Consumer);
assert_eq!(Kind::Pair.flipped(), Kind::Pair);
assert!(Kind::Pair.symmetric());
assert!(!Kind::Consumer.symmetric());
}
}