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