Skip to main content

olai_codegen/parsing/
models.rs

1use std::collections::HashMap;
2
3use crate::gnostic::openapi::v3::Operation;
4use crate::google::api::{FieldBehavior, HttpRule, ResourceDescriptor, ResourceReference};
5use crate::parsing::http::HttpPattern;
6use crate::parsing::types::{BaseType, UnifiedType};
7use crate::utils::last_segment;
8
9/// Collected metadata for code generation
10#[derive(Debug, Default)]
11pub struct CodeGenMetadata {
12    pub messages: HashMap<String, MessageInfo>,
13    pub enums: HashMap<String, EnumInfo>,
14    pub services: HashMap<String, ServiceInfo>,
15}
16
17impl CodeGenMetadata {
18    /// Get message fields for a given type name
19    pub fn get_message_fields(&self, type_name: &str) -> Vec<MessageField> {
20        self.messages
21            .get(type_name)
22            .map(|msg| msg.fields.clone())
23            .unwrap_or_default()
24    }
25
26    /// Find a resource descriptor whose `singular` field matches `name`.
27    pub fn resource_from_singular(&self, name: &str) -> Option<&ResourceDescriptor> {
28        self.messages.values().find_map(|info| {
29            info.resource_descriptor
30                .as_ref()
31                .filter(|r| r.singular == name)
32        })
33    }
34
35    /// Find a resource descriptor whose `plural` field matches `name`.
36    pub fn resource_from_plural(&self, name: &str) -> Option<&ResourceDescriptor> {
37        self.messages.values().find_map(|info| {
38            info.resource_descriptor
39                .as_ref()
40                .filter(|r| r.plural == name)
41        })
42    }
43
44    /// Get resource descriptor by message type name (simple or fully-qualified).
45    pub fn get_resource_descriptor(&self, type_name: &str) -> Option<&ResourceDescriptor> {
46        // Try direct lookup first (fully-qualified name)
47        if let Some(descriptor) = self
48            .messages
49            .get(type_name)
50            .and_then(|info| info.resource_descriptor.as_ref())
51        {
52            return Some(descriptor);
53        }
54
55        // Fall back to simple-name suffix match
56        self.messages.iter().find_map(|(key, info)| {
57            if last_segment(key) == type_name {
58                info.resource_descriptor.as_ref()
59            } else {
60                None
61            }
62        })
63    }
64}
65
66/// Information about a protobuf message
67#[derive(Debug, Clone)]
68pub struct MessageInfo {
69    pub name: String,
70    pub fields: Vec<MessageField>,
71    pub resource_descriptor: Option<ResourceDescriptor>,
72    pub documentation: Option<String>,
73}
74
75/// Information about a field in a protobuf message
76#[derive(Debug, Clone)]
77pub struct MessageField {
78    pub name: String,
79    /// The explicit `json_name` of the field; `None` for protoc's default.
80    pub json_name: Option<String>,
81    /// The HTTP response header this field is read from (`(olai.trestle.v1.response_header)`).
82    pub response_header: Option<String>,
83    /// The HTTP request header this field is sent in (`(olai.trestle.v1.request_header)`).
84    pub request_header: Option<String>,
85    /// Whether the field is sent even when unpopulated (`(olai.trestle.v1.emit_unpopulated)`).
86    pub emit_unpopulated: bool,
87    /// Whether the field is the raw HTTP body of its message (`(olai.trestle.v1.raw_body)`).
88    pub raw_body: bool,
89    /// Language-agnostic type; carries `is_optional` and `is_repeated` flags.
90    pub unified_type: UnifiedType,
91    pub documentation: Option<String>,
92    /// For oneof fields, contains the variants with their field names and types.
93    pub oneof_variants: Option<Vec<OneofVariant>>,
94    /// Field behavior annotations from google.api.field_behavior
95    pub field_behavior: Vec<FieldBehavior>,
96    /// Whether the field is marked with `debug_redact = true` in the proto definition.
97    /// Indicates the field contains sensitive data that should be routed to secret storage.
98    pub is_sensitive: bool,
99    /// Resource reference annotation from `google.api.resource_reference` (ext 1055).
100    ///
101    /// - `child_type` non-empty: this field identifies a parent container for the named resource
102    ///   (e.g. `catalog_name` with `child_type = "example.io/Schema"`).
103    /// - `r#type` non-empty: this field directly identifies a resource of that type
104    ///   (e.g. `full_name` with `type = "example.io/Table"`).
105    pub resource_reference: Option<ResourceReference>,
106}
107
108/// Information about a variant in a oneof field
109#[derive(Debug, Clone)]
110pub struct OneofVariant {
111    pub field_name: String,      // e.g., "azure_service_principal"
112    pub variant_name: String,    // e.g., "AzureServicePrincipal"
113    pub field_type: UnifiedType, // the unified type (language-agnostic)
114    pub documentation: Option<String>,
115}
116
117impl OneofVariant {
118    /// Primitive-type oneofs use `i32` as the enum discriminant when crossing
119    /// FFI boundaries (NAPI/Python extractors).  Return true when the variant
120    /// holds a plain integer.
121    pub fn is_int32(&self) -> bool {
122        matches!(self.field_type.base_type, BaseType::Int32)
123    }
124}
125
126/// Information about a protobuf enum
127#[derive(Debug, Clone)]
128pub struct EnumInfo {
129    pub name: String,
130    pub values: Vec<EnumValue>,
131    pub documentation: Option<String>,
132}
133
134/// Information about an enum value
135#[derive(Debug, Clone)]
136pub struct EnumValue {
137    pub name: String,
138    pub number: i32,
139    pub documentation: Option<String>,
140}
141
142/// Information about a protobuf service
143#[derive(Debug, Clone)]
144pub struct ServiceInfo {
145    pub name: String,
146    /// Proto package name (e.g. `"example.catalogs.v1"`).
147    pub package: String,
148    pub documentation: Option<String>,
149    pub methods: Vec<MethodMetadata>,
150}
151
152/// Metadata extracted from a service method
153#[derive(Debug, Clone)]
154pub struct MethodMetadata {
155    pub service_name: String,
156    pub method_name: String,
157    pub input_type: String,
158    pub output_type: String,
159    pub operation: Option<Operation>,
160    pub http_rule: HttpRule,
161    /// Pre-parsed HTTP URL pattern. Analysis should use this directly instead of re-parsing.
162    pub http_pattern: HttpPattern,
163    pub documentation: Option<String>,
164    /// A server-streaming RPC (`returns (stream T)`): NDJSON over REST.
165    pub server_streaming: bool,
166}
167
168impl MethodMetadata {
169    /// Return the HTTP method string (e.g. "GET", "POST").
170    pub fn http_method(&self) -> Option<&str> {
171        use crate::google::api::http_rule::Pattern;
172        self.http_rule.pattern.as_ref().map(|p| match p {
173            Pattern::Get(_) => "GET",
174            Pattern::Post(_) => "POST",
175            Pattern::Put(_) => "PUT",
176            Pattern::Delete(_) => "DELETE",
177            Pattern::Patch(_) => "PATCH",
178            Pattern::Custom(c) => c.kind.as_str(),
179        })
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn test_service_based_method_structure() {
189        let mut codegen_metadata = CodeGenMetadata {
190            messages: HashMap::new(),
191            services: HashMap::new(),
192            enums: HashMap::new(),
193        };
194
195        // Create a test service with methods
196        let mut service_info = ServiceInfo {
197            name: "TestService".to_string(),
198            package: "test.v1".to_string(),
199            documentation: Some("Test service documentation".to_string()),
200            methods: Vec::new(),
201        };
202
203        // Add a method to the service
204        let http_rule = crate::google::api::HttpRule {
205            pattern: Some(crate::google::api::http_rule::Pattern::Get(
206                "/test".to_string(),
207            )),
208            ..Default::default()
209        };
210
211        let method = MethodMetadata {
212            service_name: "TestService".to_string(),
213            method_name: "TestMethod".to_string(),
214            input_type: ".test.TestRequest".to_string(),
215            output_type: ".test.TestResponse".to_string(),
216            server_streaming: false,
217            operation: None,
218            http_pattern: crate::parsing::http::HttpPattern::parse("/test"),
219            http_rule,
220            documentation: Some("Test method documentation".to_string()),
221        };
222        service_info.methods.push(method);
223
224        codegen_metadata
225            .services
226            .insert("TestService".to_string(), service_info);
227
228        // Test that we can access the service and its methods directly
229        let service = codegen_metadata.services.get("TestService").unwrap();
230        assert_eq!(service.name, "TestService");
231        assert_eq!(service.methods.len(), 1);
232        assert_eq!(service.methods[0].method_name, "TestMethod");
233    }
234}