cronus_generator/
lib.rs

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
28/// relative path => file content
29type 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    /// Write the results/files of the generator to the disk
75    /// 
76    /// 
77    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}