1use crate::swagger_convert;
9use mockforge_foundation::error::{Error, Result};
10use openapiv3::{OpenAPI, ReferenceOr, Schema};
11use std::collections::HashSet;
12use std::path::Path;
13use tokio::fs;
14use tracing;
15
16#[derive(Debug, Clone)]
18pub struct OpenApiSpec {
19 pub spec: OpenAPI,
21 pub file_path: Option<String>,
23 pub raw_document: Option<serde_json::Value>,
25}
26
27impl OpenApiSpec {
28 pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
33 let path_ref = path.as_ref();
34 let content = fs::read_to_string(path_ref)
35 .await
36 .map_err(|e| Error::io_with_context("reading OpenAPI spec file", e.to_string()))?;
37
38 let raw_json = if path_ref.extension().and_then(|s| s.to_str()) == Some("yaml")
39 || path_ref.extension().and_then(|s| s.to_str()) == Some("yml")
40 {
41 let yaml_value: serde_yaml::Value = serde_yaml::from_str(&content)
42 .map_err(|e| Error::config(format!("Failed to parse YAML OpenAPI spec: {}", e)))?;
43 serde_json::to_value(&yaml_value).map_err(|e| {
44 Error::config(format!("Failed to convert YAML OpenAPI spec to JSON: {}", e))
45 })?
46 } else {
47 serde_json::from_str(&content)
48 .map_err(|e| Error::config(format!("Failed to parse JSON OpenAPI spec: {}", e)))?
49 };
50
51 let (raw_document, spec) = if swagger_convert::is_swagger_2(&raw_json) {
53 tracing::info!("Detected Swagger 2.0 specification, converting to OpenAPI 3.0");
54 let converted =
55 swagger_convert::convert_swagger_to_openapi3(&raw_json).map_err(|e| {
56 Error::config(format!("Failed to convert Swagger 2.0 to OpenAPI 3.0: {}", e))
57 })?;
58 let spec: OpenAPI = serde_json::from_value(converted.clone()).map_err(|e| {
59 Error::config(format!("Failed to parse converted OpenAPI spec: {}", e))
60 })?;
61 (converted, spec)
62 } else {
63 let spec: OpenAPI = serde_json::from_value(raw_json.clone()).map_err(|e| {
64 let error_str = format!("{}", e);
66 let mut error_msg = format!("Failed to read OpenAPI spec: {}", e);
67
68 if error_str.contains("missing field") {
70 tracing::error!("OpenAPI deserialization error: {}", error_str);
71
72 if let Some(info) = raw_json.get("info") {
74 if let Some(info_obj) = info.as_object() {
75 let has_desc = info_obj.contains_key("description");
76 error_msg
77 .push_str(&format!(" | Info.description present: {}", has_desc));
78 }
79 }
80 if let Some(servers) = raw_json.get("servers") {
81 if let Some(servers_arr) = servers.as_array() {
82 error_msg.push_str(&format!(" | Servers count: {}", servers_arr.len()));
83 }
84 }
85 }
86
87 Error::config(error_msg)
88 })?;
89 (raw_json, spec)
90 };
91
92 Ok(Self {
93 spec,
94 file_path: path_ref.to_str().map(|s| s.to_string()),
95 raw_document: Some(raw_document),
96 })
97 }
98
99 pub fn from_string(content: &str, format: Option<&str>) -> Result<Self> {
104 let raw_json = if format == Some("yaml") || format == Some("yml") {
105 let yaml_value: serde_yaml::Value = serde_yaml::from_str(content)
106 .map_err(|e| Error::config(format!("Failed to parse YAML OpenAPI spec: {}", e)))?;
107 serde_json::to_value(&yaml_value).map_err(|e| {
108 Error::config(format!("Failed to convert YAML OpenAPI spec to JSON: {}", e))
109 })?
110 } else {
111 serde_json::from_str(content)
112 .map_err(|e| Error::config(format!("Failed to parse JSON OpenAPI spec: {}", e)))?
113 };
114
115 let (raw_document, spec) = if swagger_convert::is_swagger_2(&raw_json) {
117 let converted =
118 swagger_convert::convert_swagger_to_openapi3(&raw_json).map_err(|e| {
119 Error::config(format!("Failed to convert Swagger 2.0 to OpenAPI 3.0: {}", e))
120 })?;
121 let spec: OpenAPI = serde_json::from_value(converted.clone()).map_err(|e| {
122 Error::config(format!("Failed to parse converted OpenAPI spec: {}", e))
123 })?;
124 (converted, spec)
125 } else {
126 let spec: OpenAPI = serde_json::from_value(raw_json.clone())
127 .map_err(|e| Error::io_with_context("reading OpenAPI spec", e.to_string()))?;
128 (raw_json, spec)
129 };
130
131 Ok(Self {
132 spec,
133 file_path: None,
134 raw_document: Some(raw_document),
135 })
136 }
137
138 pub fn from_json(json: serde_json::Value) -> Result<Self> {
143 let (raw_document, spec) = if swagger_convert::is_swagger_2(&json) {
145 let converted = swagger_convert::convert_swagger_to_openapi3(&json).map_err(|e| {
146 Error::config(format!("Failed to convert Swagger 2.0 to OpenAPI 3.0: {}", e))
147 })?;
148 let spec: OpenAPI = serde_json::from_value(converted.clone()).map_err(|e| {
149 Error::config(format!("Failed to parse converted OpenAPI spec: {}", e))
150 })?;
151 (converted, spec)
152 } else {
153 let json_for_doc = json.clone();
154 let spec: OpenAPI = serde_json::from_value(json)
155 .map_err(|e| Error::config(format!("Failed to parse JSON OpenAPI spec: {}", e)))?;
156 (json_for_doc, spec)
157 };
158
159 Ok(Self {
160 spec,
161 file_path: None,
162 raw_document: Some(raw_document),
163 })
164 }
165
166 pub fn validate(&self) -> Result<()> {
171 if self.spec.paths.paths.is_empty() {
173 return Err(Error::validation("OpenAPI spec must contain at least one path"));
174 }
175
176 if self.spec.info.title.is_empty() {
178 return Err(Error::validation("OpenAPI spec info must have a title"));
179 }
180
181 if self.spec.info.version.is_empty() {
182 return Err(Error::validation("OpenAPI spec info must have a version"));
183 }
184
185 Ok(())
186 }
187
188 pub fn validate_enhanced(&self) -> crate::spec_parser::ValidationResult {
190 if let Some(raw) = &self.raw_document {
192 let format = if raw.get("swagger").is_some() {
193 crate::spec_parser::SpecFormat::OpenApi20
194 } else if let Some(version) = raw.get("openapi").and_then(|v| v.as_str()) {
195 if version.starts_with("3.1") {
196 crate::spec_parser::SpecFormat::OpenApi31
197 } else {
198 crate::spec_parser::SpecFormat::OpenApi30
199 }
200 } else {
201 crate::spec_parser::SpecFormat::OpenApi30
203 };
204 crate::spec_parser::OpenApiValidator::validate(raw, format)
205 } else {
206 crate::spec_parser::ValidationResult::failure(vec![
208 crate::spec_parser::ValidationError::new(
209 "Cannot perform enhanced validation without raw document".to_string(),
210 ),
211 ])
212 }
213 }
214
215 pub fn version(&self) -> &str {
217 &self.spec.openapi
218 }
219
220 pub fn title(&self) -> &str {
222 &self.spec.info.title
223 }
224
225 pub fn description(&self) -> Option<&str> {
227 self.spec.info.description.as_deref()
228 }
229
230 pub fn api_version(&self) -> &str {
232 &self.spec.info.version
233 }
234
235 pub fn servers(&self) -> &[openapiv3::Server] {
237 &self.spec.servers
238 }
239
240 pub fn paths(&self) -> &openapiv3::Paths {
242 &self.spec.paths
243 }
244
245 pub fn schemas(&self) -> Option<&indexmap::IndexMap<String, ReferenceOr<Schema>>> {
247 self.spec.components.as_ref().map(|c| &c.schemas)
248 }
249
250 pub fn security_schemes(
252 &self,
253 ) -> Option<&indexmap::IndexMap<String, ReferenceOr<openapiv3::SecurityScheme>>> {
254 self.spec.components.as_ref().map(|c| &c.security_schemes)
255 }
256
257 pub fn operations_for_path(
259 &self,
260 path: &str,
261 ) -> std::collections::HashMap<String, openapiv3::Operation> {
262 let mut operations = std::collections::HashMap::new();
263
264 if let Some(path_item_ref) = self.spec.paths.paths.get(path) {
265 if let Some(path_item) = path_item_ref.as_item() {
267 let resolved_path_params: Vec<openapiv3::ReferenceOr<openapiv3::Parameter>> =
285 path_item.parameters.iter().map(|p| self.resolve_parameter_ref(p)).collect();
286 let merge = |op: &openapiv3::Operation| -> openapiv3::Operation {
287 let mut resolved_op = op.clone();
289 resolved_op.parameters =
290 op.parameters.iter().map(|p| self.resolve_parameter_ref(p)).collect();
291 merge_path_params_into_operation(&resolved_op, &resolved_path_params)
292 };
293 if let Some(op) = &path_item.get {
294 operations.insert("GET".to_string(), merge(op));
295 }
296 if let Some(op) = &path_item.post {
297 operations.insert("POST".to_string(), merge(op));
298 }
299 if let Some(op) = &path_item.put {
300 operations.insert("PUT".to_string(), merge(op));
301 }
302 if let Some(op) = &path_item.delete {
303 operations.insert("DELETE".to_string(), merge(op));
304 }
305 if let Some(op) = &path_item.patch {
306 operations.insert("PATCH".to_string(), merge(op));
307 }
308 if let Some(op) = &path_item.head {
309 operations.insert("HEAD".to_string(), merge(op));
310 }
311 if let Some(op) = &path_item.options {
312 operations.insert("OPTIONS".to_string(), merge(op));
313 }
314 if let Some(op) = &path_item.trace {
315 operations.insert("TRACE".to_string(), merge(op));
316 }
317 }
318 }
319
320 operations
321 }
322
323 pub fn all_paths_and_operations(
325 &self,
326 ) -> std::collections::HashMap<String, std::collections::HashMap<String, openapiv3::Operation>>
327 {
328 self.spec
329 .paths
330 .paths
331 .iter()
332 .map(|(path, _)| (path.clone(), self.operations_for_path(path)))
333 .collect()
334 }
335
336 pub fn get_schema(&self, reference: &str) -> Option<crate::schema::OpenApiSchema> {
338 self.resolve_schema(reference).map(crate::schema::OpenApiSchema::new)
339 }
340
341 pub fn resolve_schema_ref(&self, reference: &str) -> Option<Schema> {
346 self.resolve_schema(reference)
347 }
348
349 pub fn resolve_parameter_ref(
363 &self,
364 p_ref: &openapiv3::ReferenceOr<openapiv3::Parameter>,
365 ) -> openapiv3::ReferenceOr<openapiv3::Parameter> {
366 match p_ref {
367 openapiv3::ReferenceOr::Item(_) => p_ref.clone(),
368 openapiv3::ReferenceOr::Reference { reference } => {
369 let Some(name) = reference.strip_prefix("#/components/parameters/") else {
370 return p_ref.clone();
371 };
372 let Some(components) = self.spec.components.as_ref() else {
373 return p_ref.clone();
374 };
375 match components.parameters.get(name) {
376 Some(openapiv3::ReferenceOr::Item(p)) => {
377 openapiv3::ReferenceOr::Item(p.clone())
378 }
379 Some(openapiv3::ReferenceOr::Reference { reference: nested }) => {
380 let Some(nested_name) = nested.strip_prefix("#/components/parameters/")
383 else {
384 return p_ref.clone();
385 };
386 match components.parameters.get(nested_name) {
387 Some(openapiv3::ReferenceOr::Item(p)) => {
388 openapiv3::ReferenceOr::Item(p.clone())
389 }
390 _ => p_ref.clone(),
391 }
392 }
393 None => p_ref.clone(),
394 }
395 }
396 }
397 }
398
399 pub fn validate_security_requirements(
401 &self,
402 security_requirements: &[openapiv3::SecurityRequirement],
403 auth_header: Option<&str>,
404 api_key: Option<&str>,
405 ) -> Result<()> {
406 if security_requirements.is_empty() {
407 return Ok(());
408 }
409
410 for requirement in security_requirements {
412 if self.is_security_requirement_satisfied(requirement, auth_header, api_key)? {
413 return Ok(());
414 }
415 }
416
417 Err(Error::validation(
418 "Security validation failed: no valid authentication provided",
419 ))
420 }
421
422 fn resolve_schema(&self, reference: &str) -> Option<Schema> {
423 let mut visited = HashSet::new();
424 self.resolve_schema_recursive(reference, &mut visited)
425 }
426
427 fn resolve_schema_recursive(
428 &self,
429 reference: &str,
430 visited: &mut HashSet<String>,
431 ) -> Option<Schema> {
432 if !visited.insert(reference.to_string()) {
433 tracing::warn!("Detected recursive schema reference: {}", reference);
434 return None;
435 }
436
437 let schema_name = reference.strip_prefix("#/components/schemas/")?;
438 let components = self.spec.components.as_ref()?;
439 let schema_ref = components.schemas.get(schema_name)?;
440
441 match schema_ref {
442 ReferenceOr::Item(schema) => Some(schema.clone()),
443 ReferenceOr::Reference { reference: nested } => {
444 self.resolve_schema_recursive(nested, visited)
445 }
446 }
447 }
448
449 fn is_security_requirement_satisfied(
451 &self,
452 requirement: &openapiv3::SecurityRequirement,
453 auth_header: Option<&str>,
454 api_key: Option<&str>,
455 ) -> Result<bool> {
456 for (scheme_name, _scopes) in requirement {
458 if !self.is_security_scheme_satisfied(scheme_name, auth_header, api_key)? {
459 return Ok(false);
460 }
461 }
462 Ok(true)
463 }
464
465 fn is_security_scheme_satisfied(
467 &self,
468 scheme_name: &str,
469 auth_header: Option<&str>,
470 api_key: Option<&str>,
471 ) -> Result<bool> {
472 let security_schemes = match self.security_schemes() {
473 Some(schemes) => schemes,
474 None => return Ok(false),
475 };
476
477 let scheme = match security_schemes.get(scheme_name) {
478 Some(scheme) => scheme,
479 None => {
480 return Err(Error::config(format!("Security scheme '{}' not found", scheme_name)))
481 }
482 };
483
484 let scheme = match scheme {
485 ReferenceOr::Item(s) => s,
486 ReferenceOr::Reference { reference } => {
487 let ref_name =
489 reference.strip_prefix("#/components/securitySchemes/").ok_or_else(|| {
490 Error::config(format!(
491 "Unsupported security scheme reference format: {}",
492 reference
493 ))
494 })?;
495 match security_schemes.get(ref_name) {
496 Some(ReferenceOr::Item(resolved)) => resolved,
497 Some(ReferenceOr::Reference { .. }) => {
498 return Err(Error::config(format!(
499 "Nested security scheme reference not supported: {}",
500 ref_name
501 )))
502 }
503 None => {
504 return Err(Error::config(format!(
505 "Security scheme '{}' not found",
506 ref_name
507 )))
508 }
509 }
510 }
511 };
512
513 match scheme {
514 openapiv3::SecurityScheme::HTTP { scheme, .. } => {
515 match scheme.as_str() {
516 "bearer" => match auth_header {
517 Some(header) if header.starts_with("Bearer ") => Ok(true),
518 _ => Ok(false),
519 },
520 "basic" => match auth_header {
521 Some(header) if header.starts_with("Basic ") => Ok(true),
522 _ => Ok(false),
523 },
524 _ => Ok(false), }
526 }
527 openapiv3::SecurityScheme::APIKey { location, .. } => match location {
528 openapiv3::APIKeyLocation::Header => Ok(auth_header.is_some()),
529 openapiv3::APIKeyLocation::Query => Ok(api_key.is_some()),
530 openapiv3::APIKeyLocation::Cookie => Ok(api_key.is_some()),
531 },
532 openapiv3::SecurityScheme::OpenIDConnect { .. } => {
533 match auth_header {
535 Some(header) if header.starts_with("Bearer ") => Ok(true),
536 _ => Ok(false),
537 }
538 }
539 openapiv3::SecurityScheme::OAuth2 { .. } => {
540 match auth_header {
542 Some(header) if header.starts_with("Bearer ") => Ok(true),
543 _ => Ok(false),
544 }
545 }
546 }
547 }
548
549 pub fn get_global_security_requirements(&self) -> Vec<openapiv3::SecurityRequirement> {
551 self.spec.security.clone().unwrap_or_default()
552 }
553
554 pub fn get_request_body(&self, reference: &str) -> Option<&openapiv3::RequestBody> {
556 if let Some(components) = &self.spec.components {
557 if let Some(param_name) = reference.strip_prefix("#/components/requestBodies/") {
558 if let Some(request_body_ref) = components.request_bodies.get(param_name) {
559 return request_body_ref.as_item();
560 }
561 }
562 }
563 None
564 }
565
566 pub fn get_response(&self, reference: &str) -> Option<&openapiv3::Response> {
568 if let Some(components) = &self.spec.components {
569 if let Some(response_name) = reference.strip_prefix("#/components/responses/") {
570 if let Some(response_ref) = components.responses.get(response_name) {
571 return response_ref.as_item();
572 }
573 }
574 }
575 None
576 }
577
578 pub fn get_example(&self, reference: &str) -> Option<&openapiv3::Example> {
580 if let Some(components) = &self.spec.components {
581 if let Some(example_name) = reference.strip_prefix("#/components/examples/") {
582 if let Some(example_ref) = components.examples.get(example_name) {
583 return example_ref.as_item();
584 }
585 }
586 }
587 None
588 }
589}
590
591pub(crate) fn merge_path_params_into_operation(
600 operation: &openapiv3::Operation,
601 path_level_params: &[openapiv3::ReferenceOr<openapiv3::Parameter>],
602) -> openapiv3::Operation {
603 use std::collections::HashSet;
604 if path_level_params.is_empty() {
605 return operation.clone();
606 }
607 let mut op_keys: HashSet<(String, String)> = HashSet::new();
608 for p_ref in &operation.parameters {
609 if let Some(key) = parameter_key(p_ref) {
610 op_keys.insert(key);
611 }
612 }
613 let mut merged: Vec<openapiv3::ReferenceOr<openapiv3::Parameter>> =
614 Vec::with_capacity(path_level_params.len() + operation.parameters.len());
615 for p_ref in path_level_params {
616 match parameter_key(p_ref) {
617 Some(key) if op_keys.contains(&key) => {}
618 _ => merged.push(p_ref.clone()),
619 }
620 }
621 merged.extend(operation.parameters.iter().cloned());
622 let mut cloned = operation.clone();
623 cloned.parameters = merged;
624 cloned
625}
626
627fn parameter_key(p_ref: &openapiv3::ReferenceOr<openapiv3::Parameter>) -> Option<(String, String)> {
628 let p = p_ref.as_item()?;
629 let (name, in_loc) = match p {
630 openapiv3::Parameter::Path { parameter_data, .. } => (parameter_data.name.clone(), "path"),
631 openapiv3::Parameter::Query { parameter_data, .. } => {
632 (parameter_data.name.clone(), "query")
633 }
634 openapiv3::Parameter::Header { parameter_data, .. } => {
635 (parameter_data.name.clone(), "header")
636 }
637 openapiv3::Parameter::Cookie { parameter_data, .. } => {
638 (parameter_data.name.clone(), "cookie")
639 }
640 };
641 Some((name, in_loc.to_string()))
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647 use openapiv3::{SchemaKind, Type};
648
649 #[test]
650 fn resolves_security_scheme_ref() {
651 let yaml = r#"
652openapi: 3.0.3
653info:
654 title: Test API
655 version: "1.0.0"
656paths:
657 /test:
658 get:
659 security:
660 - BearerRef: []
661 responses:
662 '200':
663 description: OK
664components:
665 securitySchemes:
666 BearerAuth:
667 type: http
668 scheme: bearer
669 BearerRef:
670 $ref: '#/components/securitySchemes/BearerAuth'
671 "#;
672
673 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("spec parses");
674
675 let result = spec
677 .is_security_scheme_satisfied("BearerRef", Some("Bearer token123"), None)
678 .expect("should resolve ref");
679 assert!(result);
680
681 let result = spec
683 .is_security_scheme_satisfied("BearerRef", None, None)
684 .expect("should resolve ref");
685 assert!(!result);
686 }
687
688 #[test]
689 fn resolves_nested_schema_references() {
690 let yaml = r#"
691openapi: 3.0.3
692info:
693 title: Test API
694 version: "1.0.0"
695paths: {}
696components:
697 schemas:
698 Apiary:
699 type: object
700 properties:
701 id:
702 type: string
703 hive:
704 $ref: '#/components/schemas/Hive'
705 Hive:
706 type: object
707 properties:
708 name:
709 type: string
710 HiveWrapper:
711 $ref: '#/components/schemas/Hive'
712 "#;
713
714 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("spec parses");
715
716 let apiary = spec.get_schema("#/components/schemas/Apiary").expect("resolve apiary schema");
717 assert!(matches!(apiary.schema.schema_kind, SchemaKind::Type(Type::Object(_))));
718
719 let wrapper = spec
720 .get_schema("#/components/schemas/HiveWrapper")
721 .expect("resolve wrapper schema");
722 assert!(matches!(wrapper.schema.schema_kind, SchemaKind::Type(Type::Object(_))));
723 }
724}