Skip to main content

st/inputs/
openapi.rs

1//! OpenAPI/Swagger specification input adapter
2//!
3//! Transforms API specs into navigable context trees
4
5use super::*;
6use anyhow::Result;
7use async_trait::async_trait;
8use serde_json;
9
10pub struct OpenApiAdapter;
11
12#[async_trait]
13impl InputAdapter for OpenApiAdapter {
14    fn name(&self) -> &'static str {
15        "OpenAPI"
16    }
17
18    fn supported_formats(&self) -> Vec<&'static str> {
19        vec!["openapi", "swagger", "oas", "api"]
20    }
21
22    async fn can_handle(&self, input: &InputSource) -> bool {
23        match input {
24            InputSource::Path(path) => {
25                if let Some(ext) = path.extension() {
26                    let ext = ext.to_string_lossy().to_lowercase();
27                    return ext == "yaml" || ext == "yml" || ext == "json";
28                }
29                false
30            }
31            InputSource::Url(url) => {
32                url.contains("swagger") || url.contains("openapi") || url.ends_with("/api-docs")
33            }
34            InputSource::Raw { format_hint, .. } => format_hint
35                .as_ref()
36                .map(|h| h == "openapi" || h == "swagger")
37                .unwrap_or(false),
38            _ => false,
39        }
40    }
41
42    async fn parse(&self, input: InputSource) -> Result<ContextNode> {
43        let spec = match input {
44            InputSource::Path(path) => {
45                let content = std::fs::read_to_string(&path)?;
46                if path
47                    .extension()
48                    .map(|e| e == "yaml" || e == "yml")
49                    .unwrap_or(false)
50                {
51                    serde_yaml::from_str(&content)?
52                } else {
53                    serde_json::from_str(&content)?
54                }
55            }
56            InputSource::Url(url) => {
57                let response = reqwest::get(url).await?;
58                response.json::<serde_json::Value>().await?
59            }
60            InputSource::Raw { data, .. } => serde_json::from_slice(&data)?,
61            _ => anyhow::bail!("Invalid input for OpenAPI adapter"),
62        };
63
64        self.parse_openapi_spec(&spec)
65    }
66}
67
68impl OpenApiAdapter {
69    fn parse_openapi_spec(&self, spec: &serde_json::Value) -> Result<ContextNode> {
70        let title = spec
71            .get("info")
72            .and_then(|i| i.get("title"))
73            .and_then(|t| t.as_str())
74            .unwrap_or("API");
75
76        let version = spec
77            .get("info")
78            .and_then(|i| i.get("version"))
79            .and_then(|v| v.as_str())
80            .unwrap_or("1.0.0");
81
82        let mut root = ContextNode {
83            id: "api_root".to_string(),
84            name: format!("{} v{}", title, version),
85            node_type: NodeType::ApiSchema,
86            quantum_state: None,
87            children: vec![],
88            metadata: spec.get("info").cloned().unwrap_or_default(),
89            entanglements: vec![],
90        };
91
92        // Parse paths
93        if let Some(paths) = spec.get("paths").and_then(|p| p.as_object()) {
94            let mut path_nodes = Vec::new();
95
96            for (path, methods) in paths {
97                let mut path_node = ContextNode {
98                    id: path.clone(),
99                    name: path.clone(),
100                    node_type: NodeType::ApiEndpoint,
101                    quantum_state: None,
102                    children: vec![],
103                    metadata: serde_json::json!({}),
104                    entanglements: vec![],
105                };
106
107                // Parse methods
108                if let Some(methods_obj) = methods.as_object() {
109                    for (method, details) in methods_obj {
110                        if ["get", "post", "put", "delete", "patch", "head", "options"]
111                            .contains(&method.as_str())
112                        {
113                            let operation_id = details
114                                .get("operationId")
115                                .and_then(|o| o.as_str())
116                                .unwrap_or(method);
117
118                            let mut method_node = ContextNode {
119                                id: format!("{}_{}", path, method),
120                                name: format!("{} {}", method.to_uppercase(), operation_id),
121                                node_type: NodeType::ApiEndpoint,
122                                quantum_state: Some(
123                                    self.calculate_endpoint_quantum_state(method, details),
124                                ),
125                                children: vec![],
126                                metadata: details.clone(),
127                                entanglements: self
128                                    .find_endpoint_entanglements(path, method, details),
129                            };
130
131                            // Add parameter nodes
132                            if let Some(params) =
133                                details.get("parameters").and_then(|p| p.as_array())
134                            {
135                                for param in params {
136                                    let param_name = param
137                                        .get("name")
138                                        .and_then(|n| n.as_str())
139                                        .unwrap_or("param");
140
141                                    method_node.children.push(ContextNode {
142                                        id: format!("{}_{}_{}", path, method, param_name),
143                                        name: format!("param: {}", param_name),
144                                        node_type: NodeType::ApiSchema,
145                                        quantum_state: None,
146                                        children: vec![],
147                                        metadata: param.clone(),
148                                        entanglements: vec![],
149                                    });
150                                }
151                            }
152
153                            path_node.children.push(method_node);
154                        }
155                    }
156                }
157
158                path_nodes.push(path_node);
159            }
160
161            // Group by path segments
162            root.children.push(ContextNode {
163                id: "endpoints".to_string(),
164                name: "Endpoints".to_string(),
165                node_type: NodeType::Directory,
166                quantum_state: None,
167                children: self.organize_by_path_segments(path_nodes),
168                metadata: serde_json::json!({}),
169                entanglements: vec![],
170            });
171        }
172
173        // Parse schemas/components
174        if let Some(components) = spec.get("components").or_else(|| spec.get("definitions")) {
175            root.children.push(self.parse_schemas(components)?);
176        }
177
178        Ok(root)
179    }
180
181    fn calculate_endpoint_quantum_state(
182        &self,
183        method: &str,
184        details: &serde_json::Value,
185    ) -> QuantumState {
186        // Calculate quantum properties based on endpoint characteristics
187        let complexity = details
188            .get("parameters")
189            .and_then(|p| p.as_array())
190            .map(|a| a.len())
191            .unwrap_or(0) as f64;
192
193        let has_security = details.get("security").is_some();
194
195        QuantumState {
196            amplitude: match method {
197                "get" => 0.9,    // READ operations are stable
198                "post" => 0.7,   // CREATE operations
199                "put" => 0.6,    // UPDATE operations
200                "delete" => 0.5, // DELETE operations are disruptive
201                _ => 0.8,
202            },
203            frequency: 1.0 + complexity, // More params = higher frequency
204            phase: if has_security {
205                std::f64::consts::PI / 2.0
206            } else {
207                0.0
208            },
209            collapse_probability: if method == "delete" { 0.9 } else { 0.3 },
210        }
211    }
212
213    fn find_endpoint_entanglements(
214        &self,
215        path: &str,
216        _method: &str,
217        details: &serde_json::Value,
218    ) -> Vec<Entanglement> {
219        let mut entanglements = Vec::new();
220
221        // Find schema references
222        if let Some(schema_ref) = details
223            .get("requestBody")
224            .and_then(|r| r.get("content"))
225            .and_then(|c| c.get("application/json"))
226            .and_then(|j| j.get("schema"))
227            .and_then(|s| s.get("$ref"))
228            .and_then(|r| r.as_str())
229        {
230            if let Some(schema_name) = schema_ref.split('/').next_back() {
231                entanglements.push(Entanglement {
232                    target_id: format!("schema_{}", schema_name),
233                    strength: 0.9,
234                    relationship: "uses_schema".to_string(),
235                });
236            }
237        }
238
239        // Find related endpoints (same resource)
240        let resource = path.split('/').nth(1).unwrap_or("");
241        if !resource.is_empty() {
242            entanglements.push(Entanglement {
243                target_id: format!("resource_{}", resource),
244                strength: 0.7,
245                relationship: "same_resource".to_string(),
246            });
247        }
248
249        entanglements
250    }
251
252    fn organize_by_path_segments(&self, paths: Vec<ContextNode>) -> Vec<ContextNode> {
253        // For simplicity, return as-is
254        // In real implementation, would group by common path prefixes
255        paths
256    }
257
258    fn parse_schemas(&self, components: &serde_json::Value) -> Result<ContextNode> {
259        let mut schemas_node = ContextNode {
260            id: "schemas".to_string(),
261            name: "Schemas".to_string(),
262            node_type: NodeType::Directory,
263            quantum_state: None,
264            children: vec![],
265            metadata: serde_json::json!({}),
266            entanglements: vec![],
267        };
268
269        if let Some(schemas) = components.get("schemas").and_then(|s| s.as_object()) {
270            for (name, schema) in schemas {
271                schemas_node.children.push(ContextNode {
272                    id: format!("schema_{}", name),
273                    name: name.clone(),
274                    node_type: NodeType::ApiSchema,
275                    quantum_state: None,
276                    children: self.parse_schema_properties(schema),
277                    metadata: schema.clone(),
278                    entanglements: vec![],
279                });
280            }
281        }
282
283        Ok(schemas_node)
284    }
285
286    fn parse_schema_properties(&self, schema: &serde_json::Value) -> Vec<ContextNode> {
287        let mut props = Vec::new();
288
289        if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
290            for (prop_name, prop_schema) in properties {
291                props.push(ContextNode {
292                    id: format!("prop_{}", prop_name),
293                    name: prop_name.clone(),
294                    node_type: NodeType::ApiSchema,
295                    quantum_state: None,
296                    children: vec![],
297                    metadata: prop_schema.clone(),
298                    entanglements: vec![],
299                });
300            }
301        }
302
303        props
304    }
305}