headless_lms_server/programs/seed/builder/
json_source.rs

1use anyhow::{Context, Result};
2use serde_json::Value;
3use std::fs;
4use std::path::PathBuf;
5
6/// Source for JSON data used in exercises and pages.
7///
8/// Use `asset_json!("path.json")` for compile-time files or `JsonSource::File("path".into())` for runtime paths.
9#[derive(Clone, Debug)]
10pub enum JsonSource {
11    /// JSON data inline in the code
12    Inline(Value),
13    /// JSON file loaded at runtime
14    File(PathBuf),
15}
16
17impl JsonSource {
18    pub fn load(&self) -> Result<Value> {
19        match self {
20            JsonSource::Inline(v) => Ok(v.clone()),
21            JsonSource::File(p) => {
22                let s =
23                    fs::read_to_string(p).with_context(|| format!("Reading {}", p.display()))?;
24                Ok(serde_json::from_str(&s).context("Parsing JSON file")?)
25            }
26        }
27    }
28}
29
30/// Compile-time JSON inclusion macro. Usage: `asset_json!("../../assets/quizzes.json")`
31#[macro_export]
32macro_rules! asset_json {
33    ($path:literal) => {{
34        let _s: &str = include_str!($path);
35        $crate::programs::seed::builder::JsonSource::Inline(
36            serde_json::from_str(_s).expect(concat!("Invalid JSON in asset: ", $path)),
37        )
38    }};
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44    use serde_json::json;
45    use std::io::Write;
46    use tempfile::NamedTempFile;
47
48    #[test]
49    fn inline_loads() {
50        let v = json!({"a":1});
51        let src = JsonSource::Inline(v.clone());
52        let out = src.load().unwrap();
53        assert_eq!(out, v);
54    }
55
56    #[test]
57    fn file_loads() {
58        let mut tmp = NamedTempFile::new().unwrap();
59        write!(tmp, "{{\"k\":\"v\"}}").unwrap();
60        let src = JsonSource::File(tmp.path().to_path_buf());
61        let out = src.load().unwrap();
62        assert_eq!(out["k"], "v");
63    }
64
65    #[test]
66    fn file_bad_json() {
67        let mut tmp = NamedTempFile::new().unwrap();
68        write!(tmp, "not-json").unwrap();
69        let src = JsonSource::File(tmp.path().to_path_buf());
70        assert!(src.load().is_err());
71    }
72
73    #[test]
74    fn json_file_ok_and_bad() {
75        let mut f = tempfile::NamedTempFile::new().unwrap();
76        write!(f, r#"{{"ok":true}}"#).unwrap();
77
78        let src_ok = JsonSource::File(f.path().to_path_buf());
79        let v = src_ok.load().unwrap();
80        assert_eq!(v["ok"], true);
81
82        let mut f2 = tempfile::NamedTempFile::new().unwrap();
83        write!(f2, "nope").unwrap();
84        let src_bad = JsonSource::File(f2.path().to_path_buf());
85        assert!(src_bad.load().is_err());
86    }
87}