use serde::{Deserialize, Serialize};
use tatara_lisp_derive::TataraDomain as DeriveTataraDomain;
use crate::derivation::Derivation;
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
Default,
tatara_lisp::DeriveClosedSet,
)]
#[closed_set(via = "as_str", display, generate_unknown)]
pub enum OverlayTarget {
#[default]
PackageSet,
PerSystem,
Module,
}
impl OverlayTarget {
pub const ALL: [Self; 3] = [Self::PackageSet, Self::PerSystem, Self::Module];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::PackageSet => "PackageSet",
Self::PerSystem => "PerSystem",
Self::Module => "Module",
}
}
}
#[derive(DeriveTataraDomain, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[tatara(keyword = "defoverlay")]
pub struct Overlay {
pub name: String,
#[serde(default)]
pub target: OverlayTarget,
#[serde(default)]
pub adds: Vec<Derivation>,
#[serde(default)]
pub replaces: Vec<Replacement>,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Replacement {
pub upstream_name: String,
pub with: Derivation,
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
use tatara_lisp::{domain::TataraDomain, read, ClosedSet};
#[test]
fn minimal_overlay_compiles() {
let forms = read(
r#"(defoverlay
:name "patched"
:target PackageSet
:description "carries a local patch")"#,
)
.unwrap();
let o = Overlay::compile_from_sexp(&forms[0]).unwrap();
assert_eq!(o.name, "patched");
assert_eq!(o.target, OverlayTarget::PackageSet);
assert!(o.adds.is_empty());
}
#[test]
fn overlay_target_is_well_formed_closed_set() {
tatara_lisp::assert_closed_set_well_formed::<OverlayTarget>();
}
#[test]
fn as_str_matches_serde_unit_variant_shape_bytewise() {
for v in OverlayTarget::ALL {
let via_serde = serde_json::to_string(&v).unwrap();
let via_as_str = format!("\"{}\"", v.as_str());
assert_eq!(
via_serde, via_as_str,
"as_str drift from serde output on {v:?}",
);
}
}
#[test]
fn all_covers_the_overlay_target_closed_set_exhaustively() {
assert_eq!(
OverlayTarget::ALL.len(),
3,
"ALL must enumerate every variant of the overlay-target closed set — \
a regression that added a variant at `as_str` but forgot to extend \
`ALL` surfaces here",
);
let seen: std::collections::HashSet<OverlayTarget> =
OverlayTarget::ALL.iter().copied().collect();
assert_eq!(
seen.len(),
OverlayTarget::ALL.len(),
"ALL must not stamp any variant twice — a copy-paste at the sweep surfaces here",
);
assert_eq!(
OverlayTarget::ALL,
[
OverlayTarget::PackageSet,
OverlayTarget::PerSystem,
OverlayTarget::Module,
],
"ALL must preserve declaration order — every downstream consumer \
that binds to the ordering inherits it through this sweep",
);
}
#[test]
fn as_str_matches_pre_lift_pascal_case_literals_bytewise() {
assert_eq!(OverlayTarget::PackageSet.as_str(), "PackageSet");
assert_eq!(OverlayTarget::PerSystem.as_str(), "PerSystem");
assert_eq!(OverlayTarget::Module.as_str(), "Module");
}
#[test]
fn display_composes_through_as_str_bytewise() {
for v in OverlayTarget::ALL {
assert_eq!(v.to_string(), v.as_str());
assert_eq!(format!("{v}"), v.as_str());
}
}
#[test]
fn from_str_round_trip_holds_for_every_variant() {
for v in OverlayTarget::ALL {
let parsed = OverlayTarget::from_str(v.as_str()).unwrap();
assert_eq!(parsed, v, "round-trip drift on {v:?}");
}
}
#[test]
fn from_str_rejects_case_drift_and_out_of_vocabulary_inputs() {
for bad in [
"",
"packageset",
"PACKAGESET",
"Package_Set",
" PackageSet",
"PackageSet ",
"packages-set",
"AttrSet",
"System",
"module",
] {
let err = OverlayTarget::from_str(bad).unwrap_err();
assert_eq!(
err.0, bad,
"UnknownOverlayTarget must echo input verbatim, got {err:?}",
);
}
}
#[test]
fn unknown_carrier_renders_substrate_wide_diagnostic_shape() {
let err = OverlayTarget::from_str("bogus").unwrap_err();
assert_eq!(err.to_string(), "unknown overlay target: bogus");
}
#[test]
fn set_label_matches_pascal_case_spacing_projection() {
assert_eq!(<OverlayTarget as ClosedSet>::SET_LABEL, "overlay target",);
}
#[test]
fn find_by_label_zero_alloc_decode_matches_from_str_on_canonical_input() {
for v in OverlayTarget::ALL {
assert_eq!(
<OverlayTarget as ClosedSet>::find_by_label(v.as_str()),
Some(v),
);
}
assert_eq!(<OverlayTarget as ClosedSet>::find_by_label("bogus"), None,);
}
#[test]
fn cardinality_matches_all_length_on_the_trait() {
assert_eq!(
<OverlayTarget as ClosedSet>::CARDINALITY,
OverlayTarget::ALL.len(),
);
assert_eq!(<OverlayTarget as ClosedSet>::CARDINALITY, 3);
}
}