use prost::Message as _;
use protobuf::Message;
use protobuf::descriptor::{MethodDescriptorProto, ServiceDescriptorProto, SourceCodeInfo};
use tracing::{debug, warn};
use super::{CodeGenMetadata, MethodMetadata, ServiceInfo, extract_documentation};
use crate::gnostic::openapi::v3::Operation;
use crate::google::api::{HttpRule, http_rule::Pattern};
use crate::parsing::http::HttpPattern;
use crate::{Error, Result};
pub(super) fn process_service(
service: &ServiceDescriptorProto,
package: &str,
codegen_metadata: &mut CodeGenMetadata,
source_code_info: Option<&SourceCodeInfo>,
service_index: usize,
) -> Result<()> {
let service_name = service.name();
let service_path = vec![6, service_index as i32]; let service_documentation = extract_documentation(source_code_info, &service_path);
let service_info = ServiceInfo {
name: service_name.to_string(),
package: package.to_string(),
documentation: service_documentation,
methods: Vec::new(),
};
codegen_metadata
.services
.insert(service_name.to_string(), service_info);
for (method_index, method) in service.method.iter().enumerate() {
process_method(
method,
service_name,
codegen_metadata,
source_code_info,
service_index,
method_index,
)?;
}
Ok(())
}
fn process_method(
method: &MethodDescriptorProto,
service_name: &str,
codegen_metadata: &mut CodeGenMetadata,
source_code_info: Option<&SourceCodeInfo>,
service_index: usize,
method_index: usize,
) -> Result<()> {
let method_name = method.name();
let input_type = method.input_type();
let output_type = method.output_type();
let method_path = vec![6, service_index as i32, 2, method_index as i32];
let method_documentation = extract_documentation(source_code_info, &method_path);
let (operation, http_rule) = extract_method_annotations(method, service_name)?;
let http_pattern = {
let raw_path = match &http_rule.pattern {
Some(Pattern::Get(p))
| Some(Pattern::Post(p))
| Some(Pattern::Put(p))
| Some(Pattern::Delete(p))
| Some(Pattern::Patch(p)) => p.as_str(),
Some(Pattern::Custom(c)) => c.path.as_str(),
None => "",
};
HttpPattern::parse(raw_path)
};
let method_metadata = MethodMetadata {
service_name: service_name.to_string(),
method_name: method_name.to_string(),
input_type: input_type.to_string(),
output_type: output_type.to_string(),
operation,
http_rule,
http_pattern,
documentation: method_documentation,
};
if let Some(service_info) = codegen_metadata.services.get_mut(service_name) {
service_info.methods.push(method_metadata);
}
Ok(())
}
fn extract_method_annotations(
method: &MethodDescriptorProto,
service_name: &str,
) -> Result<(Option<Operation>, HttpRule)> {
if method.options.is_none() {
return Err(Error::MissingAnnotation {
object: method.name().to_string(),
message: "missing required google.api.http annotation".to_string(),
});
}
let options = method.options.as_ref().unwrap();
let unknown_fields = options.unknown_fields();
let mut operation = None;
let mut http_rule = None;
for (field_number, field_value) in unknown_fields.iter() {
let data = match field_value {
protobuf::UnknownValueRef::LengthDelimited(bytes) => bytes,
_ => {
debug!("Skipping non-length-delimited field {}", field_number);
continue;
}
};
match field_number {
super::GOOGLE_API_HTTP_EXTENSION => {
match HttpRule::decode(data) {
Ok(rule) => {
http_rule = Some(rule);
}
Err(e) => {
return Err(Error::InvalidAnnotation {
object: method.name().to_string(),
message: format!(
"Failed to parse HTTP rule for {}.{}: {}",
service_name,
method.name(),
e
),
});
}
}
}
super::GNOSTIC_OPERATION_EXTENSION => {
match Operation::decode(data) {
Ok(op) => {
operation = Some(op);
}
Err(e) => {
warn!("Failed to parse gnostic operation: {}", e);
}
}
}
_ => {
debug!(
"Unknown extension field {} in {}.{}",
field_number,
service_name,
method.name()
);
}
}
}
let http_rule = http_rule.ok_or_else(|| Error::MissingAnnotation {
object: method.name().to_string(),
message: "missing required google.api.http annotation".to_string(),
})?;
Ok((operation, http_rule))
}
#[cfg(test)]
mod tests {
use protobuf::descriptor::MethodOptions;
use super::*;
#[test]
fn test_extract_service_documentation() {
let mut sci = SourceCodeInfo::new();
let mut location = protobuf::descriptor::source_code_info::Location::new();
location.path = vec![6, 0];
location.set_leading_comments("This is a test service for documentation.".to_string());
sci.location.push(location);
let result = extract_documentation(Some(&sci), &[6, 0]);
assert_eq!(
result.as_deref(),
Some("This is a test service for documentation.")
);
}
#[test]
fn test_extract_method_documentation() {
let mut sci = SourceCodeInfo::new();
let mut location = protobuf::descriptor::source_code_info::Location::new();
location.path = vec![6, 0, 2, 0];
location.set_leading_comments("This method does something useful.".to_string());
sci.location.push(location);
let result = extract_documentation(Some(&sci), &[6, 0, 2, 0]);
assert_eq!(
result.as_deref(),
Some("This method does something useful.")
);
}
#[test]
fn test_missing_http_rule_causes_error() {
let method = MethodDescriptorProto {
name: Some("TestMethod".to_string()),
input_type: Some(".test.TestRequest".to_string()),
output_type: Some(".test.TestResponse".to_string()),
options: Some(MethodOptions::default()).into(), ..Default::default()
};
let result = extract_method_annotations(&method, "TestService");
assert!(result.is_err());
let error_message = result.unwrap_err().to_string();
assert!(error_message.contains("missing required google.api.http annotation"));
}
#[test]
fn test_method_without_options_causes_error() {
let method = MethodDescriptorProto {
name: Some("TestMethod".to_string()),
input_type: Some(".test.TestRequest".to_string()),
output_type: Some(".test.TestResponse".to_string()),
options: None.into(), ..Default::default()
};
let result = extract_method_annotations(&method, "TestService");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
Error::MissingAnnotation { .. }
));
}
}