Skip to main content

zen_engine/loader/
config.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5
6use crate::loader::{DynamicLoader, FilesystemLoader, FilesystemLoaderOptions, MemoryLoader};
7use crate::model::DecisionContent;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(
11    tag = "type",
12    rename_all = "camelCase",
13    rename_all_fields = "camelCase"
14)]
15pub enum LoaderConfig {
16    Static {
17        content: HashMap<String, DecisionContent>,
18    },
19    #[serde(rename = "fs")]
20    Filesystem {
21        path: String,
22    },
23    Zip {
24        bytes: Vec<u8>,
25    },
26}
27
28impl LoaderConfig {
29    pub fn into_loader(self) -> anyhow::Result<DynamicLoader> {
30        match self {
31            LoaderConfig::Static { content } => {
32                let loader = MemoryLoader::default();
33                for (key, decision_content) in content {
34                    loader.add(key, decision_content);
35                }
36                Ok(Arc::new(loader))
37            }
38            LoaderConfig::Filesystem { path } => {
39                Ok(Arc::new(FilesystemLoader::new(FilesystemLoaderOptions {
40                    root: path,
41                })))
42            }
43            LoaderConfig::Zip { bytes } => Self::loader_from_zip(&bytes),
44        }
45    }
46
47    fn loader_from_zip(bytes: &[u8]) -> anyhow::Result<DynamicLoader> {
48        use std::io::Read;
49
50        let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes))?;
51        let loader = MemoryLoader::default();
52        for index in 0..archive.len() {
53            let mut entry = archive.by_index(index)?;
54            if !entry.is_file() || !entry.name().ends_with(".json") {
55                continue;
56            }
57
58            let key = entry.name().to_string();
59            let mut buffer = Vec::with_capacity(entry.size() as usize);
60            entry.read_to_end(&mut buffer)?;
61            let content: DecisionContent = serde_json::from_slice(&buffer)?;
62            loader.add(key, content);
63        }
64
65        Ok(Arc::new(loader))
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    const GRAPH_JSON: &str = r#"{"nodes":[],"edges":[]}"#;
74
75    #[tokio::test]
76    async fn static_config_serves_decisions_by_key() {
77        let mut content = HashMap::new();
78        content.insert(
79            "graph.json".to_string(),
80            serde_json::from_str::<DecisionContent>(GRAPH_JSON).unwrap(),
81        );
82
83        let loader = LoaderConfig::Static { content }.into_loader().unwrap();
84        assert!(loader.load("graph.json").await.is_ok());
85        assert!(loader.load("missing.json").await.is_err());
86    }
87
88    #[test]
89    fn fs_config_reads_path() {
90        let config: LoaderConfig = serde_json::from_str(r#"{"type":"fs","path":"p"}"#).unwrap();
91
92        let LoaderConfig::Filesystem { path } = config else {
93            panic!("expected filesystem loader config");
94        };
95
96        assert_eq!(path, "p");
97    }
98
99    #[tokio::test]
100    async fn zip_config_decompresses_and_serves_decisions() {
101        use std::io::Write;
102
103        let mut cursor = std::io::Cursor::new(Vec::new());
104        {
105            let mut writer = zip::ZipWriter::new(&mut cursor);
106            let options = zip::write::SimpleFileOptions::default()
107                .compression_method(zip::CompressionMethod::Deflated);
108            writer.start_file("graph.json", options).unwrap();
109            writer.write_all(GRAPH_JSON.as_bytes()).unwrap();
110            writer.finish().unwrap();
111        }
112
113        let loader = LoaderConfig::Zip {
114            bytes: cursor.into_inner(),
115        }
116        .into_loader()
117        .unwrap();
118
119        assert!(loader.load("graph.json").await.is_ok());
120        assert!(loader.load("missing.json").await.is_err());
121    }
122}