olai_codegen/parsing/mod.rs
1//! Parsing stage: compiled descriptor → [`CodeGenMetadata`].
2//!
3//! This is the first stage of the code-generation pipeline. It walks every
4//! message, enum, and service in a [`FileDescriptorSet`] and extracts the
5//! annotations that drive generation into a structured, runtime-friendly form.
6//!
7//! The generator reads the standard Google API extensions (`google.api.http`,
8//! `google.api.resource`, `google.api.field_behavior`,
9//! `google.api.resource_reference`), `gnostic.openapi.v3.operation`, and four
10//! trestle field options from `proto/olai/trestle/v1/annotations.proto`
11//! (`olai.trestle.v1.response_header`, `emit_unpopulated`, `raw_body`,
12//! `request_header`). Their field numbers are the `*_EXTENSION` module constants
13//! below. The core `debug_redact` option (`google.protobuf.FieldOptions` field 16)
14//! is handled in the `message` submodule. [`parse_file_descriptor_set`] is the
15//! entry point.
16
17use std::collections::HashMap;
18
19pub use self::http::{HttpPattern, UrlSegment, extract_http_rule_pattern, extract_path_parameters};
20pub use self::models::*;
21use protobuf::descriptor::{FileDescriptorProto, FileDescriptorSet, SourceCodeInfo};
22pub mod types;
23use crate::Result;
24
25mod enum_parser;
26pub mod http;
27mod message;
28mod models;
29mod service;
30
31/// Extract documentation text for a protobuf element at the given source path.
32///
33/// Scans `SourceCodeInfo.location` for an entry whose path matches `path`,
34/// prefers leading comments over trailing comments, and returns the trimmed text.
35pub(super) fn extract_documentation(sci: Option<&SourceCodeInfo>, path: &[i32]) -> Option<String> {
36 let sci = sci?;
37 for location in &sci.location {
38 if location.path.as_slice() == path {
39 let text = if location.has_leading_comments() {
40 location.leading_comments().trim().to_string()
41 } else if location.has_trailing_comments() {
42 location.trailing_comments().trim().to_string()
43 } else {
44 String::new()
45 };
46 if !text.is_empty() {
47 return Some(text);
48 }
49 }
50 }
51 None
52}
53
54// Known extension field numbers
55const GOOGLE_API_HTTP_EXTENSION: u32 = 72295728; // google.api.http
56const GNOSTIC_OPERATION_EXTENSION: u32 = 1143; // gnostic.openapi.v3.operation
57const GOOGLE_API_RESOURCE_EXTENSION: u32 = 1053; // google.api.resource
58const GOOGLE_API_FIELD_BEHAVIOR_EXTENSION: u32 = 1052; // google.api.field_behavior
59/// `olai.trestle.v1.response_header` (`proto/olai/trestle/v1/annotations.proto`).
60const TRESTLE_RESPONSE_HEADER_EXTENSION: u32 = 51001;
61const TRESTLE_EMIT_UNPOPULATED_EXTENSION: u32 = 51002;
62const TRESTLE_RAW_BODY_EXTENSION: u32 = 51003;
63const TRESTLE_REQUEST_HEADER_EXTENSION: u32 = 51004;
64const GOOGLE_API_RESOURCE_REFERENCE_EXTENSION: u32 = 1055; // google.api.resource_reference
65
66/// Parse a compiled protobuf [`FileDescriptorSet`] into [`CodeGenMetadata`].
67///
68/// This is the first stage of the code-generation pipeline. It walks every
69/// message, enum, and service in the descriptor set and extracts the annotations
70/// that drive generation (`google.api.http`, `google.api.resource`,
71/// `field_behavior`, `debug_redact`, …) into a structured form.
72///
73/// The full pipeline is three stages:
74///
75/// ```text
76/// FileDescriptorSet
77/// │ parse_file_descriptor_set
78/// ▼
79/// CodeGenMetadata
80/// │ analyze_metadata (analysis/mod.rs)
81/// ▼
82/// GenerationPlan
83/// │ generate_code (codegen/mod.rs)
84/// ▼
85/// Rust / Python / TypeScript source written to disk
86/// ```
87///
88/// Typical usage from a build script:
89///
90/// ```ignore
91/// let fds = prost_types::FileDescriptorSet::decode(&bytes[..])?;
92/// let metadata = parse_file_descriptor_set(&fds)?;
93/// let config = CodeGenConfig::new(/* … */);
94/// generate_code(&metadata, &config)?;
95/// ```
96///
97/// Methods without HTTP annotations are not an error here; they are recorded later,
98/// during [`analyze_metadata`](crate::analyze_metadata).
99///
100/// # Errors
101///
102/// Returns:
103///
104/// - [`Error::InvalidAnnotation`](crate::Error::InvalidAnnotation) if a `google.api.http`,
105/// `google.api.resource`, or `field_behavior` annotation is present but malformed (e.g. a
106/// resource pattern or HTTP rule that cannot be interpreted).
107/// - [`Error::Build`](crate::Error::Build) if an annotation extension cannot be decoded from its
108/// raw descriptor bytes.
109pub fn parse_file_descriptor_set(
110 file_descriptor_set: &FileDescriptorSet,
111) -> Result<CodeGenMetadata> {
112 let mut codegen_metadata = CodeGenMetadata {
113 messages: HashMap::new(),
114 enums: HashMap::new(),
115 services: HashMap::new(),
116 };
117
118 // Process each file descriptor
119 for file_descriptor in &file_descriptor_set.file {
120 process_file_descriptor(file_descriptor, &mut codegen_metadata)?;
121 }
122
123 Ok(codegen_metadata)
124}
125
126/// Process a single protobuf file descriptor
127///
128/// Extracts all messages, services, and annotations from the file.
129/// Collects metadata for code generation.
130pub fn process_file_descriptor(
131 file_desc: &FileDescriptorProto,
132 codegen_metadata: &mut CodeGenMetadata,
133) -> Result<()> {
134 // Extract source code info for documentation
135 let source_code_info = file_desc.source_code_info.as_ref();
136
137 let package_name = file_desc.package();
138 let type_prefix = if package_name.is_empty() {
139 String::new()
140 } else {
141 format!(".{package_name}")
142 };
143
144 // Process enums in the file
145 for (enum_index, enum_desc) in file_desc.enum_type.iter().enumerate() {
146 enum_parser::process_enum(
147 enum_desc,
148 codegen_metadata,
149 &type_prefix,
150 source_code_info,
151 &[5, enum_index as i32], // enum_type is field 5 in FileDescriptorProto
152 )?;
153 }
154
155 // Process messages in the file
156 for (message_index, message) in file_desc.message_type.iter().enumerate() {
157 message::process_message(
158 message,
159 codegen_metadata,
160 &type_prefix,
161 source_code_info,
162 &[4, message_index as i32], // message_type is field 4 in FileDescriptorProto
163 )?;
164 }
165
166 // Process services in the file
167 for (service_index, service) in file_desc.service.iter().enumerate() {
168 service::process_service(
169 service,
170 package_name,
171 codegen_metadata,
172 source_code_info,
173 service_index,
174 )?;
175 }
176
177 Ok(())
178}