use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Verdict {
Greenfield,
Brownfield,
NeedsDecision,
}
impl Verdict {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Greenfield => "greenfield",
Self::Brownfield => "brownfield",
Self::NeedsDecision => "needs-decision",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RepositoryFacts {
pub release_markers: Vec<String>,
pub collisions: Vec<String>,
pub tags: usize,
pub long_lived_branches: Vec<String>,
}
#[must_use]
pub fn verdict(facts: &RepositoryFacts) -> Verdict {
if !facts.release_markers.is_empty() || !facts.collisions.is_empty() {
return Verdict::Brownfield;
}
if facts.tags > 0 || !facts.long_lived_branches.is_empty() {
return Verdict::NeedsDecision;
}
Verdict::Greenfield
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Classification {
Setup,
Migration,
Upgrade,
Drift,
Invalid,
}
impl Classification {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Setup => "setup",
Self::Migration => "migration",
Self::Upgrade => "upgrade",
Self::Drift => "drift",
Self::Invalid => "invalid",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Finding {
pub code: &'static str,
pub detail: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecordState {
Absent,
Present,
Invalid,
}
#[must_use]
pub const fn classify(record: RecordState, verdict: Verdict, owned_drift: bool) -> Classification {
match record {
RecordState::Invalid => Classification::Invalid,
RecordState::Absent => match verdict {
Verdict::Greenfield => Classification::Setup,
Verdict::Brownfield | Verdict::NeedsDecision => Classification::Migration,
},
RecordState::Present => {
if owned_drift {
Classification::Drift
} else {
Classification::Upgrade
}
}
}
}
#[cfg(test)]
mod tests {
use super::{Classification, RecordState, RepositoryFacts, Verdict, classify, verdict};
#[test]
fn nothing_is_greenfield() {
assert_eq!(verdict(&RepositoryFacts::default()), Verdict::Greenfield);
}
#[test]
fn a_release_marker_or_a_collision_is_brownfield() {
let with_marker = RepositoryFacts {
release_markers: vec!["CHANGELOG.md".into()],
..RepositoryFacts::default()
};
assert_eq!(verdict(&with_marker), Verdict::Brownfield);
let with_collision = RepositoryFacts {
collisions: vec!["release-plz.toml".into()],
..RepositoryFacts::default()
};
assert_eq!(verdict(&with_collision), Verdict::Brownfield);
}
#[test]
fn a_mechanism_beside_activity_is_still_brownfield() {
let both = RepositoryFacts {
release_markers: vec!["CHANGELOG.md".into()],
tags: 7,
long_lived_branches: vec!["develop".into()],
..RepositoryFacts::default()
};
assert_eq!(verdict(&both), Verdict::Brownfield);
}
#[test]
fn activity_with_no_mechanism_needs_a_decision() {
let tagged = RepositoryFacts {
tags: 1,
..RepositoryFacts::default()
};
assert_eq!(verdict(&tagged), Verdict::NeedsDecision);
let branched = RepositoryFacts {
long_lived_branches: vec!["develop".into()],
..RepositoryFacts::default()
};
assert_eq!(verdict(&branched), Verdict::NeedsDecision);
}
#[test]
fn the_classification_table_covers_the_six_states() {
assert_eq!(
classify(RecordState::Absent, Verdict::Greenfield, false),
Classification::Setup
);
assert_eq!(
classify(RecordState::Absent, Verdict::Brownfield, false),
Classification::Migration
);
assert_eq!(
classify(RecordState::Absent, Verdict::NeedsDecision, false),
Classification::Migration
);
assert_eq!(
classify(RecordState::Present, Verdict::Brownfield, false),
Classification::Upgrade
);
assert_eq!(
classify(RecordState::Present, Verdict::Brownfield, true),
Classification::Drift
);
assert_eq!(
classify(RecordState::Invalid, Verdict::Brownfield, true),
Classification::Invalid
);
}
#[test]
fn the_words_are_the_wire_form() {
for (classification, word) in [
(Classification::Setup, "setup"),
(Classification::Migration, "migration"),
(Classification::Upgrade, "upgrade"),
(Classification::Drift, "drift"),
(Classification::Invalid, "invalid"),
] {
assert_eq!(classification.as_str(), word);
assert_eq!(
serde_json::to_string(&classification).expect("serializes"),
format!("\"{word}\"")
);
}
assert_eq!(Verdict::Greenfield.as_str(), "greenfield");
assert_eq!(Verdict::Brownfield.as_str(), "brownfield");
assert_eq!(Verdict::NeedsDecision.as_str(), "needs-decision");
}
}