1mod rust;
2mod rust_axum;
3mod openapi;
4mod openapi_utils;
5mod utils;
6mod ts;
7mod ts_nestjs;
8mod python;
9mod tauri;
10mod rust_utils;
11mod python_fastapi;
12
13use std::{rc::Rc, cell::{RefCell}, collections::{HashMap, HashSet}, path::{Path, PathBuf}, error::Error, fs::{self, OpenOptions, File}, io::Write};
14
15use openapi::OpenAPIGenerator;
16use rust::RustGenerator;
17use rust_axum::RustAxumGenerator;
18use cronus_spec::{RawSchema, RawSpec, RawUsecase, RawUsecaseMethod};
19use tauri::TauriGenerator;
20use tracing::info;
21use ts::TypescriptGenerator;
22use ts_nestjs::TypescriptNestjsGenerator;
23use anyhow::{bail, Context as _, Ok, Result};
24
25type GeneratorFileSystem = Rc<RefCell<HashMap<String, String>>>;
27
28pub struct Context {
29 pub generator_fs: RefCell<HashMap<&'static str, GeneratorFileSystem>>,
30 pub spec: RawSpec,
31}
32
33impl Context {
34 pub fn new(spec: RawSpec) -> Self {
35 Self {
36 generator_fs: RefCell::new(HashMap::new()),
37 spec,
38 }
39 }
40
41 pub fn get_gfs(&self, name: &'static str) -> GeneratorFileSystem {
42 if self.generator_fs.borrow().contains_key(name) {
43 self.generator_fs.borrow().get(name).unwrap().clone()
44 } else {
45 self.init_gfs(name)
46 }
47 }
48
49 fn init_gfs(&self, name: &'static str) -> GeneratorFileSystem {
50 let fs = Rc::new(RefCell::new(HashMap::new()));
51 self.generator_fs.borrow_mut().insert(name, fs.clone());
52 fs
53 }
54
55 pub fn append_file(&self, name:&'static str, path:&str, content: &str) {
56 let fs = self.get_gfs(name);
57 let mut mutated_fs = fs.borrow_mut();
58 match mutated_fs.get_mut(path) {
59 Some(f) => {
60 f.push_str(&content);
61 },
62 None => {
63 mutated_fs.insert(path.to_string(), content.to_string());
64 },
65 };
66
67 }
68
69
70
71 pub fn dump(&self) -> Result<()> {
75 let mut touched_files: HashSet<String> = Default::default();
76
77 for (g, fs) in self.generator_fs.borrow().iter() {
78 for (path, contents) in fs.borrow().iter() {
79 let pb = PathBuf::from(path);
80 let par = pb.parent().unwrap();
81 if !par.exists() {
82 std::fs::create_dir_all(par)?;
83 }
84 let mut file: File;
85 if touched_files.contains(path) {
86 file = OpenOptions::new()
87 .write(true)
88 .append(true)
89 .create(true)
90 .open(path).context(format!("failed to open {}", path))?;
91 } else {
92 file = OpenOptions::new()
93 .write(true)
94 .create(true)
95 .truncate(true)
96 .open(path).context(format!("failed to open {}", path))?;
97 touched_files.insert(path.to_string());
98 }
99
100 file.write_all(contents.as_bytes())?;
101 info!("[+] {}", path);
102
103 }
104 }
105 Ok(())
106 }
107
108}
109
110
111#[derive(Clone)]
112pub struct Ctxt(std::sync::Arc<Context>);
113
114impl std::ops::Deref for Ctxt {
115 type Target = Context;
116
117 fn deref(&self) -> &Self::Target {
118 self.0.as_ref()
119 }
120}
121
122impl Ctxt {
123 pub fn new(spec: RawSpec) -> Self {
124 Self(std::sync::Arc::new( Context::new(spec)))
125 }
126}
127
128pub trait Generator {
129 fn name(&self) -> &'static str;
130 fn before_all(&self, _ctx: &Ctxt) -> Result<()> {
131 Ok(())
132 }
133 fn after_all(&self, _ctx: &Ctxt) -> Result<()> {
134 Ok(())
135 }
136 fn generate_schema(&self, _ctx: &Ctxt, _schema_name:&str, _schema: &RawSchema)-> Result<()> {
137 Ok(())
138 }
139 fn generate_usecase(&self, _ctx: &Ctxt, _usecase_name: &str, _usecase: &RawUsecase) -> Result<()> {
140 Ok(())
141 }
142}
143
144pub fn generate(ctx: &Ctxt) -> Result<()> {
145 let generators:Vec<Rc<dyn Generator>> = vec![
146 Rc::new(RustGenerator::new()),
147 Rc::new(RustAxumGenerator::new()),
148 Rc::new(OpenAPIGenerator::new()),
149 Rc::new(TypescriptGenerator::new()),
150 Rc::new(TypescriptNestjsGenerator::new()),
151 Rc::new(TauriGenerator::new()),
152 Rc::new(python::PythonGenerator::new()),
153 Rc::new(python_fastapi::PythonFastApiGenerator::new()),
154 ];
155 let mut generator_map: HashMap<&str, Rc<dyn Generator>> = HashMap::new();
156 generators
157 .iter()
158 .for_each(|g| {
159 generator_map.insert(g.name(), g.clone());
160 });
161
162
163 if ctx.spec.option.is_none() {
164 info!("No generator(s) is configured.");
165 } else {
166 if let Some(generator) = &ctx.spec.option.as_ref().unwrap().generator {
167
168 let json_value = serde_yaml::to_value(generator).expect("Failed to serialize");
169
170 if let serde_yaml::Value::Mapping(map) = &json_value {
171 for (generator_name, config) in map {
172 if config.is_null(){
173 continue;
174 }
175 match generator_map.get(generator_name.as_str().unwrap()) {
176 Some(g) => {
177 run_generator(g.as_ref(), ctx)?;
178 },
179 None => {
180 bail!("Cannot find generator '{}'", generator_name.as_str().unwrap())
181 },
182 }
183
184 }
185 }
186
187 } else {
188 info!("No generator(s) is configured.");
189 }
190 }
191 Ok(())
192
193}
194
195pub fn run_generator(g: &dyn Generator, ctx: &Ctxt) -> Result<()> {
196 g.before_all(ctx)?;
197 let schema_items = ctx.spec
198 .ty
199 .iter()
200 .flat_map(|t| t.iter());
201
202 for (name, schema) in schema_items {
203 g.generate_schema(ctx, name,schema)?
204 }
205
206
207 let usecase_items = ctx.spec
208 .usecases
209 .iter()
210 .flat_map(|m| m.iter());
211
212 for (name, usecase) in usecase_items {
213 g.generate_usecase(ctx, name, usecase)?
214 }
215
216
217 g.after_all(ctx)
218
219}
220
221
222#[cfg(test)]
223mod test {
224 use std::{collections::HashSet, path::{Path, PathBuf}, process::Command};
225
226 use cronus_spec::RawSpec;
227 use anyhow::{bail, Result};
228 use crate::{generate, Context, Ctxt};
229
230
231 #[test]
232 fn context_get_files_by_generator(){
233 let ctx = Context::new(RawSpec::new());
234 ctx.init_gfs("abcde");
235 ctx.get_gfs("abcde");
236 }
237
238 #[test]
239 fn context_append_file(){
240 let ctx = Context::new( RawSpec::new());
241 ctx.init_gfs("agenerator");
242
243 ctx.append_file("agenerator", "src/lib.rs", "hello");
244 }
245
246 fn get_cargo_manifest_dir() -> Option<PathBuf> {
247 std::env::var("CARGO_MANIFEST_DIR").ok().map(PathBuf::from)
248 }
249
250 #[test]
251 fn e2e_hello_rust() -> Result<()> {
252 let proj_dir = get_cargo_manifest_dir().unwrap().join("testdata").join("hello").join("rust");
253 let spec_file = proj_dir.join("main.api");
254 let mut explored = HashSet::new();
255 let spec = cronus_parser::from_file(&spec_file, true, None, &mut explored)?;
256 let ctx = Ctxt::new(spec);
257 generate(&ctx)?;
258 run_cargo_check(&proj_dir)
259 }
260
261 #[test]
262 fn e2e_hello_rust_axum() -> Result<()> {
263 let proj_dir = get_cargo_manifest_dir().unwrap().join("testdata").join("hello").join("rust_axum");
264 let spec_file = proj_dir.join("main.api");
265 let mut explored = HashSet::new();
266 let spec = cronus_parser::from_file(&spec_file, true, None, &mut explored)?;
267 let ctx = Ctxt::new(spec);
268 generate(&ctx)?;
269 run_cargo_check(&proj_dir)
270 }
271
272 fn run_cargo_check(dir: &Path) -> Result<()> {
273 let output = Command::new("cargo")
274 .arg("check")
275 .current_dir(dir)
276 .output()?;
277
278 if !output.status.success() {
279 bail!("Stdout: {}\nStderr: {}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr))
280 }
281
282 Ok(())
283 }
284}