overlay-file 1.0.0-rc.2

Rust implementation of OverlayFile used to define overlays in OCA
Documentation
use crate::OverlayFile;
use crate::{OverlayDef, parse_from_string};
use log::debug;
use std::{collections::HashMap, fs, path::Path};

pub trait OverlayRegistry {
    fn get_by_filename(&self, name: &str) -> Option<&OverlayFile>;
    /// Get overlay by name (namespace + name)
    fn get_by_name(&self, name: &str) -> Result<Option<&OverlayDef>, &'static str>;
    /// Get overlay by fully qualified name (namespace + name + version)
    fn get_by_fqn(&self, name: &str) -> Result<&OverlayDef, &'static str>;
    fn list_by_namespace(&self, namespace: &str) -> Vec<&OverlayFile>;

    fn list_all(&self) -> Vec<String>;
}

#[derive(Debug, Clone)]
/// File based registry for overlays
pub struct OverlayLocalRegistry {
    overlays: HashMap<String, OverlayFile>,
}

impl OverlayLocalRegistry {
    pub fn from_dir<P: AsRef<Path>>(dir: P) -> Result<Self, std::io::Error> {
        let mut overlays = HashMap::new();

        for entry in fs::read_dir(dir)? {
            let path = entry?.path();
            if path.extension().and_then(|s| s.to_str()) == Some("overlayfile")
                && let Some(name) = Self::overlay_name_from_path(&path)
            {
                let content = fs::read_to_string(&path)?;
                debug!("Parsing overlay file: {}", path.display());
                let schema = parse_from_string(content);
                overlays.insert(name, schema.unwrap());
            }
        }

        Ok(OverlayLocalRegistry { overlays })
    }

    pub fn from_file<P: AsRef<Path>>(file: P) -> Result<Self, std::io::Error> {
        let path = file.as_ref();

        // Ensure it’s an overlay file
        if path.extension().and_then(|s| s.to_str()) != Some("overlayfile") {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                "File does not have .overlayfile extension",
            ));
        }

        let mut overlays = HashMap::new();

        if let Some(name) = Self::overlay_name_from_path(path) {
            debug!("Parsing overlay file: {}", path.display());
            let content = fs::read_to_string(path)?;
            let schema = parse_from_string(content);
            overlays.insert(name, schema.unwrap());
        }

        Ok(OverlayLocalRegistry { overlays })
    }

    fn overlay_name_from_path(path: &Path) -> Option<String> {
        path.file_stem()
            .and_then(|s| s.to_str())
            .map(|s| s.to_string())
    }

    pub fn new() -> Self {
        OverlayLocalRegistry {
            overlays: HashMap::new(),
        }
    }
}

impl Default for OverlayLocalRegistry {
    fn default() -> Self {
        Self::new()
    }
}

impl OverlayRegistry for OverlayLocalRegistry {
    // TODO remove it shouldn't be needed
    fn get_by_filename(&self, name: &str) -> Option<&OverlayFile> {
        self.overlays.get(name)
    }

    fn get_by_fqn(&self, overlay_name: &str) -> Result<&OverlayDef, &'static str> {
        debug!("Getting overlay by fq name: {}", overlay_name);
        let (namespace, name) = overlay_name
            .split_once(':')
            .map(|(ns, n)| (Some(ns), n))
            .unwrap_or((None, overlay_name));
        let (name, version) = name
            .split_once("/")
            .ok_or("Invalid overlay name format: version not found or in wrong format")?;
        let name = name.to_ascii_lowercase();
        let namespace = namespace.map(|ns| ns.to_ascii_lowercase());

        self.overlays
            .values()
            .flat_map(|overlay_file| &overlay_file.overlays_def)
            .find(|o| {
                let o_ns = o.namespace.as_ref().map(|s| s.to_ascii_lowercase());
                o_ns == namespace
                    && o.name.eq_ignore_ascii_case(&name)
                    && o.version.eq_ignore_ascii_case(version)
            })
            .ok_or("Overlay definition not found in registry")
    }

    // When processing OCAFILE we normally does not have version specify and need to fetch definition just by name
    fn get_by_name(&self, name: &str) -> Result<Option<&OverlayDef>, &'static str> {
        debug!("Getting overlay by name: {}", name);
        let overlay_def = self.overlays.values().find_map(|overlay_file| {
            overlay_file
                .overlays_def
                .iter()
                .find(|o| o.name.eq_ignore_ascii_case(name))
        });
        Ok(overlay_def)
    }

    fn list_all(&self) -> Vec<String> {
        // Extract all overlay namespace
        self.overlays
            .iter()
            .flat_map(|(_, overlay_file)| {
                overlay_file.overlays_def.iter().map(|o| {
                    let namespace = o
                        .namespace
                        .as_ref()
                        .map_or(String::new(), |ns| format!("{:?}:", ns));
                    format!("{}{}/{}", namespace, o.name, o.version)
                })
            })
            .collect::<Vec<String>>()
    }

    fn list_by_namespace(&self, _namespace: &str) -> Vec<&OverlayFile> {
        todo!()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_overlay_registry() {
        let _ = env_logger::builder().is_test(true).try_init();
        let registry = OverlayLocalRegistry::from_dir("core_overlays").unwrap();
        assert_eq!(registry.list_all().len(), 13);
        assert!(registry.get_by_filename("semantic").is_some());
        assert_eq!(registry.get_by_fqn("label/2.0.0").unwrap().name, "label");

        // TODO file can include more then one overlay
        let semantic_overlay_file = registry.get_by_filename("semantic").unwrap();
        assert_eq!(semantic_overlay_file.overlays_def.len(), 13);
        let label_overlay = semantic_overlay_file.overlays_def.first().unwrap();
        assert_eq!(label_overlay.name, "label");
    }
}