modrpc_codegen/codegen/rust/
project.rs

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