synapse-codegen 0.0.2

Code generation from protobuf service definitions for Synapse
Documentation
//! Parser for protobuf service definitions

use crate::{MethodDef, ServiceDef};
use anyhow::{Context, Result};
use prost::Message;
use prost_types::{FileDescriptorSet, MethodDescriptorProto, ServiceDescriptorProto};
use std::{
    path::{Path, PathBuf},
    process::Command,
};

/// Parse a proto file and extract service definitions
pub fn parse_proto_file(proto_path: &Path) -> Result<Vec<ServiceDef>> {
    parse_proto_file_with_includes(proto_path, &[])
}

/// Parse a proto file with additional include paths and extract service definitions
pub fn parse_proto_file_with_includes(
    proto_path: &Path,
    include_paths: &[PathBuf],
) -> Result<Vec<ServiceDef>> {
    // Use protoc to generate FileDescriptorSet
    let descriptor_set = compile_proto_to_descriptor(proto_path, include_paths)?;

    // Get the proto filename to filter services from the main file only
    let proto_filename = proto_path
        .file_name()
        .and_then(|s| s.to_str())
        .unwrap_or("");

    // Parse services from descriptor set (only from the main proto file)
    let mut services = Vec::new();

    for file in &descriptor_set.file {
        // Only process services from the main proto file, not imported ones
        let file_name = file.name();
        if !file_name.ends_with(proto_filename) {
            continue;
        }

        let package = file.package().to_string();

        for service in &file.service {
            let service_def = parse_service(&package, service)?;
            services.push(service_def);
        }
    }

    Ok(services)
}

/// Compile proto file to FileDescriptorSet using protoc
fn compile_proto_to_descriptor(
    proto_path: &Path,
    include_paths: &[PathBuf],
) -> Result<FileDescriptorSet> {
    // Create temp file for descriptor with unique name based on proto file
    let temp_dir = std::env::temp_dir();
    let proto_hash = {
        use std::hash::{Hash, Hasher};
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        proto_path.hash(&mut hasher);
        hasher.finish()
    };
    let descriptor_path = temp_dir.join(format!("synapse_descriptor_{:x}.pb", proto_hash));

    // Get protoc path from prost-build
    let protoc = protoc_bin_vendored::protoc_bin_path().context("Failed to get protoc binary")?;

    // Get proto directory for --proto_path
    let proto_dir = proto_path
        .parent()
        .context("Proto file has no parent directory")?;

    // Build protoc command
    let mut cmd = Command::new(protoc);
    cmd.arg("--descriptor_set_out")
        .arg(&descriptor_path)
        .arg("--include_imports")
        .arg(format!("--proto_path={}", proto_dir.display()));

    // Add additional include paths
    for include_path in include_paths {
        cmd.arg(format!("--proto_path={}", include_path.display()));
    }

    cmd.arg(proto_path);

    // Run protoc
    let output = cmd.output().context("Failed to run protoc")?;

    if !output.status.success() {
        anyhow::bail!("protoc failed: {}", String::from_utf8_lossy(&output.stderr));
    }

    // Read and parse descriptor set
    let descriptor_bytes =
        std::fs::read(&descriptor_path).context("Failed to read descriptor file")?;

    let descriptor_set = FileDescriptorSet::decode(&descriptor_bytes[..])
        .context("Failed to decode descriptor set")?;

    Ok(descriptor_set)
}

/// Parse a service descriptor into ServiceDef
fn parse_service(package: &str, service: &ServiceDescriptorProto) -> Result<ServiceDef> {
    let service_name = service.name().to_string();

    let mut methods = Vec::new();
    for method in &service.method {
        let method_def = parse_method(method)?;
        methods.push(method_def);
    }

    Ok(ServiceDef {
        package: package.to_string(),
        service_name,
        methods,
    })
}

/// Parse a method descriptor into MethodDef
fn parse_method(method: &MethodDescriptorProto) -> Result<MethodDef> {
    Ok(MethodDef {
        name: method.name().to_string(),
        input_type: method.input_type().to_string(),
        output_type: method.output_type().to_string(),
        comment: None, // TODO: Extract comments from source code info
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[ignore] // Requires actual proto file
    fn test_parse_user_service() {
        let proto_path = Path::new("../proto/proto/user_service.proto");
        let services = parse_proto_file(proto_path).unwrap();

        assert_eq!(services.len(), 1);
        let service = &services[0];
        assert_eq!(service.service_name, "UserService");
        assert_eq!(service.package, "user");
        assert_eq!(service.methods.len(), 3);
    }
}