1use crate::openapi::swagger_convert;
9use crate::{Error, Result};
10use openapiv3::{OpenAPI, ReferenceOr, Schema};
11use std::collections::HashSet;
12use std::path::Path;
13use tokio::fs;
14
15#[derive(Debug, Clone)]
17pub struct OpenApiSpec {
18 pub spec: OpenAPI,
20 pub file_path: Option<String>,
22 pub raw_document: Option<serde_json::Value>,
24}
25
26impl OpenApiSpec {
27 pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
32 let path_ref = path.as_ref();
33 let content = fs::read_to_string(path_ref)
34 .await
35 .map_err(|e| Error::generic(format!("Failed to read OpenAPI spec file: {}", e)))?;
36
37 let raw_json = if path_ref.extension().and_then(|s| s.to_str()) == Some("yaml")
38 || path_ref.extension().and_then(|s| s.to_str()) == Some("yml")
39 {
40 let yaml_value: serde_yaml::Value = serde_yaml::from_str(&content)
41 .map_err(|e| Error::generic(format!("Failed to parse YAML OpenAPI spec: {}", e)))?;
42 serde_json::to_value(&yaml_value).map_err(|e| {
43 Error::generic(format!("Failed to convert YAML OpenAPI spec to JSON: {}", e))
44 })?
45 } else {
46 serde_json::from_str(&content)
47 .map_err(|e| Error::generic(format!("Failed to parse JSON OpenAPI spec: {}", e)))?
48 };
49
50 let (raw_document, spec) = if swagger_convert::is_swagger_2(&raw_json) {
52 tracing::info!("Detected Swagger 2.0 specification, converting to OpenAPI 3.0");
53 let converted = swagger_convert::convert_swagger_to_openapi3(&raw_json)
54 .map_err(|e| Error::generic(format!("Failed to convert Swagger 2.0 to OpenAPI 3.0: {}", e)))?;
55 let spec: OpenAPI = serde_json::from_value(converted.clone())
56 .map_err(|e| Error::generic(format!("Failed to parse converted OpenAPI spec: {}", e)))?;
57 (converted, spec)
58 } else {
59 let spec: OpenAPI = serde_json::from_value(raw_json.clone())
60 .map_err(|e| Error::generic(format!("Failed to read OpenAPI spec: {}", e)))?;
61 (raw_json, spec)
62 };
63
64 Ok(Self {
65 spec,
66 file_path: path_ref.to_str().map(|s| s.to_string()),
67 raw_document: Some(raw_document),
68 })
69 }
70
71 pub fn from_string(content: &str, format: Option<&str>) -> Result<Self> {
76 let raw_json = if format == Some("yaml") || format == Some("yml") {
77 let yaml_value: serde_yaml::Value = serde_yaml::from_str(content)
78 .map_err(|e| Error::generic(format!("Failed to parse YAML OpenAPI spec: {}", e)))?;
79 serde_json::to_value(&yaml_value).map_err(|e| {
80 Error::generic(format!("Failed to convert YAML OpenAPI spec to JSON: {}", e))
81 })?
82 } else {
83 serde_json::from_str(content)
84 .map_err(|e| Error::generic(format!("Failed to parse JSON OpenAPI spec: {}", e)))?
85 };
86
87 let (raw_document, spec) = if swagger_convert::is_swagger_2(&raw_json) {
89 let converted = swagger_convert::convert_swagger_to_openapi3(&raw_json)
90 .map_err(|e| Error::generic(format!("Failed to convert Swagger 2.0 to OpenAPI 3.0: {}", e)))?;
91 let spec: OpenAPI = serde_json::from_value(converted.clone())
92 .map_err(|e| Error::generic(format!("Failed to parse converted OpenAPI spec: {}", e)))?;
93 (converted, spec)
94 } else {
95 let spec: OpenAPI = serde_json::from_value(raw_json.clone())
96 .map_err(|e| Error::generic(format!("Failed to read OpenAPI spec: {}", e)))?;
97 (raw_json, spec)
98 };
99
100 Ok(Self {
101 spec,
102 file_path: None,
103 raw_document: Some(raw_document),
104 })
105 }
106
107 pub fn from_json(json: serde_json::Value) -> Result<Self> {
112 let (raw_document, spec) = if swagger_convert::is_swagger_2(&json) {
114 let converted = swagger_convert::convert_swagger_to_openapi3(&json)
115 .map_err(|e| Error::generic(format!("Failed to convert Swagger 2.0 to OpenAPI 3.0: {}", e)))?;
116 let spec: OpenAPI = serde_json::from_value(converted.clone())
117 .map_err(|e| Error::generic(format!("Failed to parse converted OpenAPI spec: {}", e)))?;
118 (converted, spec)
119 } else {
120 let json_for_doc = json.clone();
121 let spec: OpenAPI = serde_json::from_value(json)
122 .map_err(|e| Error::generic(format!("Failed to parse JSON OpenAPI spec: {}", e)))?;
123 (json_for_doc, spec)
124 };
125
126 Ok(Self {
127 spec,
128 file_path: None,
129 raw_document: Some(raw_document),
130 })
131 }
132
133 pub fn validate(&self) -> Result<()> {
138 if self.spec.paths.paths.is_empty() {
140 return Err(Error::generic("OpenAPI spec must contain at least one path"));
141 }
142
143 if self.spec.info.title.is_empty() {
145 return Err(Error::generic("OpenAPI spec info must have a title"));
146 }
147
148 if self.spec.info.version.is_empty() {
149 return Err(Error::generic("OpenAPI spec info must have a version"));
150 }
151
152 Ok(())
153 }
154
155 pub fn validate_enhanced(&self) -> crate::spec_parser::ValidationResult {
157 if let Some(raw) = &self.raw_document {
159 let format = if raw.get("swagger").is_some() {
160 crate::spec_parser::SpecFormat::OpenApi20
161 } else if let Some(version) = raw.get("openapi").and_then(|v| v.as_str()) {
162 if version.starts_with("3.1") {
163 crate::spec_parser::SpecFormat::OpenApi31
164 } else {
165 crate::spec_parser::SpecFormat::OpenApi30
166 }
167 } else {
168 crate::spec_parser::SpecFormat::OpenApi30
170 };
171 crate::spec_parser::OpenApiValidator::validate(raw, format)
172 } else {
173 crate::spec_parser::ValidationResult::failure(vec![
175 crate::spec_parser::ValidationError::new(
176 "Cannot perform enhanced validation without raw document".to_string(),
177 ),
178 ])
179 }
180 }
181
182 pub fn version(&self) -> &str {
184 &self.spec.openapi
185 }
186
187 pub fn title(&self) -> &str {
189 &self.spec.info.title
190 }
191
192 pub fn description(&self) -> Option<&str> {
194 self.spec.info.description.as_deref()
195 }
196
197 pub fn api_version(&self) -> &str {
199 &self.spec.info.version
200 }
201
202 pub fn servers(&self) -> &[openapiv3::Server] {
204 &self.spec.servers
205 }
206
207 pub fn paths(&self) -> &openapiv3::Paths {
209 &self.spec.paths
210 }
211
212 pub fn schemas(
214 &self,
215 ) -> Option<&indexmap::IndexMap<String, openapiv3::ReferenceOr<openapiv3::Schema>>> {
216 self.spec.components.as_ref().map(|c| &c.schemas)
217 }
218
219 pub fn security_schemes(
221 &self,
222 ) -> Option<&indexmap::IndexMap<String, openapiv3::ReferenceOr<openapiv3::SecurityScheme>>>
223 {
224 self.spec.components.as_ref().map(|c| &c.security_schemes)
225 }
226
227 pub fn operations_for_path(
229 &self,
230 path: &str,
231 ) -> std::collections::HashMap<String, openapiv3::Operation> {
232 let mut operations = std::collections::HashMap::new();
233
234 if let Some(path_item_ref) = self.spec.paths.paths.get(path) {
235 if let Some(path_item) = path_item_ref.as_item() {
237 if let Some(op) = &path_item.get {
238 operations.insert("GET".to_string(), op.clone());
239 }
240 if let Some(op) = &path_item.post {
241 operations.insert("POST".to_string(), op.clone());
242 }
243 if let Some(op) = &path_item.put {
244 operations.insert("PUT".to_string(), op.clone());
245 }
246 if let Some(op) = &path_item.delete {
247 operations.insert("DELETE".to_string(), op.clone());
248 }
249 if let Some(op) = &path_item.patch {
250 operations.insert("PATCH".to_string(), op.clone());
251 }
252 if let Some(op) = &path_item.head {
253 operations.insert("HEAD".to_string(), op.clone());
254 }
255 if let Some(op) = &path_item.options {
256 operations.insert("OPTIONS".to_string(), op.clone());
257 }
258 if let Some(op) = &path_item.trace {
259 operations.insert("TRACE".to_string(), op.clone());
260 }
261 }
262 }
263
264 operations
265 }
266
267 pub fn all_paths_and_operations(
269 &self,
270 ) -> std::collections::HashMap<String, std::collections::HashMap<String, openapiv3::Operation>>
271 {
272 self.spec
273 .paths
274 .paths
275 .iter()
276 .map(|(path, _)| (path.clone(), self.operations_for_path(path)))
277 .collect()
278 }
279
280 pub fn get_schema(&self, reference: &str) -> Option<crate::openapi::schema::OpenApiSchema> {
282 self.resolve_schema(reference).map(crate::openapi::schema::OpenApiSchema::new)
283 }
284
285 pub fn validate_security_requirements(
287 &self,
288 security_requirements: &[openapiv3::SecurityRequirement],
289 auth_header: Option<&str>,
290 api_key: Option<&str>,
291 ) -> Result<()> {
292 if security_requirements.is_empty() {
293 return Ok(());
294 }
295
296 for requirement in security_requirements {
298 if self.is_security_requirement_satisfied(requirement, auth_header, api_key)? {
299 return Ok(());
300 }
301 }
302
303 Err(Error::generic("Security validation failed: no valid authentication provided"))
304 }
305
306 fn resolve_schema(&self, reference: &str) -> Option<Schema> {
307 let mut visited = HashSet::new();
308 self.resolve_schema_recursive(reference, &mut visited)
309 }
310
311 fn resolve_schema_recursive(
312 &self,
313 reference: &str,
314 visited: &mut HashSet<String>,
315 ) -> Option<Schema> {
316 if !visited.insert(reference.to_string()) {
317 tracing::warn!("Detected recursive schema reference: {}", reference);
318 return None;
319 }
320
321 let schema_name = reference.strip_prefix("#/components/schemas/")?;
322 let components = self.spec.components.as_ref()?;
323 let schema_ref = components.schemas.get(schema_name)?;
324
325 match schema_ref {
326 ReferenceOr::Item(schema) => Some(schema.clone()),
327 ReferenceOr::Reference { reference: nested } => {
328 self.resolve_schema_recursive(nested, visited)
329 }
330 }
331 }
332
333 fn is_security_requirement_satisfied(
335 &self,
336 requirement: &openapiv3::SecurityRequirement,
337 auth_header: Option<&str>,
338 api_key: Option<&str>,
339 ) -> Result<bool> {
340 for (scheme_name, _scopes) in requirement {
342 if !self.is_security_scheme_satisfied(scheme_name, auth_header, api_key)? {
343 return Ok(false);
344 }
345 }
346 Ok(true)
347 }
348
349 fn is_security_scheme_satisfied(
351 &self,
352 scheme_name: &str,
353 auth_header: Option<&str>,
354 api_key: Option<&str>,
355 ) -> Result<bool> {
356 let security_schemes = match self.security_schemes() {
357 Some(schemes) => schemes,
358 None => return Ok(false),
359 };
360
361 let scheme = match security_schemes.get(scheme_name) {
362 Some(scheme) => scheme,
363 None => {
364 return Err(Error::generic(format!("Security scheme '{}' not found", scheme_name)))
365 }
366 };
367
368 let scheme = match scheme {
369 openapiv3::ReferenceOr::Item(s) => s,
370 openapiv3::ReferenceOr::Reference { .. } => {
371 return Err(Error::generic("Referenced security schemes not supported"))
372 }
373 };
374
375 match scheme {
376 openapiv3::SecurityScheme::HTTP { scheme, .. } => {
377 match scheme.as_str() {
378 "bearer" => match auth_header {
379 Some(header) if header.starts_with("Bearer ") => Ok(true),
380 _ => Ok(false),
381 },
382 "basic" => match auth_header {
383 Some(header) if header.starts_with("Basic ") => Ok(true),
384 _ => Ok(false),
385 },
386 _ => Ok(false), }
388 }
389 openapiv3::SecurityScheme::APIKey { location, .. } => {
390 match location {
391 openapiv3::APIKeyLocation::Header => Ok(auth_header.is_some()),
392 openapiv3::APIKeyLocation::Query => Ok(api_key.is_some()),
393 _ => Ok(false), }
395 }
396 openapiv3::SecurityScheme::OpenIDConnect { .. } => Ok(false), openapiv3::SecurityScheme::OAuth2 { .. } => {
398 match auth_header {
400 Some(header) if header.starts_with("Bearer ") => Ok(true),
401 _ => Ok(false),
402 }
403 }
404 }
405 }
406
407 pub fn get_global_security_requirements(&self) -> Vec<openapiv3::SecurityRequirement> {
409 self.spec.security.clone().unwrap_or_default()
410 }
411
412 pub fn get_request_body(&self, reference: &str) -> Option<&openapiv3::RequestBody> {
414 if let Some(components) = &self.spec.components {
415 if let Some(param_name) = reference.strip_prefix("#/components/requestBodies/") {
416 if let Some(request_body_ref) = components.request_bodies.get(param_name) {
417 return request_body_ref.as_item();
418 }
419 }
420 }
421 None
422 }
423
424 pub fn get_response(&self, reference: &str) -> Option<&openapiv3::Response> {
426 if let Some(components) = &self.spec.components {
427 if let Some(response_name) = reference.strip_prefix("#/components/responses/") {
428 if let Some(response_ref) = components.responses.get(response_name) {
429 return response_ref.as_item();
430 }
431 }
432 }
433 None
434 }
435
436 pub fn get_example(&self, reference: &str) -> Option<&openapiv3::Example> {
438 if let Some(components) = &self.spec.components {
439 if let Some(example_name) = reference.strip_prefix("#/components/examples/") {
440 if let Some(example_ref) = components.examples.get(example_name) {
441 return example_ref.as_item();
442 }
443 }
444 }
445 None
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use openapiv3::{SchemaKind, Type};
453
454 #[test]
455 fn resolves_nested_schema_references() {
456 let yaml = r#"
457openapi: 3.0.3
458info:
459 title: Test API
460 version: "1.0.0"
461paths: {}
462components:
463 schemas:
464 Apiary:
465 type: object
466 properties:
467 id:
468 type: string
469 hive:
470 $ref: '#/components/schemas/Hive'
471 Hive:
472 type: object
473 properties:
474 name:
475 type: string
476 HiveWrapper:
477 $ref: '#/components/schemas/Hive'
478 "#;
479
480 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("spec parses");
481
482 let apiary = spec.get_schema("#/components/schemas/Apiary").expect("resolve apiary schema");
483 assert!(matches!(apiary.schema.schema_kind, SchemaKind::Type(Type::Object(_))));
484
485 let wrapper = spec
486 .get_schema("#/components/schemas/HiveWrapper")
487 .expect("resolve wrapper schema");
488 assert!(matches!(wrapper.schema.schema_kind, SchemaKind::Type(Type::Object(_))));
489 }
490}