use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BackpressurePolicy {
DropOldest,
DegradeQuality,
PauseUpstream,
Fail,
}
impl BackpressurePolicy {
pub const ALL: &'static [BackpressurePolicy] = &[
BackpressurePolicy::DropOldest,
BackpressurePolicy::DegradeQuality,
BackpressurePolicy::PauseUpstream,
BackpressurePolicy::Fail,
];
pub fn slug(self) -> &'static str {
match self {
BackpressurePolicy::DropOldest => "drop_oldest",
BackpressurePolicy::DegradeQuality => "degrade_quality",
BackpressurePolicy::PauseUpstream => "pause_upstream",
BackpressurePolicy::Fail => "fail",
}
}
pub fn from_slug(slug: &str) -> Option<BackpressurePolicy> {
Self::ALL.iter().copied().find(|p| p.slug() == slug)
}
}
impl fmt::Display for BackpressurePolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.slug())
}
}
pub const BACKPRESSURE_CATALOG: &[&str] = &[
"drop_oldest",
"degrade_quality",
"pause_upstream",
"fail",
];
pub const STREAM_TYPE_CTOR: &str = "Stream";
pub fn is_stream_type(name: &str) -> bool {
name == STREAM_TYPE_CTOR
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackpressureAnnotation {
pub policy: BackpressurePolicy,
pub options: Vec<(String, String)>,
}
pub fn parse_backpressure_annotation(
body: &str,
) -> Option<BackpressureAnnotation> {
let mut parts = body.split(',').map(|p| p.trim());
let policy_slug = parts.next()?.trim();
let policy = BackpressurePolicy::from_slug(policy_slug)?;
let mut options = Vec::new();
for raw in parts {
if raw.is_empty() {
continue;
}
let (k, v) = raw.split_once('=')?;
options.push((k.trim().to_string(), v.trim().to_string()));
}
Some(BackpressureAnnotation { policy, options })
}
pub const STREAM_EFFECT_SLUG: &str = "stream";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slug_roundtrip_covers_closed_catalog() {
for policy in BackpressurePolicy::ALL {
let slug = policy.slug();
assert_eq!(Some(*policy), BackpressurePolicy::from_slug(slug));
assert!(BACKPRESSURE_CATALOG.contains(&slug));
}
assert_eq!(
BackpressurePolicy::ALL.len(),
BACKPRESSURE_CATALOG.len()
);
}
#[test]
fn unknown_policy_slug_rejected() {
assert!(BackpressurePolicy::from_slug("retry_forever").is_none());
assert!(BackpressurePolicy::from_slug("").is_none());
}
#[test]
fn stream_type_recognised() {
assert!(is_stream_type("Stream"));
assert!(!is_stream_type("stream")); assert!(!is_stream_type("Iterator"));
}
#[test]
fn parse_annotation_minimal() {
let ann = parse_backpressure_annotation("drop_oldest").unwrap();
assert_eq!(ann.policy, BackpressurePolicy::DropOldest);
assert!(ann.options.is_empty());
}
#[test]
fn parse_annotation_with_options() {
let ann = parse_backpressure_annotation(
"degrade_quality, resample_to=8000, codec=mulaw",
)
.unwrap();
assert_eq!(ann.policy, BackpressurePolicy::DegradeQuality);
assert_eq!(
ann.options,
vec![
("resample_to".to_string(), "8000".to_string()),
("codec".to_string(), "mulaw".to_string()),
]
);
}
#[test]
fn parse_annotation_rejects_malformed() {
assert!(parse_backpressure_annotation("drop_oldest, no_equals").is_none());
assert!(parse_backpressure_annotation("bogus_policy").is_none());
}
#[test]
fn stream_effect_slug_is_stable() {
assert_eq!(STREAM_EFFECT_SLUG, "stream");
}
}