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