use std::collections::HashMap;
use crate::graph::{LevelGraph, Source};
use crate::pipeline::PipelineFile;
pub(super) type SourceKey<'a> = Option<&'a str>;
pub(super) fn key(source: Source<'_>) -> SourceKey<'_> {
match source {
Source::LevelInput => None,
Source::Node(id) => Some(id),
}
}
pub(super) fn consumer_counts<'a>(graph: &LevelGraph<'a>) -> HashMap<SourceKey<'a>, usize> {
let mut counts = HashMap::new();
for source in graph.inputs.values().flatten().chain(&graph.outputs) {
*counts.entry(key(*source)).or_insert(0) += 1;
}
counts
}
pub(super) struct Routed<'a> {
counts: HashMap<SourceKey<'a>, usize>,
held: HashMap<SourceKey<'a>, Held>,
}
struct Held {
files: Vec<PipelineFile>,
left: usize,
}
impl<'a> Routed<'a> {
pub(super) fn new(graph: &LevelGraph<'a>, files: Vec<PipelineFile>) -> Self {
let mut routed = Self {
counts: consumer_counts(graph),
held: HashMap::new(),
};
routed.hold(None, files);
routed
}
pub(super) fn hand_over(&mut self, node_id: &'a str, files: Vec<PipelineFile>) {
self.hold(Some(node_id), files);
}
fn hold(&mut self, key: SourceKey<'a>, files: Vec<PipelineFile>) {
let left = self.counts.get(&key).copied().unwrap_or(0);
if left > 0 {
self.held.insert(key, Held { files, left });
}
}
pub(super) fn take(&mut self, sources: &[Source<'a>]) -> Vec<PipelineFile> {
let mut files = Vec::new();
for source in sources {
files.extend(self.take_one(key(*source)));
}
files
}
fn take_one(&mut self, key: SourceKey<'a>) -> Vec<PipelineFile> {
let Some(held) = self.held.get_mut(&key) else {
return Vec::new();
};
held.left -= 1;
if held.left > 0 {
return held.files.clone();
}
self.held.remove(&key).map(|h| h.files).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use crate::processor::FileData;
fn file(name: &str) -> PipelineFile {
PipelineFile {
name: name.to_string(),
data: FileData::Bytes(name.as_bytes().to_vec()),
mime_type: "text/plain".to_string(),
metadata: serde_json::Map::new(),
}
}
fn graph<'a>(
inputs: &[(&'a str, Vec<Source<'a>>)],
outputs: Vec<Source<'a>>,
) -> LevelGraph<'a> {
LevelGraph {
order: Vec::new(),
inputs: inputs.iter().cloned().collect::<BTreeMap<_, _>>(),
outputs,
}
}
#[test]
fn test_every_consumer_gets_the_files_and_the_last_empties_the_slot() {
let g = graph(&[("a", vec![Source::LevelInput])], vec![Source::LevelInput]);
let mut routed = Routed::new(&g, vec![file("x")]);
assert_eq!(routed.take(&[Source::LevelInput])[0].name, "x");
assert_eq!(routed.take(&[Source::LevelInput])[0].name, "x");
assert!(routed.held.is_empty());
}
#[test]
fn test_fan_in_joins_sources_in_the_order_given() {
let sources = vec![Source::Node("b"), Source::Node("a")];
let g = graph(&[("j", sources.clone())], vec![Source::Node("j")]);
let mut routed = Routed::new(&g, vec![]);
routed.hand_over("a", vec![file("from-a")]);
routed.hand_over("b", vec![file("from-b")]);
let names: Vec<String> = routed.take(&sources).into_iter().map(|f| f.name).collect();
assert_eq!(names, vec!["from-b", "from-a"]);
}
#[test]
fn test_a_source_nobody_reads_is_dropped() {
let g = graph(&[("a", vec![])], vec![Source::Node("a")]);
let mut routed = Routed::new(&g, vec![file("unread")]);
routed.hand_over("lost", vec![file("lost")]);
assert!(routed.held.is_empty());
}
}