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 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 #[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 #[serde(alias = "sh")]
35 pub sh_before: Option<String>,
36 pub sh_after: Option<String>,
37
38 #[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>, #[serde(default, deserialize_with = "sparql_map")]
48 pub sparql: BTreeMap<String, String>,
49
50 #[serde(default)]
52 pub vars: BTreeMap<String, String>,
53
54 #[serde(default)]
56 pub backup: Option<bool>,
57 #[serde(default)]
58 pub idempotent: bool,
59
60 #[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, pub body: String,
71}
72
73impl Template {
74 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 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 pub fn process_graph(
96 &mut self, graph: &mut Graph, tera: &mut Tera, vars: &Context, template_path: &std::path::Path,
97 ) -> Result<()> {
98 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 let prolog = crate::graph::build_prolog(&self.front.prefixes, self.front.base.as_deref());
110
111 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 for rdf_file in &self.front.rdf {
124 let rendered_path = tera.render_str(rdf_file, vars)?;
125
126 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 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 pub fn render(&self, tera: &mut Tera, vars: &Context) -> Result<String> {
157 let body_source = if let Some(from_path) = &self.front.from {
158 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
171fn 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
219fn 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()); 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 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 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 assert!(!graph.is_empty());
502
503 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 assert!(!graph.is_empty());
539
540 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); } 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 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 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 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 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 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 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}