Skip to main content

modrpc_codegen/codegen/rust/
project.rs

1use std::path::Path;
2use std::fs::File;
3use std::io::Write;
4
5use genco::quote;
6use mproto_codegen::codegen::name_util::camel_to_snake_case;
7
8use crate::{
9    ast::Schema,
10    codegen,
11    Database,
12};
13
14const PROTO_CARGO_TOML: &'static str = include_str!("templates/interface/cargo.toml");
15const PROTO_LIB_RS: &'static str = include_str!("templates/interface/lib.rs");
16
17pub fn rust_project_gen(
18    root_dir: impl AsRef<Path>,
19    project_name: &str,
20    schema: &Schema,
21) -> std::io::Result<()> {
22    let pkg_name = &format!("{}-modrpc", project_name);
23    let pkg_root = root_dir.as_ref().join(pkg_name).join("rust");
24
25    std::fs::create_dir_all(&pkg_root)?;
26
27    let _db = rust_proto_package_gen(&pkg_root, pkg_name, schema)?;
28
29    Ok(())
30}
31
32pub fn rust_proto_package_gen(
33    pkg_root: impl AsRef<Path>,
34    pkg_name: &str,
35    schema: &Schema,
36) -> std::io::Result<Database> {
37    let pkg_root = pkg_root.as_ref();
38    let local_mproto_module = mproto_codegen::Module::from_type_defs(
39        schema.type_defs.clone(),
40    );
41    let mproto_db = mproto_codegen::Database::new(local_mproto_module);
42
43    let mut db = Database::new(mproto_db);
44    codegen::load_imports_recursive(&mut db, schema).unwrap();
45
46    for interface in &schema.interfaces {
47        let _ = db.local().add_interface(interface.clone());
48
49        // Define per-interface *InitState and per-role *Config structs
50        codegen::define_interface_config_and_state_structs(
51            &mut db,
52            interface,
53        );
54    }
55
56    let src_dir = pkg_root.join("src");
57    let roles_dir = src_dir.join("roles");
58    let role_impls_dir = src_dir.join("role_impls");
59
60    std::fs::create_dir_all(&pkg_root)?;
61    std::fs::create_dir_all(&src_dir)?;
62    std::fs::create_dir_all(&roles_dir)?;
63    std::fs::create_dir_all(&role_impls_dir)?;
64
65    // Write Cargo.toml
66    let cargo_toml_path = pkg_root.join("Cargo.toml");
67    if !cargo_toml_path.exists() {
68        let mut cargo_toml_file = File::create(cargo_toml_path)?;
69        cargo_toml_file.write_all(
70            PROTO_CARGO_TOML.replace("PKG_NAME", pkg_name).as_bytes()
71        )?;
72    } else {
73        println!(
74            "{} already exists - skipping as it might contain hand-written code.",
75            cargo_toml_path.display(),
76        );
77    }
78
79    // Write lib.rs
80    let lib_rs_path = src_dir.join("lib.rs");
81    if !lib_rs_path.exists() {
82        let mut lib_rs_file = File::create(&lib_rs_path)?;
83        lib_rs_file.write_all(PROTO_LIB_RS.as_bytes())?;
84    } else {
85        println!(
86            "{} already exists - skipping as it might contain hand-written code.",
87            lib_rs_path.display(),
88        );
89    }
90
91    // Write proto.rs
92    let fmt = genco::fmt::Config::from_lang::<genco::lang::Rust>()
93        .with_indentation(genco::fmt::Indentation::Space(4));
94    let config = genco::lang::rust::Config::default();
95    let proto_rust_file = File::create(src_dir.join("proto.rs"))?;
96    let mut w = genco::fmt::IoWriter::new(proto_rust_file);
97    let mut tokens = genco::lang::rust::Tokens::new();
98
99    for type_def in db.mproto_db().local().type_defs() {
100        let type_def_tokens = mproto_codegen::codegen::rust::rust_type_def(
101            &mproto_codegen::codegen::CodegenCx::new(db.mproto_db(), None, true),
102            type_def,
103        );
104        tokens = quote! {
105            $tokens
106
107            $type_def_tokens
108        };
109    }
110
111    tokens
112        .format_file(&mut w.as_formatter(&fmt), &config)
113        .expect("format proto.rs file");
114
115    // Write interface.rs
116    let fmt = genco::fmt::Config::from_lang::<genco::lang::Rust>()
117        .with_indentation(genco::fmt::Indentation::Space(4));
118    let config = genco::lang::rust::Config::default();
119    let interface_rust_file = File::create(src_dir.join("interface.rs"))?;
120    let mut w = genco::fmt::IoWriter::new(interface_rust_file);
121    let mut tokens = genco::lang::rust::Tokens::new();
122
123    for interface in &schema.interfaces {
124        let interface_tokens = codegen::rust::rust_interface(&db, interface);
125        tokens = quote! {
126            $tokens
127
128            $interface_tokens
129        };
130    }
131
132    tokens
133        .format_file(&mut w.as_formatter(&fmt), &config)
134        .expect("format interface.rs file");
135
136    // Write role files
137    let mut roles_mod_tokens = quote! { };
138    for interface in &schema.interfaces {
139        for role_name in &interface.roles {
140            let snake_interface_role_name = camel_to_snake_case(
141                &format!("{}{}", interface.name, role_name)
142            );
143            write_rust_file(
144                &roles_dir.join(format!("{}.rs", snake_interface_role_name)),
145                &codegen::rust::rust_interface_role(&db, interface, role_name),
146                |f| {
147                    f.write_all(b"#![allow(unused_variables)]\n\n")
148                },
149            )?;
150
151            roles_mod_tokens = quote! {
152                mod $(&snake_interface_role_name);
153                pub use $(&snake_interface_role_name)::*;
154                $roles_mod_tokens
155            };
156        }
157    }
158    write_rust_file(&roles_dir.join("mod.rs"), &roles_mod_tokens, |_| Ok(()))?;
159
160    // Write role impls file
161    let mut role_impls_mod_tokens = quote! { };
162    for interface in &schema.interfaces {
163        for role_name in &interface.roles {
164            let snake_interface_role_name = camel_to_snake_case(
165                &format!("{}{}", interface.name, role_name)
166            );
167            let path = role_impls_dir.join(&format!("{}.rs", snake_interface_role_name));
168            let tokens = &codegen::rust::rust_interface_role_impl(&db, interface, role_name);
169
170            if !path.exists() {
171                if !tokens.is_empty() {
172                    write_rust_file(&path, tokens, |_| Ok(()))?;
173                }
174            } else {
175                println!(
176                    "{} already exists - skipping as it might contain hand-written code.",
177                    path.display(),
178                );
179            }
180
181            if !tokens.is_empty() {
182                role_impls_mod_tokens = quote! {
183                    mod $(&snake_interface_role_name);
184                    pub use $(&snake_interface_role_name)::*;
185                    $role_impls_mod_tokens
186                };
187            }
188        }
189    }
190    write_rust_file(&role_impls_dir.join("mod.rs"), &role_impls_mod_tokens, |_| Ok(()))?;
191
192    Ok(db)
193}
194
195fn write_rust_file(
196    path: impl AsRef<Path> + std::fmt::Debug,
197    tokens: &genco::lang::rust::Tokens,
198    prepend: impl FnOnce(&mut File) -> std::io::Result<()>,
199) -> std::io::Result<()> {
200    let fmt = genco::fmt::Config::from_lang::<genco::lang::Rust>()
201        .with_indentation(genco::fmt::Indentation::Space(4));
202    let config = genco::lang::rust::Config::default();
203    let mut file = File::create(&path)?;
204    prepend(&mut file)?;
205    let mut w = genco::fmt::IoWriter::new(file);
206
207    tokens
208        .format_file(&mut w.as_formatter(&fmt), &config)
209        .expect(&format!("format {:?} file", path));
210    
211    Ok(())
212}
213
214pub fn rust_role_impl_gen(
215    project_name: &str,
216    root_dir: impl AsRef<Path>,
217    schema: &Schema,
218    role_name: &str,
219) -> std::io::Result<()> {
220    let local_mproto_module = mproto_codegen::Module::from_type_defs(
221        schema.type_defs.clone(),
222    );
223    let mproto_db = mproto_codegen::Database::new(local_mproto_module);
224
225    let pkg_name = format!("{}-{}", project_name, &role_name.to_lowercase());
226
227    let pkg_root = root_dir.as_ref().join(pkg_name);
228    let src_dir = pkg_root.join("src");
229
230    std::fs::create_dir_all(&pkg_root)?;
231    std::fs::create_dir_all(&src_dir)?;
232
233    // Write Cargo.toml
234    let mut cargo_toml_file = File::create(pkg_root.join("Cargo.toml"))?;
235    cargo_toml_file.write_all(
236        include_str!("templates/server/cargo.toml").replace("PKG_NAME", project_name).as_bytes()
237    )?;
238
239    // Write lib.rs
240    let fmt = genco::fmt::Config::from_lang::<genco::lang::Rust>()
241        .with_indentation(genco::fmt::Indentation::Space(4));
242    let config = genco::lang::rust::Config::default();
243    let mut main_rs_file = File::create(src_dir.join("main.rs"))?;
244
245    main_rs_file.write("\
246        #![feature(type_alias_impl_trait)]\n\
247        \n\
248        use std::future::Future;\n\
249        \n".as_bytes())?;
250
251    let mut w = genco::fmt::IoWriter::new(main_rs_file);
252
253    let interface = &schema.interfaces[0];
254    let role_impl = codegen::rust::RustInterfaceImpl::generate(
255        &mproto_db, project_name, interface, role_name,
256    );
257
258    role_impl.main_rs
259        .format_file(&mut w.as_formatter(&fmt), &config)
260        .expect("format main.rs file");
261
262    let handlers_dir = src_dir.join("handlers");
263    std::fs::create_dir_all(&handlers_dir)?;
264    for (handler_name, handler_impl_tokens) in role_impl.handlers {
265        write_rust_file(&handlers_dir.join(&format!("{handler_name}.rs")), &handler_impl_tokens, |_| Ok(()))?;
266    }
267
268    Ok(())
269}