actr-web-protoc-codegen 0.2.1

Protoc plugin for generating actr-web code from protobuf definitions
Documentation
//! Rust code generator

use crate::{
    GeneratedFile, ProtoMessage, ProtoMethod, ProtoService, config::WebCodegenConfig, descriptor,
    error::CodegenError, error::Result,
};

/// Parse proto files by compiling them into a `FileDescriptorSet` via
/// `protoc` and walking the structured output, yielding one `ProtoService`
/// per file that declares a service.
///
/// Files without a service are skipped with a warning — this mirrors the
/// previous behaviour, where `parse_proto_content` would bail on such files
/// as "invalid".
pub(crate) fn parse_proto_files(config: &WebCodegenConfig) -> Result<Vec<ProtoService>> {
    let set = descriptor::compile_to_descriptor_set(&config.proto_files, &config.includes)?;

    let mut services = Vec::new();
    for proto_path in &config.proto_files {
        let file = descriptor::find_file(&set, proto_path).ok_or_else(|| {
            CodegenError::proto_parse(format!(
                "protoc emitted no descriptor for {}; check --include paths",
                proto_path.display()
            ))
        })?;

        match descriptor::file_to_proto_service(file) {
            Some(service) => services.push(service),
            None => {
                tracing::warn!(
                    "{}: no service declaration found, skipping",
                    proto_path.display()
                );
            }
        }
    }

    Ok(services)
}

/// Generate Rust Actor code
pub(crate) fn generate_rust_actors(
    config: &WebCodegenConfig,
    services: &[ProtoService],
) -> Result<Vec<GeneratedFile>> {
    let mut files = Vec::new();

    for service in services {
        let file = generate_rust_actor_for_service(config, service)?;
        files.push(file);
    }

    // Generate mod.rs
    let mod_file = generate_rust_mod_file(config, services)?;
    files.push(mod_file);

    Ok(files)
}

/// Generate Rust Actor code for a single service
fn generate_rust_actor_for_service(
    config: &WebCodegenConfig,
    service: &ProtoService,
) -> Result<GeneratedFile> {
    use heck::ToSnakeCase;

    let file_name = format!("{}.rs", service.name.to_snake_case());
    let file_path = config.rust_output_dir.join(&file_name);

    let mut content = format!(
        r#"//! Auto-generated Actor code
//! Service: {}
//! Package: {}
//!
//! DO NOT EDIT this file manually

use wasm_bindgen::prelude::*;
use serde::{{Serialize, Deserialize}};

"#,
        service.name, service.package
    );

    // Generate message type definitions
    for message in &service.messages {
        content.push_str(&generate_rust_message(message));
        content.push('\n');
    }

    // Generate Actor struct
    content.push_str(&format!(
        r#"/// {} Actor
#[wasm_bindgen]
pub struct {}Actor {{
    // Actor state
}}

#[wasm_bindgen]
impl {}Actor {{
    /// Create a new Actor instance
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {{
        Self {{}}
    }}

"#,
        service.name, service.name, service.name
    ));

    // Generate methods
    for method in &service.methods {
        content.push_str(&generate_rust_method(method));
        content.push('\n');
    }

    content.push_str("}\n");

    Ok(GeneratedFile::new(file_path, content))
}

/// Generate Rust message type
fn generate_rust_message(message: &ProtoMessage) -> String {
    let mut content = format!(
        r#"/// {} message
#[derive(Debug, Clone, Serialize, Deserialize)]
#[wasm_bindgen]
pub struct {} {{
"#,
        message.name, message.name
    );

    for field in &message.fields {
        let rust_type = proto_type_to_rust(&field.field_type);
        let field_type = if field.is_repeated {
            format!("Vec<{}>", rust_type)
        } else if field.is_optional {
            format!("Option<{}>", rust_type)
        } else {
            rust_type
        };

        content.push_str(&format!("    pub {}: {},\n", field.name, field_type));
    }

    content.push_str("}\n");
    content
}

/// Generate Rust method
fn generate_rust_method(method: &ProtoMethod) -> String {
    use heck::ToSnakeCase;

    let method_name = method.name.to_snake_case();
    let input_type = &method.input_type;
    let output_type = &method.output_type;

    if method.is_streaming {
        // Streaming method
        format!(
            r#"    /// {} method (streaming)
    pub async fn {}(&self, request: {}) -> Result<JsValue, JsValue> {{
        // TODO: implement streaming method
        todo!("implement streaming method: {}")
    }}
"#,
            method.name, method_name, input_type, method.name
        )
    } else {
        // Regular RPC method
        format!(
            r#"    /// {} method
    pub async fn {}(&self, request: {}) -> Result<{}, JsValue> {{
        // TODO: implement method logic
        todo!("implement method: {}")
    }}
"#,
            method.name, method_name, input_type, output_type, method.name
        )
    }
}

/// Convert Proto type to Rust type
fn proto_type_to_rust(proto_type: &str) -> String {
    match proto_type {
        "string" => "String".to_string(),
        "bytes" => "Vec<u8>".to_string(),
        "int32" | "sint32" | "sfixed32" => "i32".to_string(),
        "int64" | "sint64" | "sfixed64" => "i64".to_string(),
        "uint32" | "fixed32" => "u32".to_string(),
        "uint64" | "fixed64" => "u64".to_string(),
        "bool" => "bool".to_string(),
        "float" => "f32".to_string(),
        "double" => "f64".to_string(),
        // Custom types remain as-is
        custom => custom.to_string(),
    }
}

/// Generate Rust mod.rs
fn generate_rust_mod_file(
    config: &WebCodegenConfig,
    services: &[ProtoService],
) -> Result<GeneratedFile> {
    use heck::ToSnakeCase;

    let file_path = config.rust_output_dir.join("mod.rs");

    let mut content = String::from(
        r#"//! Auto-generated module
//!
//! DO NOT EDIT this file manually

"#,
    );

    for service in services {
        let module_name = service.name.to_snake_case();
        content.push_str(&format!("pub mod {};\n", module_name));
    }

    content.push('\n');

    for service in services {
        let module_name = service.name.to_snake_case();
        content.push_str(&format!(
            "pub use {}::{}Actor;\n",
            module_name, service.name
        ));
    }

    Ok(GeneratedFile::new(file_path, content))
}