cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! [`AssetsReader`] decorator: a base layer overlaid by a second
//! reader, with overlay entries winning on path collision. The
//! merge happens eagerly when [`assets`](AssetsReader::assets) is
//! called, then re-streams as an owned iterator — overlay
//! semantics belong to the composition itself, not to consumers.

use std::collections::BTreeMap;

use crate::domain::model::site::Asset;
use crate::domain::usecases::site::readers::AssetsReader;

pub struct OverlayAssetsReader<'a> {
    base: &'a dyn AssetsReader,
    overlay: &'a dyn AssetsReader,
}

impl<'a> OverlayAssetsReader<'a> {
    pub fn new(base: &'a dyn AssetsReader, overlay: &'a dyn AssetsReader) -> Self {
        Self { base, overlay }
    }

    fn merged(&self) -> anyhow::Result<BTreeMap<String, Asset>> {
        let mut map: BTreeMap<String, Asset> = BTreeMap::new();
        for asset in self.base.assets() {
            let asset = asset?;
            map.insert(asset.source.as_str().to_string(), asset);
        }
        for asset in self.overlay.assets() {
            let asset = asset?;
            map.insert(asset.source.as_str().to_string(), asset);
        }
        Ok(map)
    }
}

impl AssetsReader for OverlayAssetsReader<'_> {
    fn assets<'a>(&'a self) -> Box<dyn Iterator<Item = anyhow::Result<Asset>> + 'a> {
        match self.merged() {
            Ok(map) => Box::new(map.into_values().map(Ok)),
            Err(e) => Box::new(std::iter::once(Err(e))),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::model::site::Source;

    struct StubReader(Vec<Asset>);

    impl AssetsReader for StubReader {
        fn assets<'a>(&'a self) -> Box<dyn Iterator<Item = anyhow::Result<Asset>> + 'a> {
            Box::new(self.0.clone().into_iter().map(Ok))
        }
    }

    fn asset(path: &str, body: &str) -> Asset {
        Asset::new(
            Source::relative_path(path).unwrap(),
            body.as_bytes().to_vec(),
        )
    }

    fn collect(reader: &dyn AssetsReader) -> Vec<(String, Vec<u8>)> {
        reader
            .assets()
            .map(Result::unwrap)
            .map(|a| (a.source.as_str().to_string(), a.bytes))
            .collect()
    }

    #[test]
    fn overlay_wins_on_path_collision() {
        let base = StubReader(vec![asset("css/site.css", "base")]);
        let overlay = StubReader(vec![asset("css/site.css", "overlay")]);
        let reader = OverlayAssetsReader::new(&base, &overlay);

        let out = collect(&reader);
        assert_eq!(out, vec![("css/site.css".into(), b"overlay".to_vec())]);
    }

    #[test]
    fn base_assets_pass_through_without_collision() {
        let base = StubReader(vec![asset("a.css", "A"), asset("b.css", "B")]);
        let overlay = StubReader(vec![asset("c.css", "C")]);
        let reader = OverlayAssetsReader::new(&base, &overlay);

        let out = collect(&reader);
        assert_eq!(
            out,
            vec![
                ("a.css".into(), b"A".to_vec()),
                ("b.css".into(), b"B".to_vec()),
                ("c.css".into(), b"C".to_vec()),
            ]
        );
    }

    #[test]
    fn surface_errors_from_either_layer() {
        struct FailingReader;
        impl AssetsReader for FailingReader {
            fn assets<'a>(&'a self) -> Box<dyn Iterator<Item = anyhow::Result<Asset>> + 'a> {
                Box::new(std::iter::once(Err(anyhow::anyhow!("boom"))))
            }
        }
        let base = StubReader(vec![asset("a.css", "A")]);
        let overlay = FailingReader;
        let reader = OverlayAssetsReader::new(&base, &overlay);

        let mut it = reader.assets();
        let first = it.next().unwrap();
        assert!(first.is_err());
    }
}