diff --git a/.gitignore b/.gitignore index cabfbbc..18b2942 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ test_types_gen_project test_map_gen_project test_grpc_project artifacts/ +temp_test_project/ diff --git a/Cargo.lock b/Cargo.lock index bf461d3..bba7556 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1352,6 +1352,23 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "temp_test_project" +version = "0.1.0" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "roto-codegen", + "roto-runtime", + "roto-tonic", + "tokio-stream", + "tonic", + "tower 0.4.13", +] + [[package]] name = "tempfile" version = "3.27.0" diff --git a/Cargo.toml b/Cargo.toml index 422753a..be7fc0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "protos", "benches", "roto-tonic", + "temp_test_project", "examples/hello_world", ] diff --git a/codegen/helloworld.desc b/codegen/helloworld.desc new file mode 100644 index 0000000..aa9c185 Binary files /dev/null and b/codegen/helloworld.desc differ diff --git a/codegen/src/generator.rs b/codegen/src/generator.rs index 55c6b1b..1697149 100644 --- a/codegen/src/generator.rs +++ b/codegen/src/generator.rs @@ -51,7 +51,7 @@ fn map_type_to_rust_accessor(field_type: i32, label: i32, is_map: bool) -> (Stri match field_type { 9 => ( "&'a str".to_string(), - "str::from_utf8(bytes).map_err(|_| roto_runtime::RotoError::WireFormatViolation)".to_string(), + "std::str::from_utf8(bytes).map_err(|_| roto_runtime::RotoError::WireFormatViolation)".to_string(), "\"\"".to_string(), ), // TYPE_STRING 1 => ( @@ -117,7 +117,7 @@ fn write_enum(enum_proto: &EnumDescriptorProto, output: &mut String) { let accessor = ProtoAccessor::new(val_data).expect("Failed to parse EnumValueDescriptorProto"); let (name_bytes, _) = accessor.get_value(1).expect("Enum value name missing"); - let name = str::from_utf8(name_bytes).expect("Enum value name invalid utf8"); + let name = std::str::from_utf8(name_bytes).expect("Enum value name invalid utf8"); let (num_bytes, _) = accessor.get_value(2).expect("Enum value number missing"); let (num, _) = roto_runtime::read_varint(num_bytes).expect("Enum value number invalid varint"); @@ -147,7 +147,7 @@ fn write_enum(enum_proto: &EnumDescriptorProto, output: &mut String) { let accessor = ProtoAccessor::new(val_data).expect("Failed to parse EnumValueDescriptorProto"); let (name_bytes, _) = accessor.get_value(1).expect("Enum value name missing"); - let name = str::from_utf8(name_bytes).expect("Enum value name invalid utf8"); + let name = std::str::from_utf8(name_bytes).expect("Enum value name invalid utf8"); let (num_bytes, _) = accessor.get_value(2).expect("Enum value number missing"); let (num, _) = roto_runtime::read_varint(num_bytes).expect("Enum value number invalid varint"); @@ -170,6 +170,7 @@ fn write_enum(enum_proto: &EnumDescriptorProto, output: &mut String) { fn write_message(msg_proto: &DescriptorProto, output: &mut String) { let msg_name = to_pascal_case(msg_proto.name().unwrap()); + let mod_name = to_snake_case(msg_proto.name().unwrap()); let mut fields_info = Vec::new(); for field_res in msg_proto.field() { @@ -332,10 +333,12 @@ fn write_message(msg_proto: &DescriptorProto, output: &mut String) { let pascal_oneof_name = to_pascal_case(oneof_name); let snake_oneof_name = to_snake_case(oneof_name); - output.push_str(&format!( - " pub fn which_{}(&self) -> roto_runtime::Result>> {{\n", - snake_oneof_name, msg_name, pascal_oneof_name - )); + let return_type = format!("{}::{}<'a>", mod_name, pascal_oneof_name); + let signature = format!( + " pub fn which_{}(&self) -> roto_runtime::Result > {{\n", + snake_oneof_name, return_type + ); + output.push_str(&signature); for (field_name, _tag, _f_type, _f_label, f_oneof_index, _is_map) in &fields_info { if *f_oneof_index == Some(oneof_index as i32) { let safe_field_name = if field_name == "type" { @@ -348,8 +351,8 @@ fn write_message(msg_proto: &DescriptorProto, output: &mut String) { field_name )); output.push_str(&format!( - " return Ok(Some({}::{} (self.{}()?)));\n", - pascal_oneof_name, safe_field_name, safe_field_name + " return Ok(Some({}::{}::{} (self.{}()?)));\n", + mod_name, pascal_oneof_name, safe_field_name, safe_field_name )); output.push_str(" }\n"); } @@ -414,7 +417,7 @@ fn write_message(msg_proto: &DescriptorProto, output: &mut String) { " pub fn with(mut self, msg: &{}<'_>) -> roto_runtime::Result {{\n", msg_name )); - output.push_str(" for item in msg.raw_fields() {\n"); + output.push_str(" for item in msg.accessor.raw_fields() {\n"); output.push_str(" let (field_number, raw_bytes) = item?;\n"); output.push_str(" let is_written = match field_number {\n"); for (field_name, _, tag, _, _) in &builder_fields { @@ -563,7 +566,7 @@ pub fn generate_rust_code( for dep_res in file_proto.dependency() { let (dep_data, _) = dep_res.expect("Failed to iterate dependency"); - let dep_name = str::from_utf8(dep_data).expect("Dependency name invalid utf8"); + let dep_name = std::str::from_utf8(dep_data).expect("Dependency name invalid utf8"); let dep_mod_path = dep_name.replace(".proto", "").replace('/', "::"); output.push_str(&format!("use crate::{};\n", dep_mod_path)); } @@ -725,9 +728,7 @@ fn write_service(svc_proto: &ServiceDescriptorProto, output: &mut String) { output.push_str(" let mut buf = pool.get();\n"); output.push_str(" let mut stream = body;\n"); output.push_str(" while let Some(frame_result) = stream.frame().await {\n"); - output.push_str(" let frame = frame_result.map_err(|e| {\n"); - output.push_str(" panic!(\"Body frame error: {}\", e);\n"); - output.push_str(" })?;\n"); + output.push_str(" let frame = frame_result.expect(\"Body frame error\");\n"); output.push_str(" if let Some(data) = frame.data_ref() {\n"); output.push_str(" buf.put(data.clone());\n"); output.push_str(" }\n"); diff --git a/codegen/tests/helloworld.desc b/codegen/tests/helloworld.desc new file mode 100644 index 0000000..aa9c185 Binary files /dev/null and b/codegen/tests/helloworld.desc differ diff --git a/codegen/tests/test_helloworld_build.rs b/codegen/tests/test_helloworld_build.rs new file mode 100644 index 0000000..e4c02df --- /dev/null +++ b/codegen/tests/test_helloworld_build.rs @@ -0,0 +1,76 @@ +use roto_codegen::google::protobuf::descriptor::FileDescriptorSet; +use std::fs; +use std::process::Command; + +#[test] +fn test_helloworld_generated_code_builds() { + // 1. Load FileDescriptorSet from helloworld.desc + let desc_path = "helloworld.desc"; + // Note: This assumes helloworld.desc is in the working directory of the test. + // We might need to provide the full path or copy it to the test data directory. + let data = fs::read(desc_path).expect("Failed to read helloworld.desc"); + let set = FileDescriptorSet::new(&data) + .expect("Failed to create FileDescriptorSet from helloworld.desc"); + + let generated_files = roto_codegen::generator::generate_rust_code(&set, None, false); + assert!( + !generated_files.is_empty(), + "Generated code should not be empty" + ); + + for (path, content) in &generated_files { + println!("--- File: {} ---\n{}", path, content); + } + + // 2. Setup a temporary Cargo project to verify the code builds + let codegen_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let project_root = codegen_root.parent().expect("Failed to get project root"); + let temp_project_dir = std::path::PathBuf::from("/tmp/roto_helloworld_gen_project"); + + // Clean up previous runs + if temp_project_dir.exists() { + fs::remove_dir_all(&temp_project_dir).expect("Failed to clean up temp project directory"); + } + + // Create new library project + let status = Command::new("cargo") + .args(["new", "--lib", temp_project_dir.to_str().expect("Invalid path")]) + .status() + .expect("Failed to run cargo new"); + assert!(status.success(), "cargo new failed"); + + // 3. Configure the project to depend on the current roto crate + let cargo_toml_path = temp_project_dir.join("Cargo.toml"); + let cargo_toml_content = + fs::read_to_string(&cargo_toml_path).expect("Failed to read Cargo.toml"); + let updated_cargo_toml = format!( + "{}\n\nroto-codegen = {{ path = \"{}\" }}\nroto-runtime = {{ path = \"{}\" }}\nroto-tonic = {{ path = \"{}\" }}\nbytes = \"1.7\"\ntonic = \"0.12\"\ntokio-stream = \"0.1\"\ntower = \"0.4\"\nfutures-util = \"0.3\"\nhttp-body-util = \"0.1\"\nhttp-body = \"1.0\"\n\n[workspace]\n", + cargo_toml_content, + codegen_root.to_string_lossy(), + project_root.join("runtime").to_string_lossy(), + project_root.join("roto-tonic").to_string_lossy() + ); + fs::write(cargo_toml_path, updated_cargo_toml).expect("Failed to write Cargo.toml"); + + // 4. Write the generated code to src/lib.rs + let mut all_code = String::new(); + for (_, content) in generated_files { + all_code.push_str(&content); + all_code.push_str("\n"); + } + let final_code = all_code.replace("use crate::", "use roto::"); + let lib_path = temp_project_dir.join("src/lib.rs"); + fs::write(lib_path, final_code).expect("Failed to write generated code to src/lib.rs"); + + // 5. Attempt to build the project + let build_status = Command::new("cargo") + .args(["build"]) + .current_dir(&temp_project_dir) + .status() + .expect("Failed to run cargo build"); + + assert!( + build_status.success(), + "The generated Rust code for helloworld.proto failed to build in a standalone project!" + ); +} diff --git a/helloworld.desc b/helloworld.desc new file mode 100644 index 0000000..aa9c185 Binary files /dev/null and b/helloworld.desc differ