1use std::collections::BTreeMap;
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use zynk_schema::ApiGraph;
12
13#[derive(Debug, Clone, Copy)]
15pub struct GenerationContext<'a> {
16 pub graph: &'a ApiGraph,
18 pub options: &'a Value,
20}
21
22impl<'a> GenerationContext<'a> {
23 pub fn new(graph: &'a ApiGraph, options: &'a Value) -> Self {
25 Self { graph, options }
26 }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct GeneratedFile {
32 pub path: PathBuf,
34 pub contents: String,
36}
37
38impl GeneratedFile {
39 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#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
50pub struct GenerationResult {
51 pub files: Vec<GeneratedFile>,
53}
54
55impl GenerationResult {
56 pub fn new(files: Vec<GeneratedFile>) -> Self {
58 Self { files }
59 }
60}
61
62pub trait Generator {
64 fn generate(&self, ctx: &GenerationContext) -> GenerationResult;
66}
67
68#[derive(Default)]
70pub struct GeneratorRegistry {
71 generators: BTreeMap<String, Box<dyn Generator>>,
72}
73
74impl GeneratorRegistry {
75 pub fn new() -> Self {
77 Self::default()
78 }
79
80 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 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}