cronus_generator/
lib.rs

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
25/// relative path => file content
26type 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    /// Write the results/files of the generator to the disk
72    /// 
73    /// 
74    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}