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    /// Language-agnostic type; carries `is_optional` and `is_repeated` flags.
82    pub unified_type: UnifiedType,
83    pub documentation: Option<String>,
84    /// For oneof fields, contains the variants with their field names and types.
85    pub oneof_variants: Option<Vec<OneofVariant>>,
86    /// Field behavior annotations from google.api.field_behavior
87    pub field_behavior: Vec<FieldBehavior>,
88    /// Whether the field is marked with `debug_redact = true` in the proto definition.
89    /// Indicates the field contains sensitive data that should be routed to secret storage.
90    pub is_sensitive: bool,
91    /// Resource reference annotation from `google.api.resource_reference` (ext 1055).
92    ///
93    /// - `child_type` non-empty: this field identifies a parent container for the named resource
94    ///   (e.g. `catalog_name` with `child_type = "example.io/Schema"`).
95    /// - `r#type` non-empty: this field directly identifies a resource of that type
96    ///   (e.g. `full_name` with `type = "example.io/Table"`).
97    pub resource_reference: Option<ResourceReference>,
98}
99
100/// Information about a variant in a oneof field
101#[derive(Debug, Clone)]
102pub struct OneofVariant {
103    pub field_name: String,      // e.g., "azure_service_principal"
104    pub variant_name: String,    // e.g., "AzureServicePrincipal"
105    pub field_type: UnifiedType, // the unified type (language-agnostic)
106    pub documentation: Option<String>,
107}
108
109impl OneofVariant {
110    /// Primitive-type oneofs use `i32` as the enum discriminant when crossing
111    /// FFI boundaries (NAPI/Python extractors).  Return true when the variant
112    /// holds a plain integer.
113    pub fn is_int32(&self) -> bool {
114        matches!(self.field_type.base_type, BaseType::Int32)
115    }
116}
117
118/// Information about a protobuf enum
119#[derive(Debug, Clone)]
120pub struct EnumInfo {
121    pub name: String,
122    pub values: Vec<EnumValue>,
123    pub documentation: Option<String>,
124}
125
126/// Information about an enum value
127#[derive(Debug, Clone)]
128pub struct EnumValue {
129    pub name: String,
130    pub number: i32,
131    pub documentation: Option<String>,
132}
133
134/// Information about a protobuf service
135#[derive(Debug, Clone)]
136pub struct ServiceInfo {
137    pub name: String,
138    /// Proto package name (e.g. `"example.catalogs.v1"`).
139    pub package: String,
140    pub documentation: Option<String>,
141    pub methods: Vec<MethodMetadata>,
142}
143
144/// Metadata extracted from a service method
145#[derive(Debug, Clone)]
146pub struct MethodMetadata {
147    pub service_name: String,
148    pub method_name: String,
149    pub input_type: String,
150    pub output_type: String,
151    pub operation: Option<Operation>,
152    pub http_rule: HttpRule,
153    /// Pre-parsed HTTP URL pattern. Analysis should use this directly instead of re-parsing.
154    pub http_pattern: HttpPattern,
155    pub documentation: Option<String>,
156}
157
158impl MethodMetadata {
159    /// Return the HTTP method string (e.g. "GET", "POST").
160    pub fn http_method(&self) -> Option<&str> {
161        use crate::google::api::http_rule::Pattern;
162        self.http_rule.pattern.as_ref().map(|p| match p {
163            Pattern::Get(_) => "GET",
164            Pattern::Post(_) => "POST",
165            Pattern::Put(_) => "PUT",
166            Pattern::Delete(_) => "DELETE",
167            Pattern::Patch(_) => "PATCH",
168            Pattern::Custom(c) => c.kind.as_str(),
169        })
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_service_based_method_structure() {
179        let mut codegen_metadata = CodeGenMetadata {
180            messages: HashMap::new(),
181            services: HashMap::new(),
182            enums: HashMap::new(),
183        };
184
185        // Create a test service with methods
186        let mut service_info = ServiceInfo {
187            name: "TestService".to_string(),
188            package: "test.v1".to_string(),
189            documentation: Some("Test service documentation".to_string()),
190            methods: Vec::new(),
191        };
192
193        // Add a method to the service
194        let http_rule = crate::google::api::HttpRule {
195            pattern: Some(crate::google::api::http_rule::Pattern::Get(
196                "/test".to_string(),
197            )),
198            ..Default::default()
199        };
200
201        let method = MethodMetadata {
202            service_name: "TestService".to_string(),
203            method_name: "TestMethod".to_string(),
204            input_type: ".test.TestRequest".to_string(),
205            output_type: ".test.TestResponse".to_string(),
206            operation: None,
207            http_pattern: crate::parsing::http::HttpPattern::parse("/test"),
208            http_rule,
209            documentation: Some("Test method documentation".to_string()),
210        };
211        service_info.methods.push(method);
212
213        codegen_metadata
214            .services
215            .insert("TestService".to_string(), service_info);
216
217        // Test that we can access the service and its methods directly
218        let service = codegen_metadata.services.get("TestService").unwrap();
219        assert_eq!(service.name, "TestService");
220        assert_eq!(service.methods.len(), 1);
221        assert_eq!(service.methods[0].method_name, "TestMethod");
222    }
223}