Skip to main content

zynk_codegen/
lib.rs

1//! Pure in-memory interfaces shared by Zynk code generators.
2//!
3//! This crate defines generator-neutral data structures and traits only. Callers
4//! own all filesystem, process, and network side effects.
5
6use std::collections::BTreeMap;
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use zynk_schema::ApiGraph;
12
13/// Inputs made available to a code generator invocation.
14#[derive(Debug, Clone, Copy)]
15pub struct GenerationContext<'a> {
16    /// Canonical API graph to generate from.
17    pub graph: &'a ApiGraph,
18    /// Generator-specific options, interpreted by concrete generators.
19    pub options: &'a Value,
20}
21
22impl<'a> GenerationContext<'a> {
23    /// Create a generation context for an API graph and arbitrary options.
24    pub fn new(graph: &'a ApiGraph, options: &'a Value) -> Self {
25        Self { graph, options }
26    }
27}
28
29/// One generated file, represented entirely in memory.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct GeneratedFile {
32    /// Relative output path chosen by the generator.
33    pub path: PathBuf,
34    /// Complete UTF-8 source contents for the generated file.
35    pub contents: String,
36}
37
38impl GeneratedFile {
39    /// Create an in-memory generated file.
40    pub fn new(path: impl Into<PathBuf>, contents: impl Into<String>) -> Self {
41        Self {
42            path: path.into(),
43            contents: contents.into(),
44        }
45    }
46}
47
48/// In-memory output from a generator invocation.
49#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
50pub struct GenerationResult {
51    /// Generated files. The caller decides whether and where to write them.
52    pub files: Vec<GeneratedFile>,
53}
54
55impl GenerationResult {
56    /// Create a generation result from in-memory files.
57    pub fn new(files: Vec<GeneratedFile>) -> Self {
58        Self { files }
59    }
60}
61
62/// A pure code generator from an API graph to generated files.
63pub trait Generator {
64    /// Generate files in memory from the provided context.
65    fn generate(&self, ctx: &GenerationContext) -> GenerationResult;
66}
67
68/// Name-based registry for generator implementations.
69#[derive(Default)]
70pub struct GeneratorRegistry {
71    generators: BTreeMap<String, Box<dyn Generator>>,
72}
73
74impl GeneratorRegistry {
75    /// Create an empty generator registry.
76    pub fn new() -> Self {
77        Self::default()
78    }
79
80    /// Register a generator by name, replacing and returning any previous entry.
81    pub fn register(
82        &mut self,
83        name: impl Into<String>,
84        generator: Box<dyn Generator>,
85    ) -> Option<Box<dyn Generator>> {
86        self.generators.insert(name.into(), generator)
87    }
88
89    /// Look up a generator by name.
90    pub fn get(&self, name: &str) -> Option<&dyn Generator> {
91        self.generators.get(name).map(Box::as_ref)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use std::path::PathBuf;
98
99    use serde_json::json;
100    use zynk_schema::{ApiGraph, Endpoint, EndpointKind, TypeRef};
101
102    use super::{GeneratedFile, GenerationContext, GenerationResult, Generator, GeneratorRegistry};
103
104    struct TinyGenerator;
105
106    impl Generator for TinyGenerator {
107        fn generate(&self, ctx: &GenerationContext) -> GenerationResult {
108            let endpoint_count = ctx.graph.endpoints.len();
109            let suffix = ctx
110                .options
111                .get("suffix")
112                .and_then(serde_json::Value::as_str)
113                .unwrap_or("txt");
114
115            GenerationResult::new(vec![GeneratedFile::new(
116                PathBuf::from(format!("api.{suffix}")),
117                format!("endpoints={endpoint_count}"),
118            )])
119        }
120    }
121
122    #[test]
123    fn generator_trait_uses_context_and_returns_generated_files() {
124        let mut graph = ApiGraph::new();
125        graph.insert_endpoint(Endpoint::new("ping", EndpointKind::Rpc, TypeRef::void()));
126        let options = json!({ "suffix": "ts" });
127        let ctx = GenerationContext::new(&graph, &options);
128
129        let result = TinyGenerator.generate(&ctx);
130
131        assert_eq!(result.files.len(), 1);
132        assert_eq!(result.files[0].path, PathBuf::from("api.ts"));
133        assert_eq!(result.files[0].contents, "endpoints=1");
134    }
135
136    #[test]
137    fn registry_registers_and_looks_up_generators_by_name() {
138        let mut registry = GeneratorRegistry::new();
139        assert!(registry.get("typescript").is_none());
140
141        assert!(registry
142            .register("typescript", Box::new(TinyGenerator))
143            .is_none());
144
145        let graph = ApiGraph::new();
146        let options = json!({ "suffix": "txt" });
147        let ctx = GenerationContext::new(&graph, &options);
148        let generator = registry.get("typescript").expect("registered generator");
149        let result = generator.generate(&ctx);
150
151        assert_eq!(
152            result.files,
153            vec![GeneratedFile::new("api.txt", "endpoints=0")]
154        );
155        assert!(registry.get("effect").is_none());
156    }
157}