Skip to main content

synapse_codegen/
parser.rs

1//! Parser for protobuf service definitions
2
3use crate::{MethodDef, ServiceDef};
4use anyhow::{Context, Result};
5use prost::Message;
6use prost_types::{FileDescriptorSet, MethodDescriptorProto, ServiceDescriptorProto};
7use std::{
8    path::{Path, PathBuf},
9    process::Command,
10};
11
12/// Parse a proto file and extract service definitions
13pub fn parse_proto_file(proto_path: &Path) -> Result<Vec<ServiceDef>> {
14    parse_proto_file_with_includes(proto_path, &[])
15}
16
17/// Parse a proto file with additional include paths and extract service definitions
18pub fn parse_proto_file_with_includes(
19    proto_path: &Path,
20    include_paths: &[PathBuf],
21) -> Result<Vec<ServiceDef>> {
22    // Use protoc to generate FileDescriptorSet
23    let descriptor_set = compile_proto_to_descriptor(proto_path, include_paths)?;
24
25    // Get the proto filename to filter services from the main file only
26    let proto_filename = proto_path
27        .file_name()
28        .and_then(|s| s.to_str())
29        .unwrap_or("");
30
31    // Parse services from descriptor set (only from the main proto file)
32    let mut services = Vec::new();
33
34    for file in &descriptor_set.file {
35        // Only process services from the main proto file, not imported ones
36        let file_name = file.name();
37        if !file_name.ends_with(proto_filename) {
38            continue;
39        }
40
41        let package = file.package().to_string();
42
43        for service in &file.service {
44            let service_def = parse_service(&package, service)?;
45            services.push(service_def);
46        }
47    }
48
49    Ok(services)
50}
51
52/// Compile proto file to FileDescriptorSet using protoc
53fn compile_proto_to_descriptor(
54    proto_path: &Path,
55    include_paths: &[PathBuf],
56) -> Result<FileDescriptorSet> {
57    // Create temp file for descriptor with unique name based on proto file
58    let temp_dir = std::env::temp_dir();
59    let proto_hash = {
60        use std::hash::{Hash, Hasher};
61        let mut hasher = std::collections::hash_map::DefaultHasher::new();
62        proto_path.hash(&mut hasher);
63        hasher.finish()
64    };
65    let descriptor_path = temp_dir.join(format!("synapse_descriptor_{:x}.pb", proto_hash));
66
67    // Get protoc path from prost-build
68    let protoc = protoc_bin_vendored::protoc_bin_path().context("Failed to get protoc binary")?;
69
70    // Get proto directory for --proto_path
71    let proto_dir = proto_path
72        .parent()
73        .context("Proto file has no parent directory")?;
74
75    // Build protoc command
76    let mut cmd = Command::new(protoc);
77    cmd.arg("--descriptor_set_out")
78        .arg(&descriptor_path)
79        .arg("--include_imports")
80        .arg(format!("--proto_path={}", proto_dir.display()));
81
82    // Add additional include paths
83    for include_path in include_paths {
84        cmd.arg(format!("--proto_path={}", include_path.display()));
85    }
86
87    cmd.arg(proto_path);
88
89    // Run protoc
90    let output = cmd.output().context("Failed to run protoc")?;
91
92    if !output.status.success() {
93        anyhow::bail!("protoc failed: {}", String::from_utf8_lossy(&output.stderr));
94    }
95
96    // Read and parse descriptor set
97    let descriptor_bytes =
98        std::fs::read(&descriptor_path).context("Failed to read descriptor file")?;
99
100    let descriptor_set = FileDescriptorSet::decode(&descriptor_bytes[..])
101        .context("Failed to decode descriptor set")?;
102
103    Ok(descriptor_set)
104}
105
106/// Parse a service descriptor into ServiceDef
107fn parse_service(package: &str, service: &ServiceDescriptorProto) -> Result<ServiceDef> {
108    let service_name = service.name().to_string();
109
110    let mut methods = Vec::new();
111    for method in &service.method {
112        let method_def = parse_method(method)?;
113        methods.push(method_def);
114    }
115
116    Ok(ServiceDef {
117        package: package.to_string(),
118        service_name,
119        methods,
120    })
121}
122
123/// Parse a method descriptor into MethodDef
124fn parse_method(method: &MethodDescriptorProto) -> Result<MethodDef> {
125    Ok(MethodDef {
126        name: method.name().to_string(),
127        input_type: method.input_type().to_string(),
128        output_type: method.output_type().to_string(),
129        comment: None, // TODO: Extract comments from source code info
130    })
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    #[ignore] // Requires actual proto file
139    fn test_parse_user_service() {
140        let proto_path = Path::new("../proto/proto/user_service.proto");
141        let services = parse_proto_file(proto_path).unwrap();
142
143        assert_eq!(services.len(), 1);
144        let service = &services[0];
145        assert_eq!(service.service_name, "UserService");
146        assert_eq!(service.package, "user");
147        assert_eq!(service.methods.len(), 3);
148    }
149}