#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum AuditFindingAttribution {
Introduced,
Worsened,
Inherited,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum AuditGateExemption {
TestFile,
}
impl AuditGateExemption {
#[must_use]
pub const fn token(self) -> &'static str {
match self {
Self::TestFile => "test-file",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum WorsenedMetricKind {
Cyclomatic,
Cognitive,
Crap,
LineCount,
}
impl WorsenedMetricKind {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Cyclomatic => "cyclomatic",
Self::Cognitive => "cognitive",
Self::Crap => "CRAP",
Self::LineCount => "lines",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WorsenedMetric {
pub metric: WorsenedMetricKind,
#[serde(serialize_with = "serialize_metric_value")]
pub base: f64,
#[serde(serialize_with = "serialize_metric_value")]
pub head: f64,
}
impl WorsenedMetric {
#[must_use]
pub fn describe(&self) -> String {
format!(
"{} {} -> {}",
self.metric.label(),
format_metric_value(self.base),
format_metric_value(self.head)
)
}
}
#[must_use]
pub fn describe_worsened_metrics(metrics: &[WorsenedMetric]) -> String {
metrics
.iter()
.map(WorsenedMetric::describe)
.collect::<Vec<_>>()
.join(", ")
}
const MAX_EXACT_WHOLE: f64 = 9_007_199_254_740_992.0;
fn is_whole(value: f64) -> bool {
value.fract() == 0.0 && value.abs() < MAX_EXACT_WHOLE
}
#[must_use]
pub fn format_metric_value(value: f64) -> String {
if is_whole(value) {
format!("{value:.0}")
} else {
format!("{value:.1}")
}
}
#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "serde serialize_with passes the field by reference"
)]
fn serialize_metric_value<S: serde::Serializer>(
value: &f64,
serializer: S,
) -> Result<S::Ok, S::Error> {
if is_whole(*value) {
#[expect(
clippy::cast_possible_truncation,
reason = "is_whole keeps the value whole and inside the exact i64 range"
)]
serializer.serialize_i64(*value as i64)
} else {
serializer.serialize_f64(*value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn whole_values_serialize_as_integers() {
let metric = WorsenedMetric {
metric: WorsenedMetricKind::LineCount,
base: 80.0,
head: 300.0,
};
let json = serde_json::to_value(metric).expect("serialize");
assert_eq!(
json,
serde_json::json!({"metric": "line_count", "base": 80, "head": 300})
);
}
#[test]
fn crap_values_keep_their_fraction() {
let metric = WorsenedMetric {
metric: WorsenedMetricKind::Crap,
base: 31.5,
head: 42.25,
};
let json = serde_json::to_value(metric).expect("serialize");
assert_eq!(json["base"], 31.5);
assert_eq!(metric.describe(), "CRAP 31.5 -> 42.2");
}
#[test]
fn describe_joins_metrics() {
let metrics = [
WorsenedMetric {
metric: WorsenedMetricKind::Cyclomatic,
base: 15.0,
head: 40.0,
},
WorsenedMetric {
metric: WorsenedMetricKind::LineCount,
base: 80.0,
head: 300.0,
},
];
assert_eq!(
describe_worsened_metrics(&metrics),
"cyclomatic 15 -> 40, lines 80 -> 300"
);
}
}