use crate::decision::AdBreakDecision;
use crate::error::{Error, Result};
use crate::splice::ConditionedSplicePoint;
use alloc::collections::BTreeMap;
use alloc::string::String;
#[derive(Debug, Clone, PartialEq)]
pub struct BreakState {
pub decision: AdBreakDecision,
pub entered_at: ConditionedSplicePoint,
pub resumed_at: Option<ConditionedSplicePoint>,
}
impl BreakState {
pub fn new(decision: AdBreakDecision, entered_at: ConditionedSplicePoint) -> Self {
BreakState {
decision,
entered_at,
resumed_at: None,
}
}
pub fn is_resumed(&self) -> bool {
self.resumed_at.is_some()
}
}
#[derive(Debug, Clone, Default)]
pub struct SessionStore {
sessions: BTreeMap<String, BreakState>,
}
impl SessionStore {
pub fn new() -> Self {
SessionStore {
sessions: BTreeMap::new(),
}
}
pub fn begin_break(&mut self, session_id: impl Into<String>, state: BreakState) -> Result<()> {
let session_id = session_id.into();
if let Some(existing) = self.sessions.get(&session_id)
&& !existing.is_resumed()
{
return Err(Error::BreakAlreadyActive(session_id));
}
self.sessions.insert(session_id, state);
Ok(())
}
pub fn resume(&mut self, session_id: &str, resumed_at: ConditionedSplicePoint) -> Result<()> {
let state = self
.sessions
.get_mut(session_id)
.ok_or_else(|| Error::NoActiveBreak(session_id.into()))?;
state.resumed_at = Some(resumed_at);
Ok(())
}
pub fn get(&self, session_id: &str) -> Option<&BreakState> {
self.sessions.get(session_id)
}
pub fn end(&mut self, session_id: &str) -> Option<BreakState> {
self.sessions.remove(session_id)
}
pub fn len(&self) -> usize {
self.sessions.len()
}
pub fn is_empty(&self) -> bool {
self.sessions.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::splice::condition_splice_point;
fn splice_point() -> ConditionedSplicePoint {
condition_splice_point(1_000, &[1_000], 0).unwrap()
}
#[test]
fn begin_get_resume_end_lifecycle() {
let mut store = SessionStore::new();
assert!(store.is_empty());
let decision = AdBreakDecision::single_asset("b1", "https://x/a.m3u8");
store
.begin_break("s1", BreakState::new(decision.clone(), splice_point()))
.unwrap();
assert_eq!(store.len(), 1);
assert!(!store.get("s1").unwrap().is_resumed());
store.resume("s1", splice_point()).unwrap();
assert!(store.get("s1").unwrap().is_resumed());
let evicted = store.end("s1").unwrap();
assert_eq!(evicted.decision, decision);
assert!(store.is_empty());
}
#[test]
fn cannot_begin_a_break_while_one_is_already_active() {
let mut store = SessionStore::new();
let decision = AdBreakDecision::single_asset("b1", "https://x/a.m3u8");
store
.begin_break("s1", BreakState::new(decision.clone(), splice_point()))
.unwrap();
let err = store
.begin_break("s1", BreakState::new(decision, splice_point()))
.unwrap_err();
assert!(matches!(err, Error::BreakAlreadyActive(ref s) if s == "s1"));
}
#[test]
fn begin_break_is_allowed_again_after_resume() {
let mut store = SessionStore::new();
let decision = AdBreakDecision::single_asset("b1", "https://x/a.m3u8");
store
.begin_break("s1", BreakState::new(decision.clone(), splice_point()))
.unwrap();
store.resume("s1", splice_point()).unwrap();
store
.begin_break("s1", BreakState::new(decision, splice_point()))
.unwrap();
assert_eq!(store.len(), 1);
}
#[test]
fn resume_without_an_active_break_errors() {
let mut store = SessionStore::new();
let err = store.resume("nope", splice_point()).unwrap_err();
assert!(matches!(err, Error::NoActiveBreak(ref s) if s == "nope"));
}
}