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 pub templates_dir: Option<String>,
10
11 pub base: Option<String>,
13
14 #[serde(default)]
16 pub prefixes: BTreeMap<String, String>,
17
18 pub rdf: Option<RdfConfig>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct RdfConfig {
24 #[serde(default)]
26 pub files: Vec<String>,
27
28 #[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 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 match current.parent() {
58 Some(parent) => current = parent.to_path_buf(),
59 None => break, }
61 }
62
63 Ok(None)
64 }
65
66 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 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 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 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 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 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 let resolved = config.resolve_path(config_dir, "templates");
163 assert_eq!(resolved, PathBuf::from("/project/templates"));
164
165 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}