Skip to main content

tatara_nix/
synth.rs

1//! Synthesizer traits — the universal rendering interface.
2//!
3//! Trait surface mirrors `arch-synthesizer::traits` (MIT-licensed sibling
4//! crate at `pleme-io/arch-synthesizer`): `Synthesizer` for
5//! Input→AST→Output morphisms, `MultiSynthesizer` for multi-file emission,
6//! `Artifact` for a path+content pair. We restate them here (rather than
7//! importing arch-synthesizer) to avoid pulling its path-dep chain
8//! (nix-synthesizer, yaml-synthesizer, helm-synthesizer, …) into every
9//! consumer of tatara-nix.
10//!
11//! Any tatara-lisp-authored domain can implement `Synthesizer` to plug into
12//! the arch-synthesizer rendering pipeline without friction — the trait
13//! shapes line up exactly.
14
15/// Universal rendering morphism: proven input → AST → emitted output.
16/// Deterministic, total, composable.
17pub trait Synthesizer {
18    type Input;
19    type Ast;
20    type Output;
21
22    fn synthesize(&self, input: &Self::Input) -> Self::Ast;
23    fn render(&self, ast: &Self::Ast) -> Self::Output;
24
25    fn generate(&self, input: &Self::Input) -> Self::Output {
26        let ast = self.synthesize(input);
27        self.render(&ast)
28    }
29}
30
31/// Multi-file synthesizer — emits a set of (path, content) pairs.
32/// `Input: ?Sized` lets concrete callers pass trait objects (e.g., a
33/// `&dyn PackageSet`) as input.
34pub trait MultiSynthesizer {
35    type Input: ?Sized;
36    fn generate_all(&self, input: &Self::Input) -> Vec<Artifact>;
37}
38
39/// One emitted file.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Artifact {
42    pub path: String,
43    pub content: String,
44}
45
46impl Artifact {
47    pub fn new(path: impl Into<String>, content: impl Into<String>) -> Self {
48        Self {
49            path: path.into(),
50            content: content.into(),
51        }
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    struct Upper;
60    impl Synthesizer for Upper {
61        type Input = String;
62        type Ast = String;
63        type Output = String;
64        fn synthesize(&self, s: &String) -> String {
65            s.to_uppercase()
66        }
67        fn render(&self, ast: &String) -> String {
68            format!("// rendered\n{ast}")
69        }
70    }
71
72    #[test]
73    fn generate_composes_synthesize_then_render() {
74        let u = Upper;
75        assert_eq!(u.generate(&"hello".into()), "// rendered\nHELLO");
76    }
77
78    struct TwoFile;
79    impl MultiSynthesizer for TwoFile {
80        type Input = &'static str;
81        fn generate_all(&self, input: &&'static str) -> Vec<Artifact> {
82            vec![
83                Artifact::new("a.txt", input.to_string()),
84                Artifact::new("b.txt", input.chars().rev().collect::<String>()),
85            ]
86        }
87    }
88
89    #[test]
90    fn multi_synthesizer_emits_several_artifacts() {
91        let out = TwoFile.generate_all(&"hi");
92        assert_eq!(out.len(), 2);
93        assert_eq!(out[0].path, "a.txt");
94        assert_eq!(out[0].content, "hi");
95        assert_eq!(out[1].content, "ih");
96    }
97}