rgen_core/
template.rs

1use anyhow::Result;
2use gray_matter::{engine::YAML, Matter, ParsedEntity};
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use tera::{Context, Tera};
6
7use crate::graph::Graph;
8
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
10pub struct Frontmatter {
11    // Hygen core
12    pub to: Option<String>,
13    pub from: Option<String>,
14    #[serde(default)]
15    pub force: bool,
16    #[serde(default)]
17    pub unless_exists: bool,
18
19    // Injection
20    #[serde(default)]
21    pub inject: bool,
22    pub before: Option<String>,
23    pub after: Option<String>,
24    #[serde(default)]
25    pub prepend: bool,
26    #[serde(default)]
27    pub append: bool,
28    pub at_line: Option<u32>,
29    #[serde(default)]
30    pub eof_last: bool,
31    pub skip_if: Option<String>,
32
33    // Shell hooks
34    #[serde(alias = "sh")]
35    pub sh_before: Option<String>,
36    pub sh_after: Option<String>,
37
38    // Graph additions (renderable)
39    #[serde(default)]
40    pub base: Option<String>,
41    #[serde(default)]
42    pub prefixes: BTreeMap<String, String>,
43    #[serde(default, deserialize_with = "string_or_seq")]
44    pub rdf_inline: Vec<String>,
45    #[serde(default, deserialize_with = "string_or_seq")]
46    pub rdf: Vec<String>, // treat as inline TTL in prototype
47    #[serde(default, deserialize_with = "sparql_map")]
48    pub sparql: BTreeMap<String, String>,
49
50    // Optional template variables defined in frontmatter
51    #[serde(default)]
52    pub vars: BTreeMap<String, String>,
53
54    // Safety and idempotency
55    #[serde(default)]
56    pub backup: Option<bool>,
57    #[serde(default)]
58    pub idempotent: bool,
59
60    // Additional fields for compatibility
61    #[serde(default, deserialize_with = "string_or_seq")]
62    pub shape: Vec<String>,
63    #[serde(default)]
64    pub determinism: Option<serde_yaml::Value>,
65}
66
67pub struct Template {
68    raw_frontmatter: serde_yaml::Value,
69    pub front: Frontmatter, // populated after render_frontmatter()
70    pub body: String,
71}
72
73impl Template {
74    /// Parse frontmatter + body. Does NOT render yet.
75    pub fn parse(input: &str) -> Result<Self> {
76        let matter = Matter::<YAML>::new();
77        let ParsedEntity { data, content, .. } = matter.parse::<serde_yaml::Value>(input)?;
78        let raw_frontmatter = data.unwrap_or(serde_yaml::Value::Null);
79        Ok(Self {
80            raw_frontmatter,
81            front: Frontmatter::default(),
82            body: content,
83        })
84    }
85
86    /// Render frontmatter through Tera once to resolve {{ }} in YAML.
87    pub fn render_frontmatter(&mut self, tera: &mut Tera, vars: &Context) -> Result<()> {
88        let yaml_src = serde_yaml::to_string(&self.raw_frontmatter)?;
89        let rendered_yaml = tera.render_str(&yaml_src, vars)?;
90        self.front = serde_yaml::from_str::<Frontmatter>(&rendered_yaml)?;
91        Ok(())
92    }
93
94    /// Load RDF and run SPARQL using the rendered frontmatter.
95    pub fn process_graph(
96        &mut self, graph: &mut Graph, tera: &mut Tera, vars: &Context, template_path: &std::path::Path,
97    ) -> Result<()> {
98        // Ensure frontmatter is rendered before graph ops
99        if self.front.to.is_none()
100            && self.front.from.is_none()
101            && self.front.rdf_inline.is_empty()
102            && self.front.rdf.is_empty()
103            && self.front.sparql.is_empty()
104        {
105            self.render_frontmatter(tera, vars)?;
106        }
107
108        // Build prolog once
109        let prolog = crate::graph::build_prolog(&self.front.prefixes, self.front.base.as_deref());
110
111        // Insert inline RDF
112        for ttl in &self.front.rdf_inline {
113            let ttl_rendered = tera.render_str(ttl, vars)?;
114            let final_ttl = if prolog.is_empty() {
115                ttl_rendered
116            } else {
117                format!("{prolog}\n{ttl_rendered}")
118            };
119            graph.insert_turtle(&final_ttl)?;
120        }
121
122        // Load RDF files - resolve relative to template directory
123        for rdf_file in &self.front.rdf {
124            let rendered_path = tera.render_str(rdf_file, vars)?;
125            
126            // Resolve relative to template's directory
127            let template_dir = template_path.parent().unwrap_or(std::path::Path::new("."));
128            let rdf_path = template_dir.join(&rendered_path);
129            
130            if let Ok(ttl_content) = std::fs::read_to_string(&rdf_path) {
131                let final_ttl = if prolog.is_empty() {
132                    ttl_content
133                } else {
134                    format!("{prolog}\n{ttl_content}")
135                };
136                graph.insert_turtle(&final_ttl)?;
137            }
138        }
139
140        // Execute SPARQL (prepend PREFIX/BASE prolog)
141        for q in self.front.sparql.values() {
142            let q_rendered = tera.render_str(q, vars)?;
143            let final_q = if prolog.is_empty() {
144                q_rendered
145            } else {
146                format!("{prolog}\n{q_rendered}")
147            };
148            let _ = graph.query(&final_q)?;
149        }
150
151        Ok(())
152    }
153
154    /// Render template body with Tera.
155    /// If `from:` is specified in frontmatter, read that file and use as body source.
156    pub fn render(&self, tera: &mut Tera, vars: &Context) -> Result<String> {
157        let body_source = if let Some(from_path) = &self.front.from {
158            // Render the from path as a template to resolve variables
159            let rendered_from = tera.render_str(from_path, vars)?;
160            std::fs::read_to_string(&rendered_from).map_err(|e| {
161                anyhow::anyhow!("Failed to read from file '{}': {}", rendered_from, e)
162            })?
163        } else {
164            self.body.clone()
165        };
166
167        Ok(tera.render_str(&body_source, vars)?)
168    }
169}
170
171/* ---------------- helpers ---------------- */
172
173// Accept either "rdf: <string>" or "rdf: [<string>, ...]"
174fn string_or_seq<'de, D>(de: D) -> Result<Vec<String>, D::Error>
175where
176    D: serde::Deserializer<'de>,
177{
178    use serde::de::{Error as DeError, SeqAccess, Visitor};
179    use std::fmt;
180
181    struct StrOrSeq;
182
183    impl<'de> Visitor<'de> for StrOrSeq {
184        type Value = Vec<String>;
185
186        fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
187            f.write_str("a string or a sequence of strings")
188        }
189
190        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
191        where
192            E: DeError,
193        {
194            Ok(vec![v.to_string()])
195        }
196
197        fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
198        where
199            E: DeError,
200        {
201            Ok(vec![v])
202        }
203
204        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
205        where
206            A: SeqAccess<'de>,
207        {
208            let mut out = Vec::new();
209            while let Some(s) = seq.next_element::<String>()? {
210                out.push(s);
211            }
212            Ok(out)
213        }
214    }
215
216    de.deserialize_any(StrOrSeq)
217}
218
219// Accept either "sparql: '<query>'" or "sparql: { name: '<query>' }"
220fn sparql_map<'de, D>(de: D) -> Result<BTreeMap<String, String>, D::Error>
221where
222    D: serde::Deserializer<'de>,
223{
224    #[derive(Deserialize)]
225    #[serde(untagged)]
226    enum OneOrMapOrSeq {
227        One(String),
228        Map(BTreeMap<String, String>),
229        Seq(Vec<String>),
230    }
231    match OneOrMapOrSeq::deserialize(de)? {
232        OneOrMapOrSeq::One(q) => {
233            let mut m = BTreeMap::new();
234            m.insert("default".to_string(), q);
235            Ok(m)
236        }
237        OneOrMapOrSeq::Map(m) => Ok(m),
238        OneOrMapOrSeq::Seq(queries) => {
239            let mut m = BTreeMap::new();
240            for (i, query) in queries.into_iter().enumerate() {
241                m.insert(format!("query_{}", i), query);
242            }
243            Ok(m)
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use std::io::Write;
252    use tera::Context;
253
254    #[test]
255    fn test_template_parse_basic() -> Result<()> {
256        let input = r#"---
257to: "{{name}}.rs"
258---
259fn main() {
260    println!("Hello, {{name}}!");
261}"#;
262        let template = Template::parse(input)?;
263
264        assert_eq!(
265            template.body,
266            "fn main() {\n    println!(\"Hello, {{name}}!\");\n}"
267        );
268        assert!(template.front.to.is_none()); // Not rendered yet
269
270        Ok(())
271    }
272
273    #[test]
274    fn test_template_parse_no_frontmatter() -> Result<()> {
275        let input = r#"fn main() {
276    println!("Hello, world!");
277}"#;
278        let template = Template::parse(input)?;
279
280        assert_eq!(
281            template.body,
282            "fn main() {\n    println!(\"Hello, world!\");\n}"
283        );
284
285        Ok(())
286    }
287
288    #[test]
289    fn test_template_parse_empty_frontmatter() -> Result<()> {
290        let input = r#"---
291---
292fn main() {
293    println!("Hello, world!");
294}"#;
295        let template = Template::parse(input)?;
296
297        assert_eq!(
298            template.body,
299            "fn main() {\n    println!(\"Hello, world!\");\n}"
300        );
301
302        Ok(())
303    }
304
305    #[test]
306    fn test_render_frontmatter() -> Result<()> {
307        let input = r#"---
308to: "{{name}}.rs"
309vars:
310  greeting: "Hello"
311---
312fn main() {
313    println!("{{greeting}}, {{name}}!");
314}"#;
315        let mut template = Template::parse(input)?;
316
317        let mut tera = Tera::default();
318        let mut vars = Context::new();
319        vars.insert("name", "Alice");
320
321        template.render_frontmatter(&mut tera, &vars)?;
322
323        assert_eq!(template.front.to, Some("Alice.rs".to_string()));
324        assert_eq!(
325            template.front.vars.get("greeting"),
326            Some(&"Hello".to_string())
327        );
328
329        Ok(())
330    }
331
332    #[test]
333    fn test_render_frontmatter_with_prefixes() -> Result<()> {
334        let input = r#"---
335prefixes:
336  ex: "http://example.org/"
337  rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
338base: "http://example.org/{{namespace}}/"
339---
340fn main() {
341    println!("Hello, world!");
342}"#;
343        let mut template = Template::parse(input)?;
344
345        let mut tera = Tera::default();
346        let mut vars = Context::new();
347        vars.insert("namespace", "test");
348
349        template.render_frontmatter(&mut tera, &vars)?;
350
351        assert_eq!(
352            template.front.prefixes.get("ex"),
353            Some(&"http://example.org/".to_string())
354        );
355        assert_eq!(
356            template.front.prefixes.get("rdf"),
357            Some(&"http://www.w3.org/1999/02/22-rdf-syntax-ns#".to_string())
358        );
359        assert_eq!(
360            template.front.base,
361            Some("http://example.org/test/".to_string())
362        );
363
364        Ok(())
365    }
366
367    #[test]
368    fn test_render_frontmatter_with_rdf_inline() -> Result<()> {
369        let input = r#"---
370rdf_inline:
371  - "@prefix ex: <http://example.org/> . ex:{{name}} a ex:Person ."
372---
373fn main() {
374    println!("Hello, world!");
375}"#;
376        let mut template = Template::parse(input)?;
377
378        let mut tera = Tera::default();
379        let mut vars = Context::new();
380        vars.insert("name", "Alice");
381
382        template.render_frontmatter(&mut tera, &vars)?;
383
384        assert_eq!(template.front.rdf_inline.len(), 1);
385        assert_eq!(
386            template.front.rdf_inline[0],
387            "@prefix ex: <http://example.org/> . ex:Alice a ex:Person ."
388        );
389
390        Ok(())
391    }
392
393    #[test]
394    fn test_render_frontmatter_with_sparql() -> Result<()> {
395        let input = r#"---
396sparql:
397  people: "SELECT ?person WHERE { ?person a ex:Person . ?person ex:name '{{name}}' }"
398---
399fn main() {
400    println!("Hello, world!");
401}"#;
402        let mut template = Template::parse(input)?;
403
404        let mut tera = Tera::default();
405        let mut vars = Context::new();
406        vars.insert("name", "Alice");
407
408        template.render_frontmatter(&mut tera, &vars)?;
409
410        assert_eq!(template.front.sparql.len(), 1);
411        assert_eq!(
412            template.front.sparql.get("people"),
413            Some(
414                &"SELECT ?person WHERE { ?person a ex:Person . ?person ex:name 'Alice' }"
415                    .to_string()
416            )
417        );
418
419        Ok(())
420    }
421
422    #[test]
423    fn test_render_template_body() -> Result<()> {
424        let input = r#"---
425to: "{{name}}.rs"
426---
427fn main() {
428    println!("Hello, {{name}}!");
429}"#;
430        let template = Template::parse(input)?;
431
432        let mut tera = Tera::default();
433        let mut vars = Context::new();
434        vars.insert("name", "Alice");
435
436        let rendered = template.render(&mut tera, &vars)?;
437
438        assert_eq!(rendered, "fn main() {\n    println!(\"Hello, Alice!\");\n}");
439
440        Ok(())
441    }
442
443    #[test]
444    fn test_render_template_with_from() -> Result<()> {
445        use tempfile::NamedTempFile;
446
447        // Create a temporary file with template content
448        let mut temp_file = NamedTempFile::new()?;
449        writeln!(temp_file, "fn main() {{")?;
450        writeln!(temp_file, "    println!(\"Hello, {{{{name}}}}!\");")?;
451        writeln!(temp_file, "}}")?;
452        temp_file.flush()?;
453
454        let input = format!(
455            r#"---
456from: "{}"
457---
458This should be ignored"#,
459            temp_file.path().to_str().unwrap()
460        );
461
462        let mut template = Template::parse(&input)?;
463
464        let mut tera = Tera::default();
465        let mut vars = Context::new();
466        vars.insert("name", "Alice");
467
468        // Render frontmatter first to populate the 'from' field
469        template.render_frontmatter(&mut tera, &vars)?;
470
471        let rendered = template.render(&mut tera, &vars)?;
472
473        assert!(rendered.contains("fn main() {"));
474        assert!(rendered.contains("println!(\"Hello, Alice!\");"));
475        assert!(!rendered.contains("This should be ignored"));
476
477        Ok(())
478    }
479
480    #[test]
481    fn test_process_graph_with_rdf_inline() -> Result<()> {
482        let input = r#"---
483prefixes:
484  ex: "http://example.org/"
485rdf_inline:
486  - "@prefix ex: <http://example.org/> . ex:{{name}} a ex:Person ."
487---
488fn main() {
489    println!("Hello, world!");
490}"#;
491        let mut template = Template::parse(input)?;
492        let mut graph = Graph::new()?;
493
494        let mut tera = Tera::default();
495        let mut vars = Context::new();
496        vars.insert("name", "Alice");
497
498        template.process_graph(&mut graph, &mut tera, &vars, std::path::Path::new("test.tmpl"))?;
499
500        // Check that the RDF was inserted
501        assert!(!graph.is_empty());
502
503        // Query for the inserted data
504        let results = graph.query("SELECT ?s WHERE { ?s a <http://example.org/Person> }")?;
505        if let oxigraph::sparql::QueryResults::Solutions(mut it) = results {
506            let first = it.next().unwrap().unwrap();
507            let s = first.get("s").unwrap().to_string();
508            assert_eq!(s, "<http://example.org/Alice>");
509        } else {
510            return Err(anyhow::anyhow!("Expected Solutions results"));
511        }
512
513        Ok(())
514    }
515
516    #[test]
517    fn test_process_graph_with_sparql() -> Result<()> {
518        let input = r#"---
519prefixes:
520  ex: "http://example.org/"
521rdf_inline:
522  - "@prefix ex: <http://example.org/> . ex:alice a ex:Person . ex:bob a ex:Person ."
523sparql:
524  count_people: "SELECT (COUNT(?person) AS ?count) WHERE { ?person a ex:Person }"
525---
526fn main() {
527    println!("Hello, world!");
528}"#;
529        let mut template = Template::parse(input)?;
530        let mut graph = Graph::new()?;
531
532        let mut tera = Tera::default();
533        let vars = Context::new();
534
535        template.process_graph(&mut graph, &mut tera, &vars, std::path::Path::new("test.tmpl"))?;
536
537        // Check that the RDF was inserted
538        assert!(!graph.is_empty());
539
540        // The SPARQL query should have been executed (though we can't easily test the result)
541        // But we can verify the graph has the expected data
542        let results = graph.query("SELECT ?s WHERE { ?s a <http://example.org/Person> }")?;
543        if let oxigraph::sparql::QueryResults::Solutions(it) = results {
544            let count = it.count();
545            assert_eq!(count, 2); // alice and bob
546        } else {
547            return Err(anyhow::anyhow!("Expected Solutions results"));
548        }
549
550        Ok(())
551    }
552
553    #[test]
554    fn test_string_or_seq_deserializer() -> Result<()> {
555        // Test string input
556        let yaml_str = r#"rdf_inline: "single string""#;
557        let frontmatter: Frontmatter = serde_yaml::from_str(yaml_str)?;
558        assert_eq!(frontmatter.rdf_inline, vec!["single string"]);
559
560        // Test array input
561        let yaml_array = r#"rdf_inline: ["string1", "string2"]"#;
562        let frontmatter: Frontmatter = serde_yaml::from_str(yaml_array)?;
563        assert_eq!(frontmatter.rdf_inline, vec!["string1", "string2"]);
564
565        Ok(())
566    }
567
568    #[test]
569    fn test_sparql_map_deserializer() -> Result<()> {
570        // Test single string input
571        let yaml_str = r#"sparql: "SELECT ?s WHERE { ?s ?p ?o }""#;
572        let frontmatter: Frontmatter = serde_yaml::from_str(yaml_str)?;
573        assert_eq!(
574            frontmatter.sparql.get("default"),
575            Some(&"SELECT ?s WHERE { ?s ?p ?o }".to_string())
576        );
577
578        // Test map input
579        let yaml_map = r#"sparql:
580  query1: "SELECT ?s WHERE { ?s ?p ?o }"
581  query2: "SELECT ?o WHERE { ?s ?p ?o }""#;
582        let frontmatter: Frontmatter = serde_yaml::from_str(yaml_map)?;
583        assert_eq!(
584            frontmatter.sparql.get("query1"),
585            Some(&"SELECT ?s WHERE { ?s ?p ?o }".to_string())
586        );
587        assert_eq!(
588            frontmatter.sparql.get("query2"),
589            Some(&"SELECT ?o WHERE { ?s ?p ?o }".to_string())
590        );
591
592        // Test array input
593        let yaml_array =
594            r#"sparql: ["SELECT ?s WHERE { ?s ?p ?o }", "SELECT ?o WHERE { ?s ?p ?o }"]"#;
595        let frontmatter: Frontmatter = serde_yaml::from_str(yaml_array)?;
596        assert_eq!(
597            frontmatter.sparql.get("query_0"),
598            Some(&"SELECT ?s WHERE { ?s ?p ?o }".to_string())
599        );
600        assert_eq!(
601            frontmatter.sparql.get("query_1"),
602            Some(&"SELECT ?o WHERE { ?s ?p ?o }".to_string())
603        );
604
605        Ok(())
606    }
607
608    #[test]
609    fn test_frontmatter_defaults() -> Result<()> {
610        let yaml_str = r#"to: "test.rs""#;
611        let frontmatter: Frontmatter = serde_yaml::from_str(yaml_str)?;
612
613        // Test default values
614        assert_eq!(frontmatter.force, false);
615        assert_eq!(frontmatter.unless_exists, false);
616        assert_eq!(frontmatter.inject, false);
617        assert_eq!(frontmatter.prepend, false);
618        assert_eq!(frontmatter.append, false);
619        assert_eq!(frontmatter.eof_last, false);
620        assert_eq!(frontmatter.idempotent, false);
621        assert!(frontmatter.prefixes.is_empty());
622        assert!(frontmatter.rdf_inline.is_empty());
623        assert!(frontmatter.rdf.is_empty());
624        assert!(frontmatter.sparql.is_empty());
625        assert!(frontmatter.vars.is_empty());
626        assert!(frontmatter.shape.is_empty());
627
628        Ok(())
629    }
630
631    #[test]
632    fn test_frontmatter_boolean_fields() -> Result<()> {
633        let yaml_str = r#"force: true
634unless_exists: true
635inject: true
636prepend: true
637append: true
638eof_last: true
639idempotent: true"#;
640        let frontmatter: Frontmatter = serde_yaml::from_str(yaml_str)?;
641
642        assert_eq!(frontmatter.force, true);
643        assert_eq!(frontmatter.unless_exists, true);
644        assert_eq!(frontmatter.inject, true);
645        assert_eq!(frontmatter.prepend, true);
646        assert_eq!(frontmatter.append, true);
647        assert_eq!(frontmatter.eof_last, true);
648        assert_eq!(frontmatter.idempotent, true);
649
650        Ok(())
651    }
652
653    #[test]
654    fn test_frontmatter_injection_fields() -> Result<()> {
655        let yaml_str = r#"inject: true
656before: "// Before comment"
657after: "// After comment"
658at_line: 5
659skip_if: "existing code""#;
660        let frontmatter: Frontmatter = serde_yaml::from_str(yaml_str)?;
661
662        assert_eq!(frontmatter.inject, true);
663        assert_eq!(frontmatter.before, Some("// Before comment".to_string()));
664        assert_eq!(frontmatter.after, Some("// After comment".to_string()));
665        assert_eq!(frontmatter.at_line, Some(5));
666        assert_eq!(frontmatter.skip_if, Some("existing code".to_string()));
667
668        Ok(())
669    }
670
671    #[test]
672    fn test_frontmatter_shell_hooks() -> Result<()> {
673        let yaml_str = r#"sh_before: "echo Before generation"
674sh_after: "echo After generation""#;
675        let frontmatter: Frontmatter = serde_yaml::from_str(yaml_str)?;
676
677        assert_eq!(
678            frontmatter.sh_before,
679            Some("echo Before generation".to_string())
680        );
681        assert_eq!(
682            frontmatter.sh_after,
683            Some("echo After generation".to_string())
684        );
685
686        Ok(())
687    }
688
689    #[test]
690    fn test_frontmatter_sh_alias() -> Result<()> {
691        let yaml_str = r#"sh: "echo Shell hook""#;
692        let frontmatter: Frontmatter = serde_yaml::from_str(yaml_str)?;
693
694        assert_eq!(frontmatter.sh_before, Some("echo Shell hook".to_string()));
695
696        Ok(())
697    }
698}