rgen_core/
config.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct RgenConfig {
8    /// Templates directory (relative to config file)
9    pub templates_dir: Option<String>,
10
11    /// Base IRI for RDF operations
12    pub base: Option<String>,
13
14    /// Global prefixes for RDF operations
15    #[serde(default)]
16    pub prefixes: BTreeMap<String, String>,
17
18    /// RDF configuration
19    pub rdf: Option<RdfConfig>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct RdfConfig {
24    /// RDF files to load (relative to config file)
25    #[serde(default)]
26    pub files: Vec<String>,
27
28    /// Inline RDF content
29    #[serde(default)]
30    pub inline: Vec<String>,
31}
32
33impl Default for RgenConfig {
34    fn default() -> Self {
35        Self {
36            templates_dir: Some("templates".to_string()),
37            base: None,
38            prefixes: BTreeMap::new(),
39            rdf: None,
40        }
41    }
42}
43
44impl RgenConfig {
45    /// Load rgen.toml from the given directory, walking up the tree
46    pub fn discover_and_load(start_dir: &Path) -> Result<Option<(Self, PathBuf)>> {
47        let mut current = start_dir.to_path_buf();
48
49        loop {
50            let config_path = current.join("rgen.toml");
51            if config_path.exists() {
52                let config = Self::load_from_file(&config_path)?;
53                return Ok(Some((config, config_path)));
54            }
55
56            // Move up one directory
57            match current.parent() {
58                Some(parent) => current = parent.to_path_buf(),
59                None => break, // Reached root
60            }
61        }
62
63        Ok(None)
64    }
65
66    /// Load rgen.toml from a specific file
67    pub fn load_from_file(config_path: &Path) -> Result<Self> {
68        let content = std::fs::read_to_string(config_path)?;
69        let config: RgenConfig = toml::from_str(&content)?;
70        Ok(config)
71    }
72
73    /// Resolve a path relative to the config file directory
74    pub fn resolve_path(&self, config_dir: &Path, path: &str) -> PathBuf {
75        if Path::new(path).is_absolute() {
76            PathBuf::from(path)
77        } else {
78            config_dir.join(path)
79        }
80    }
81
82    /// Get templates directory path, resolved relative to config file
83    pub fn templates_dir_path(&self, config_dir: &Path) -> PathBuf {
84        let templates_dir = self.templates_dir.as_deref().unwrap_or("templates");
85        self.resolve_path(config_dir, templates_dir)
86    }
87
88    /// Get RDF file paths, resolved relative to config file
89    pub fn rdf_file_paths(&self, config_dir: &Path) -> Vec<PathBuf> {
90        self.rdf
91            .as_ref()
92            .map(|rdf| {
93                rdf.files
94                    .iter()
95                    .map(|file| self.resolve_path(config_dir, file))
96                    .collect()
97            })
98            .unwrap_or_default()
99    }
100
101    /// Get inline RDF content
102    pub fn rdf_inline_content(&self) -> Vec<String> {
103        self.rdf
104            .as_ref()
105            .map(|rdf| rdf.inline.clone())
106            .unwrap_or_default()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use std::fs;
114    use tempfile::TempDir;
115
116    #[test]
117    fn test_config_discovery() -> Result<()> {
118        let temp_dir = TempDir::new()?;
119        let config_dir = temp_dir.path().join("project").join("subdir");
120        fs::create_dir_all(&config_dir)?;
121
122        // Create rgen.toml in project root
123        let config_path = temp_dir.path().join("project").join("rgen.toml");
124        fs::write(
125            &config_path,
126            r#"
127templates_dir = "custom_templates"
128base = "http://example.org/"
129[prefixes]
130ex = "http://example.org/"
131rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
132"#,
133        )?;
134
135        let (config, found_path) = RgenConfig::discover_and_load(&config_dir)?.unwrap();
136
137        assert_eq!(found_path, config_path);
138        assert_eq!(config.templates_dir, Some("custom_templates".to_string()));
139        assert_eq!(config.base, Some("http://example.org/".to_string()));
140        assert_eq!(
141            config.prefixes.get("ex"),
142            Some(&"http://example.org/".to_string())
143        );
144
145        Ok(())
146    }
147
148    #[test]
149    fn test_config_not_found() -> Result<()> {
150        let temp_dir = TempDir::new()?;
151        let result = RgenConfig::discover_and_load(temp_dir.path())?;
152        assert!(result.is_none());
153        Ok(())
154    }
155
156    #[test]
157    fn test_path_resolution() -> Result<()> {
158        let config = RgenConfig::default();
159        let config_dir = Path::new("/project");
160
161        // Relative path
162        let resolved = config.resolve_path(config_dir, "templates");
163        assert_eq!(resolved, PathBuf::from("/project/templates"));
164
165        // Absolute path
166        let resolved = config.resolve_path(config_dir, "/absolute/path");
167        assert_eq!(resolved, PathBuf::from("/absolute/path"));
168
169        Ok(())
170    }
171
172    #[test]
173    fn test_rdf_config() -> Result<()> {
174        let config_content = r#"
175templates_dir = "templates"
176base = "http://example.org/"
177[prefixes]
178ex = "http://example.org/"
179
180[rdf]
181files = ["graphs/core.ttl", "graphs/shapes.ttl"]
182inline = [
183    "@prefix ex: <http://example.org/> . ex:test a ex:Thing ."
184]
185"#;
186
187        let config: RgenConfig = toml::from_str(config_content)?;
188        let config_dir = Path::new("/project");
189
190        let rdf_paths = config.rdf_file_paths(config_dir);
191        assert_eq!(rdf_paths.len(), 2);
192        assert_eq!(rdf_paths[0], PathBuf::from("/project/graphs/core.ttl"));
193        assert_eq!(rdf_paths[1], PathBuf::from("/project/graphs/shapes.ttl"));
194
195        let inline_content = config.rdf_inline_content();
196        assert_eq!(inline_content.len(), 1);
197        assert!(inline_content[0].contains("ex:test a ex:Thing"));
198
199        Ok(())
200    }
201}