rgen_core/
rpack.rs

1use anyhow::Result;
2use glob::glob;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::path::{Path, PathBuf};
6
7/// Default conventions for pack file discovery
8#[derive(Debug, Clone)]
9pub struct PackConventions {
10    pub template_patterns: &'static [&'static str],
11    pub rdf_patterns: &'static [&'static str],
12    pub query_patterns: &'static [&'static str],
13    pub shape_patterns: &'static [&'static str],
14}
15
16impl Default for PackConventions {
17    fn default() -> Self {
18        Self {
19            template_patterns: &["templates/**/*.tmpl", "templates/**/*.tera"],
20            rdf_patterns: &[
21                "templates/**/graphs/*.ttl", 
22                "templates/**/graphs/*.rdf", 
23                "templates/**/graphs/*.jsonld"
24            ],
25            query_patterns: &["templates/**/queries/*.rq", "templates/**/queries/*.sparql"],
26            shape_patterns: &[
27                "templates/**/graphs/shapes/*.shacl.ttl", 
28                "templates/**/shapes/*.ttl"
29            ],
30        }
31    }
32}
33
34/// Rpack manifest structure
35#[derive(Debug, Clone, Deserialize, Serialize)]
36pub struct RpackManifest {
37    #[serde(rename = "rpack")]
38    pub metadata: RpackMetadata,
39    #[serde(default)]
40    pub dependencies: BTreeMap<String, String>,
41    #[serde(default)]
42    pub templates: TemplatesConfig,
43    #[serde(default)]
44    pub macros: MacrosConfig,
45    #[serde(default)]
46    pub rdf: RdfConfig,
47    #[serde(default)]
48    pub queries: QueriesConfig,
49    #[serde(default)]
50    pub shapes: ShapesConfig,
51    #[serde(default)]
52    pub preset: PresetConfig,
53}
54
55/// Rpack metadata section
56#[derive(Debug, Clone, Deserialize, Serialize)]
57pub struct RpackMetadata {
58    pub id: String,
59    pub name: String,
60    pub version: String,
61    pub description: String,
62    pub license: String,
63    pub rgen_compat: String,
64}
65
66/// Templates configuration
67#[derive(Debug, Clone, Deserialize, Serialize, Default)]
68pub struct TemplatesConfig {
69    /// Glob patterns for template discovery (empty = use conventions)
70    #[serde(default)]
71    pub patterns: Vec<String>,
72    /// Additional Tera includes
73    #[serde(default)]
74    pub includes: Vec<String>,
75}
76
77/// Macros configuration
78#[derive(Debug, Clone, Deserialize, Serialize, Default)]
79pub struct MacrosConfig {
80    #[serde(default)]
81    pub paths: Vec<String>,
82}
83
84/// RDF configuration
85#[derive(Debug, Clone, Deserialize, Serialize, Default)]
86pub struct RdfConfig {
87    #[serde(default)]
88    pub base: Option<String>,
89    #[serde(default)]
90    pub prefixes: BTreeMap<String, String>,
91    /// Glob patterns for RDF file discovery (empty = use conventions)
92    #[serde(default)]
93    pub patterns: Vec<String>,
94    /// Inline RDF content
95    #[serde(default)]
96    pub inline: Vec<String>,
97}
98
99/// Queries configuration
100#[derive(Debug, Clone, Deserialize, Serialize, Default)]
101pub struct QueriesConfig {
102    /// Glob patterns for query file discovery (empty = use conventions)
103    #[serde(default)]
104    pub patterns: Vec<String>,
105    #[serde(default)]
106    pub aliases: BTreeMap<String, String>,
107}
108
109/// Shapes configuration
110#[derive(Debug, Clone, Deserialize, Serialize, Default)]
111pub struct ShapesConfig {
112    /// Glob patterns for shape file discovery (empty = use conventions)
113    #[serde(default)]
114    pub patterns: Vec<String>,
115}
116
117/// Preset configuration
118#[derive(Debug, Clone, Deserialize, Serialize, Default)]
119pub struct PresetConfig {
120    #[serde(default)]
121    pub config: Option<PathBuf>,
122    #[serde(default)]
123    pub vars: BTreeMap<String, String>,
124}
125
126/// Discover files using glob patterns
127fn discover_files(base_path: &Path, patterns: &[&str]) -> Result<Vec<PathBuf>> {
128    let mut files = Vec::new();
129    for pattern in patterns {
130        let full_pattern = base_path.join(pattern);
131        for entry in glob(&full_pattern.to_string_lossy())? {
132            files.push(entry?);
133        }
134    }
135    files.sort(); // Deterministic order
136    Ok(files)
137}
138
139impl RpackManifest {
140    /// Load manifest from a file
141    pub fn load_from_file(path: &PathBuf) -> Result<Self> {
142        let content = std::fs::read_to_string(path)?;
143        let manifest: RpackManifest = toml::from_str(&content)?;
144        Ok(manifest)
145    }
146
147    /// Discover template files using conventions or config
148    pub fn discover_templates(&self, base_path: &Path) -> Result<Vec<PathBuf>> {
149        let patterns = if self.templates.patterns.is_empty() {
150            PackConventions::default().template_patterns
151        } else {
152            &self
153                .templates
154                .patterns
155                .iter()
156                .map(|s| s.as_str())
157                .collect::<Vec<_>>()
158        };
159
160        discover_files(base_path, patterns)
161    }
162
163    /// Discover RDF files using conventions or config
164    pub fn discover_rdf_files(&self, base_path: &Path) -> Result<Vec<PathBuf>> {
165        let patterns = if self.rdf.patterns.is_empty() {
166            PackConventions::default().rdf_patterns
167        } else {
168            &self
169                .rdf
170                .patterns
171                .iter()
172                .map(|s| s.as_str())
173                .collect::<Vec<_>>()
174        };
175
176        discover_files(base_path, patterns)
177    }
178
179    /// Discover query files using conventions or config
180    pub fn discover_query_files(&self, base_path: &Path) -> Result<Vec<PathBuf>> {
181        let patterns = if self.queries.patterns.is_empty() {
182            PackConventions::default().query_patterns
183        } else {
184            &self
185                .queries
186                .patterns
187                .iter()
188                .map(|s| s.as_str())
189                .collect::<Vec<_>>()
190        };
191
192        discover_files(base_path, patterns)
193    }
194
195    /// Discover shape files using conventions or config
196    pub fn discover_shape_files(&self, base_path: &Path) -> Result<Vec<PathBuf>> {
197        let patterns = if self.shapes.patterns.is_empty() {
198            PackConventions::default().shape_patterns
199        } else {
200            &self
201                .shapes
202                .patterns
203                .iter()
204                .map(|s| s.as_str())
205                .collect::<Vec<_>>()
206        };
207
208        discover_files(base_path, patterns)
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use std::io::Write;
216    use tempfile::NamedTempFile;
217
218    #[test]
219    fn test_manifest_parsing() {
220        let toml_content = r#"
221[rpack]
222id = "io.rgen.rust.cli-subcommand"
223name = "Rust CLI subcommand"
224version = "0.1.0"
225description = "Generate clap subcommands"
226license = "MIT"
227rgen_compat = ">=0.1 <0.2"
228
229[dependencies]
230"io.rgen.macros.std" = "^0.1"
231
232[templates]
233patterns = ["cli/subcommand/*.tmpl"]
234includes = ["macros/**/*.tera"]
235
236[rdf]
237base = "http://example.org/"
238prefixes.ex = "http://example.org/"
239patterns = ["templates/**/graphs/*.ttl"]
240inline = ["@prefix ex: <http://example.org/> . ex:Foo a ex:Type ."]
241
242[queries]
243patterns = ["../queries/*.rq"]
244aliases.component_by_name = "../queries/component_by_name.rq"
245
246[shapes]
247patterns = ["../shapes/*.ttl"]
248
249[preset]
250config = "../preset/rgen.toml"
251vars = { author = "Acme", license = "MIT" }
252"#;
253
254        let manifest: RpackManifest = toml::from_str(toml_content).unwrap();
255
256        assert_eq!(manifest.metadata.id, "io.rgen.rust.cli-subcommand");
257        assert_eq!(manifest.metadata.name, "Rust CLI subcommand");
258        assert_eq!(manifest.metadata.version, "0.1.0");
259        assert_eq!(manifest.templates.patterns.len(), 1);
260        assert_eq!(manifest.rdf.patterns.len(), 1);
261        assert_eq!(manifest.queries.aliases.len(), 1);
262    }
263
264    #[test]
265    fn test_manifest_load_from_file() {
266        let mut temp_file = NamedTempFile::new().unwrap();
267        let toml_content = r#"
268[rpack]
269id = "test"
270name = "Test"
271version = "0.1.0"
272description = "Test"
273license = "MIT"
274rgen_compat = ">=0.1 <0.2"
275"#;
276        temp_file.write_all(toml_content.as_bytes()).unwrap();
277
278        let manifest = RpackManifest::load_from_file(&temp_file.path().to_path_buf()).unwrap();
279        assert_eq!(manifest.metadata.id, "test");
280    }
281}