use crate::domain::Feature;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Loss {
None,
Flagged,
Destructive,
}
impl Loss {
#[must_use]
pub const fn is_lossless(self) -> bool {
matches!(self, Self::None)
}
#[must_use]
pub const fn is_acceptable(self) -> bool {
matches!(self, Self::None | Self::Flagged)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DegradeRule {
StructuredContentToText,
DropAnnotations,
AudioToTextPlaceholder,
DropAudio,
DropImage,
DropResourceLink,
DropResource,
}
impl DegradeRule {
pub const ALL: &'static [Self] = &[
Self::StructuredContentToText,
Self::DropAnnotations,
Self::DropAudio,
Self::DropResourceLink,
];
#[must_use]
pub const fn feature(self) -> Option<Feature> {
match self {
Self::StructuredContentToText => Some(Feature::ToolStructuredOutput),
Self::DropAnnotations => Some(Feature::ToolAnnotations),
Self::AudioToTextPlaceholder | Self::DropAudio => Some(Feature::ToolAudioContent),
Self::DropResourceLink => Some(Feature::ToolResourceLinks),
Self::DropImage | Self::DropResource => None,
}
}
#[must_use]
pub const fn loss(self) -> Loss {
match self {
Self::StructuredContentToText => Loss::None,
Self::DropAnnotations => Loss::Flagged,
Self::AudioToTextPlaceholder
| Self::DropAudio
| Self::DropImage
| Self::DropResourceLink
| Self::DropResource => Loss::Destructive,
}
}
#[must_use]
pub fn is_fallback_for(self, feature: Feature) -> bool {
matches!(self.feature(), Some(f) if f == feature)
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::panic,
reason = "tests panic with the failing precondition, which is the useful part"
)]
use super::{DegradeRule, Loss};
use crate::domain::Feature;
#[test]
fn every_rule_names_a_loss() {
for rule in DegradeRule::ALL {
let _ = rule.loss();
}
assert_eq!(DegradeRule::ALL.len(), 4);
}
#[test]
fn reductions_are_not_mistaken_for_fallbacks() {
assert_eq!(DegradeRule::DropImage.feature(), None);
assert_eq!(DegradeRule::DropResource.feature(), None);
assert!(!DegradeRule::DropImage.is_fallback_for(Feature::Tools));
assert_eq!(
DegradeRule::DropResourceLink.feature(),
Some(Feature::ToolResourceLinks),
);
assert!(DegradeRule::DropResourceLink.is_fallback_for(Feature::ToolResourceLinks));
}
#[test]
fn a_rule_matches_the_feature_it_names() {
for rule in DegradeRule::ALL {
let Some(feature) = rule.feature() else {
continue;
};
assert!(
rule.is_fallback_for(feature),
"{rule:?} names {feature} but does not match it",
);
for other in DegradeRule::ALL {
if other != rule {
assert!(
!other.is_fallback_for(feature) || other.feature() == Some(feature),
"{rule:?} and {other:?} both claim {feature}",
);
}
}
}
}
#[test]
fn lossless_and_destructive_are_not_confused() {
assert_eq!(DegradeRule::StructuredContentToText.loss(), Loss::None);
assert!(DegradeRule::StructuredContentToText.loss().is_lossless());
assert_eq!(
DegradeRule::AudioToTextPlaceholder.loss(),
Loss::Destructive
);
assert!(!DegradeRule::AudioToTextPlaceholder.loss().is_lossless());
assert!(!DegradeRule::AudioToTextPlaceholder.loss().is_acceptable());
assert!(Loss::Flagged.is_acceptable());
assert!(!Loss::Destructive.is_acceptable());
}
#[test]
fn a_lossless_alternative_is_preferred_where_one_exists() {
assert!(DegradeRule::StructuredContentToText.loss().is_acceptable());
assert!(DegradeRule::DropAnnotations.loss().is_acceptable());
for rule in [
DegradeRule::DropAudio,
DegradeRule::DropImage,
DegradeRule::DropResource,
DegradeRule::DropResourceLink,
] {
assert!(!rule.loss().is_acceptable(), "{rule:?}");
}
}
#[test]
fn the_opt_in_rule_is_not_selectable() {
assert!(!DegradeRule::ALL.contains(&DegradeRule::AudioToTextPlaceholder));
assert_eq!(
DegradeRule::AudioToTextPlaceholder.loss(),
Loss::Destructive
);
}
#[test]
fn the_audio_placeholder_covers_only_audio() {
assert!(
DegradeRule::AudioToTextPlaceholder
.is_fallback_for(crate::domain::Feature::ToolAudioContent)
);
assert!(
!DegradeRule::AudioToTextPlaceholder
.is_fallback_for(crate::domain::Feature::ToolStructuredOutput)
);
}
#[test]
fn the_all_list_has_no_duplicates() {
let mut seen = Vec::new();
for rule in DegradeRule::ALL {
assert!(!seen.contains(rule), "{rule:?} twice");
seen.push(*rule);
}
}
#[test]
fn a_listed_rule_is_findable_and_an_unlisted_one_is_not() {
for rule in DegradeRule::ALL {
let Some(feature) = rule.feature() else {
panic!("{rule:?} is listed but names no feature");
};
assert!(rule.is_fallback_for(feature), "{rule:?} / {feature}");
}
for rule in [
DegradeRule::AudioToTextPlaceholder,
DegradeRule::DropImage,
DegradeRule::DropResource,
] {
assert!(
!DegradeRule::ALL.contains(&rule),
"{rule:?} is opt-in and must not be selectable",
);
}
}
}