Skip to main content

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