Skip to main content

edgeparse_core/api/
config_loader.rs

1//! Config file loading — read [`ProcessingConfig`] from JSON files.
2
3use std::path::Path;
4
5use crate::api::config::ProcessingConfig;
6use crate::EdgePdfError;
7
8/// Load a [`ProcessingConfig`] from a JSON file at the given path.
9///
10/// Missing fields in the JSON fall back to their defaults via serde.
11///
12/// # Errors
13/// Returns `EdgePdfError::IoError` on read failure or `EdgePdfError::OutputError`
14/// on parse failure.
15pub fn load_config_from_file(path: &Path) -> Result<ProcessingConfig, EdgePdfError> {
16    let content = std::fs::read_to_string(path)?;
17    parse_config_json(&content)
18}
19
20/// Parse a [`ProcessingConfig`] from a JSON string.
21///
22/// # Errors
23/// Returns `EdgePdfError::OutputError` on JSON parse failure.
24pub fn parse_config_json(json: &str) -> Result<ProcessingConfig, EdgePdfError> {
25    serde_json::from_str(json)
26        .map_err(|e| EdgePdfError::OutputError(format!("Failed to parse config JSON: {}", e)))
27}
28
29/// Serialize a [`ProcessingConfig`] to a pretty-printed JSON string.
30///
31/// # Errors
32/// Returns `EdgePdfError::OutputError` on serialization failure.
33pub fn config_to_json(config: &ProcessingConfig) -> Result<String, EdgePdfError> {
34    serde_json::to_string_pretty(config)
35        .map_err(|e| EdgePdfError::OutputError(format!("Failed to serialize config: {}", e)))
36}
37
38/// Merge two configs: values present in `overlay` override those in `base`.
39///
40/// This works by serializing both to JSON, merging the JSON objects, and
41/// deserializing back. Fields in `overlay` that are `null` are skipped.
42pub fn merge_configs(
43    base: &ProcessingConfig,
44    overlay_json: &str,
45) -> Result<ProcessingConfig, EdgePdfError> {
46    let base_json = serde_json::to_value(base)
47        .map_err(|e| EdgePdfError::OutputError(format!("config serialization error: {}", e)))?;
48    let overlay_val: serde_json::Value = serde_json::from_str(overlay_json)
49        .map_err(|e| EdgePdfError::OutputError(format!("overlay parse error: {}", e)))?;
50
51    let merged = merge_json(base_json, overlay_val);
52    serde_json::from_value(merged)
53        .map_err(|e| EdgePdfError::OutputError(format!("merged config parse error: {}", e)))
54}
55
56fn merge_json(base: serde_json::Value, overlay: serde_json::Value) -> serde_json::Value {
57    use serde_json::Value;
58    match (base, overlay) {
59        (Value::Object(mut base_map), Value::Object(overlay_map)) => {
60            for (key, val) in overlay_map {
61                if val.is_null() {
62                    continue;
63                }
64                let entry = base_map.remove(&key).unwrap_or(Value::Null);
65                base_map.insert(key, merge_json(entry, val));
66            }
67            Value::Object(base_map)
68        }
69        (_, overlay) => overlay,
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::api::config::{OutputFormat, ReadingOrder};
77
78    #[test]
79    fn test_parse_default_config() {
80        let config = ProcessingConfig::default();
81        let json = config_to_json(&config).unwrap();
82        let parsed = parse_config_json(&json).unwrap();
83        assert_eq!(parsed.formats, config.formats);
84        assert_eq!(parsed.quiet, config.quiet);
85    }
86
87    #[test]
88    fn test_parse_partial_config() {
89        // Partial JSON doesn't work directly — use merge approach
90        let base = ProcessingConfig::default();
91        let overlay = r#"{"quiet": true, "sanitize": true}"#;
92        let config = merge_configs(&base, overlay).unwrap();
93        assert!(config.quiet);
94        assert!(config.sanitize);
95    }
96
97    #[test]
98    fn test_merge_configs() {
99        let base = ProcessingConfig::default();
100        let overlay = r#"{"quiet": true, "reading_order": "Off"}"#;
101        let merged = merge_configs(&base, overlay).unwrap();
102        assert!(merged.quiet);
103        assert_eq!(merged.reading_order, ReadingOrder::Off);
104        // Other fields should retain defaults
105        assert_eq!(merged.formats, vec![OutputFormat::Json]);
106    }
107
108    #[test]
109    fn test_config_roundtrip() {
110        let mut config = ProcessingConfig::default();
111        config.quiet = true;
112        config.sanitize = true;
113        config.pages = Some("1-5".to_string());
114
115        let json = config_to_json(&config).unwrap();
116        let parsed = parse_config_json(&json).unwrap();
117        assert_eq!(parsed.quiet, true);
118        assert_eq!(parsed.sanitize, true);
119        assert_eq!(parsed.pages, Some("1-5".to_string()));
120    }
121
122    #[test]
123    fn test_invalid_json() {
124        let result = parse_config_json("not json");
125        assert!(result.is_err());
126    }
127}