zen_engine/loader/
config.rs1use 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}