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
16const OPERATION_KEYS: [&str; 8] = [
18 "get", "put", "post", "delete", "options", "head", "patch", "trace",
19];
20
21fn fill_missing_operation_responses(raw: &mut serde_json::Value) -> usize {
44 let Some(paths) = raw.get_mut("paths").and_then(|p| p.as_object_mut()) else {
45 return 0;
46 };
47
48 let mut repaired = 0;
49 for (_path, item) in paths.iter_mut() {
50 let Some(item) = item.as_object_mut() else {
51 continue;
52 };
53 for method in OPERATION_KEYS {
54 let Some(op) = item.get_mut(method).and_then(|o| o.as_object_mut()) else {
55 continue;
56 };
57 if !op.contains_key("responses") {
58 op.insert(
59 "responses".to_string(),
60 serde_json::Value::Object(serde_json::Map::new()),
61 );
62 repaired += 1;
63 }
64 }
65 }
66 repaired
67}
68
69fn repair_spec_for_parsing(raw: &mut serde_json::Value, source: &str) {
72 let repaired = fill_missing_operation_responses(raw);
73 if repaired > 0 {
74 tracing::warn!(
75 "{source}: {repaired} operation(s) had no `responses` field, which OpenAPI 3.x \
76 requires. Treating them as having no declared responses so the spec can load. \
77 Request generation does not use `responses`, but response-schema validation \
78 will have nothing to check for these operations."
79 );
80 }
81}
82
83#[derive(Debug, Clone)]
85pub struct OpenApiSpec {
86 pub spec: OpenAPI,
88 pub file_path: Option<String>,
90 pub raw_document: Option<serde_json::Value>,
92}
93
94impl OpenApiSpec {
95 pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
100 let path_ref = path.as_ref();
101 let content = fs::read_to_string(path_ref)
102 .await
103 .map_err(|e| Error::io_with_context("reading OpenAPI spec file", e.to_string()))?;
104
105 let raw_json = if path_ref.extension().and_then(|s| s.to_str()) == Some("yaml")
106 || path_ref.extension().and_then(|s| s.to_str()) == Some("yml")
107 {
108 let yaml_value: serde_yaml::Value = serde_yaml::from_str(&content)
109 .map_err(|e| Error::config(format!("Failed to parse YAML OpenAPI spec: {}", e)))?;
110 serde_json::to_value(&yaml_value).map_err(|e| {
111 Error::config(format!("Failed to convert YAML OpenAPI spec to JSON: {}", e))
112 })?
113 } else {
114 serde_json::from_str(&content)
115 .map_err(|e| Error::config(format!("Failed to parse JSON OpenAPI spec: {}", e)))?
116 };
117
118 let (raw_document, spec) = if swagger_convert::is_swagger_2(&raw_json) {
120 tracing::info!("Detected Swagger 2.0 specification, converting to OpenAPI 3.0");
121 let converted =
122 swagger_convert::convert_swagger_to_openapi3(&raw_json).map_err(|e| {
123 Error::config(format!("Failed to convert Swagger 2.0 to OpenAPI 3.0: {}", e))
124 })?;
125 let spec: OpenAPI = serde_json::from_value(converted.clone()).map_err(|e| {
126 Error::config(format!("Failed to parse converted OpenAPI spec: {}", e))
127 })?;
128 (converted, spec)
129 } else {
130 let mut raw_json = raw_json;
131 repair_spec_for_parsing(&mut raw_json, "OpenAPI spec");
132 let spec: OpenAPI = serde_json::from_value(raw_json.clone()).map_err(|e| {
133 let error_str = format!("{}", e);
135 let mut error_msg = format!("Failed to read OpenAPI spec: {}", e);
136
137 if error_str.contains("missing field") {
139 tracing::error!("OpenAPI deserialization error: {}", error_str);
140
141 if let Some(info) = raw_json.get("info") {
143 if let Some(info_obj) = info.as_object() {
144 let has_desc = info_obj.contains_key("description");
145 error_msg
146 .push_str(&format!(" | Info.description present: {}", has_desc));
147 }
148 }
149 if let Some(servers) = raw_json.get("servers") {
150 if let Some(servers_arr) = servers.as_array() {
151 error_msg.push_str(&format!(" | Servers count: {}", servers_arr.len()));
152 }
153 }
154 }
155
156 Error::config(error_msg)
157 })?;
158 (raw_json, spec)
159 };
160
161 Ok(Self {
162 spec,
163 file_path: path_ref.to_str().map(|s| s.to_string()),
164 raw_document: Some(raw_document),
165 })
166 }
167
168 pub fn from_string(content: &str, format: Option<&str>) -> Result<Self> {
173 let raw_json = if format == Some("yaml") || format == Some("yml") {
174 let yaml_value: serde_yaml::Value = serde_yaml::from_str(content)
175 .map_err(|e| Error::config(format!("Failed to parse YAML OpenAPI spec: {}", e)))?;
176 serde_json::to_value(&yaml_value).map_err(|e| {
177 Error::config(format!("Failed to convert YAML OpenAPI spec to JSON: {}", e))
178 })?
179 } else {
180 serde_json::from_str(content)
181 .map_err(|e| Error::config(format!("Failed to parse JSON OpenAPI spec: {}", e)))?
182 };
183
184 let (raw_document, spec) = if swagger_convert::is_swagger_2(&raw_json) {
186 let converted =
187 swagger_convert::convert_swagger_to_openapi3(&raw_json).map_err(|e| {
188 Error::config(format!("Failed to convert Swagger 2.0 to OpenAPI 3.0: {}", e))
189 })?;
190 let spec: OpenAPI = serde_json::from_value(converted.clone()).map_err(|e| {
191 Error::config(format!("Failed to parse converted OpenAPI spec: {}", e))
192 })?;
193 (converted, spec)
194 } else {
195 let mut raw_json = raw_json;
196 repair_spec_for_parsing(&mut raw_json, "OpenAPI spec");
197 let spec: OpenAPI = serde_json::from_value(raw_json.clone())
198 .map_err(|e| Error::io_with_context("reading OpenAPI spec", e.to_string()))?;
199 (raw_json, spec)
200 };
201
202 Ok(Self {
203 spec,
204 file_path: None,
205 raw_document: Some(raw_document),
206 })
207 }
208
209 pub fn from_json(json: serde_json::Value) -> Result<Self> {
214 let (raw_document, spec) = if swagger_convert::is_swagger_2(&json) {
216 let converted = swagger_convert::convert_swagger_to_openapi3(&json).map_err(|e| {
217 Error::config(format!("Failed to convert Swagger 2.0 to OpenAPI 3.0: {}", e))
218 })?;
219 let spec: OpenAPI = serde_json::from_value(converted.clone()).map_err(|e| {
220 Error::config(format!("Failed to parse converted OpenAPI spec: {}", e))
221 })?;
222 (converted, spec)
223 } else {
224 let json_for_doc = json.clone();
225 let spec: OpenAPI = serde_json::from_value(json)
226 .map_err(|e| Error::config(format!("Failed to parse JSON OpenAPI spec: {}", e)))?;
227 (json_for_doc, spec)
228 };
229
230 Ok(Self {
231 spec,
232 file_path: None,
233 raw_document: Some(raw_document),
234 })
235 }
236
237 pub fn validate(&self) -> Result<()> {
242 if self.spec.paths.paths.is_empty() {
244 return Err(Error::validation("OpenAPI spec must contain at least one path"));
245 }
246
247 if self.spec.info.title.is_empty() {
249 return Err(Error::validation("OpenAPI spec info must have a title"));
250 }
251
252 if self.spec.info.version.is_empty() {
253 return Err(Error::validation("OpenAPI spec info must have a version"));
254 }
255
256 Ok(())
257 }
258
259 pub fn validate_enhanced(&self) -> crate::spec_parser::ValidationResult {
261 if let Some(raw) = &self.raw_document {
263 let format = if raw.get("swagger").is_some() {
264 crate::spec_parser::SpecFormat::OpenApi20
265 } else if let Some(version) = raw.get("openapi").and_then(|v| v.as_str()) {
266 if version.starts_with("3.1") {
267 crate::spec_parser::SpecFormat::OpenApi31
268 } else {
269 crate::spec_parser::SpecFormat::OpenApi30
270 }
271 } else {
272 crate::spec_parser::SpecFormat::OpenApi30
274 };
275 crate::spec_parser::OpenApiValidator::validate(raw, format)
276 } else {
277 crate::spec_parser::ValidationResult::failure(vec![
279 crate::spec_parser::ValidationError::new(
280 "Cannot perform enhanced validation without raw document".to_string(),
281 ),
282 ])
283 }
284 }
285
286 pub fn version(&self) -> &str {
288 &self.spec.openapi
289 }
290
291 pub fn title(&self) -> &str {
293 &self.spec.info.title
294 }
295
296 pub fn description(&self) -> Option<&str> {
298 self.spec.info.description.as_deref()
299 }
300
301 pub fn api_version(&self) -> &str {
303 &self.spec.info.version
304 }
305
306 pub fn servers(&self) -> &[openapiv3::Server] {
308 &self.spec.servers
309 }
310
311 pub fn paths(&self) -> &openapiv3::Paths {
313 &self.spec.paths
314 }
315
316 pub fn schemas(&self) -> Option<&indexmap::IndexMap<String, ReferenceOr<Schema>>> {
318 self.spec.components.as_ref().map(|c| &c.schemas)
319 }
320
321 pub fn security_schemes(
323 &self,
324 ) -> Option<&indexmap::IndexMap<String, ReferenceOr<openapiv3::SecurityScheme>>> {
325 self.spec.components.as_ref().map(|c| &c.security_schemes)
326 }
327
328 pub fn operations_for_path(
330 &self,
331 path: &str,
332 ) -> std::collections::HashMap<String, openapiv3::Operation> {
333 let mut operations = std::collections::HashMap::new();
334
335 if let Some(path_item_ref) = self.spec.paths.paths.get(path) {
336 if let Some(path_item) = path_item_ref.as_item() {
338 let resolved_path_params: Vec<ReferenceOr<openapiv3::Parameter>> =
356 path_item.parameters.iter().map(|p| self.resolve_parameter_ref(p)).collect();
357 let merge = |op: &openapiv3::Operation| -> openapiv3::Operation {
358 let mut resolved_op = op.clone();
360 resolved_op.parameters =
361 op.parameters.iter().map(|p| self.resolve_parameter_ref(p)).collect();
362 merge_path_params_into_operation(&resolved_op, &resolved_path_params)
363 };
364 if let Some(op) = &path_item.get {
365 operations.insert("GET".to_string(), merge(op));
366 }
367 if let Some(op) = &path_item.post {
368 operations.insert("POST".to_string(), merge(op));
369 }
370 if let Some(op) = &path_item.put {
371 operations.insert("PUT".to_string(), merge(op));
372 }
373 if let Some(op) = &path_item.delete {
374 operations.insert("DELETE".to_string(), merge(op));
375 }
376 if let Some(op) = &path_item.patch {
377 operations.insert("PATCH".to_string(), merge(op));
378 }
379 if let Some(op) = &path_item.head {
380 operations.insert("HEAD".to_string(), merge(op));
381 }
382 if let Some(op) = &path_item.options {
383 operations.insert("OPTIONS".to_string(), merge(op));
384 }
385 if let Some(op) = &path_item.trace {
386 operations.insert("TRACE".to_string(), merge(op));
387 }
388 }
389 }
390
391 operations
392 }
393
394 pub fn all_paths_and_operations(
396 &self,
397 ) -> std::collections::HashMap<String, std::collections::HashMap<String, openapiv3::Operation>>
398 {
399 self.spec
400 .paths
401 .paths
402 .iter()
403 .map(|(path, _)| (path.clone(), self.operations_for_path(path)))
404 .collect()
405 }
406
407 pub fn get_schema(&self, reference: &str) -> Option<crate::schema::OpenApiSchema> {
409 self.resolve_schema(reference).map(crate::schema::OpenApiSchema::new)
410 }
411
412 pub fn resolve_schema_ref(&self, reference: &str) -> Option<Schema> {
417 self.resolve_schema(reference)
418 }
419
420 pub fn resolve_parameter_ref(
434 &self,
435 p_ref: &ReferenceOr<openapiv3::Parameter>,
436 ) -> ReferenceOr<openapiv3::Parameter> {
437 match p_ref {
438 ReferenceOr::Item(_) => p_ref.clone(),
439 ReferenceOr::Reference { reference } => {
440 let Some(name) = reference.strip_prefix("#/components/parameters/") else {
441 return p_ref.clone();
442 };
443 let Some(components) = self.spec.components.as_ref() else {
444 return p_ref.clone();
445 };
446 match components.parameters.get(name) {
447 Some(ReferenceOr::Item(p)) => ReferenceOr::Item(p.clone()),
448 Some(ReferenceOr::Reference { reference: nested }) => {
449 let Some(nested_name) = nested.strip_prefix("#/components/parameters/")
452 else {
453 return p_ref.clone();
454 };
455 match components.parameters.get(nested_name) {
456 Some(ReferenceOr::Item(p)) => ReferenceOr::Item(p.clone()),
457 _ => p_ref.clone(),
458 }
459 }
460 None => p_ref.clone(),
461 }
462 }
463 }
464 }
465
466 pub fn validate_security_requirements(
468 &self,
469 security_requirements: &[openapiv3::SecurityRequirement],
470 auth_header: Option<&str>,
471 api_key: Option<&str>,
472 ) -> Result<()> {
473 if security_requirements.is_empty() {
474 return Ok(());
475 }
476
477 for requirement in security_requirements {
479 if self.is_security_requirement_satisfied(requirement, auth_header, api_key)? {
480 return Ok(());
481 }
482 }
483
484 Err(Error::validation(
485 "Security validation failed: no valid authentication provided",
486 ))
487 }
488
489 fn resolve_schema(&self, reference: &str) -> Option<Schema> {
490 let mut visited = HashSet::new();
491 self.resolve_schema_recursive(reference, &mut visited)
492 }
493
494 fn resolve_schema_recursive(
495 &self,
496 reference: &str,
497 visited: &mut HashSet<String>,
498 ) -> Option<Schema> {
499 if !visited.insert(reference.to_string()) {
500 tracing::warn!("Detected recursive schema reference: {}", reference);
501 return None;
502 }
503
504 let schema_name = reference.strip_prefix("#/components/schemas/")?;
505 let components = self.spec.components.as_ref()?;
506 let schema_ref = components.schemas.get(schema_name)?;
507
508 match schema_ref {
509 ReferenceOr::Item(schema) => Some(schema.clone()),
510 ReferenceOr::Reference { reference: nested } => {
511 self.resolve_schema_recursive(nested, visited)
512 }
513 }
514 }
515
516 fn is_security_requirement_satisfied(
518 &self,
519 requirement: &openapiv3::SecurityRequirement,
520 auth_header: Option<&str>,
521 api_key: Option<&str>,
522 ) -> Result<bool> {
523 for (scheme_name, _scopes) in requirement {
525 if !self.is_security_scheme_satisfied(scheme_name, auth_header, api_key)? {
526 return Ok(false);
527 }
528 }
529 Ok(true)
530 }
531
532 fn is_security_scheme_satisfied(
534 &self,
535 scheme_name: &str,
536 auth_header: Option<&str>,
537 api_key: Option<&str>,
538 ) -> Result<bool> {
539 let security_schemes = match self.security_schemes() {
540 Some(schemes) => schemes,
541 None => return Ok(false),
542 };
543
544 let scheme = match security_schemes.get(scheme_name) {
545 Some(scheme) => scheme,
546 None => {
547 return Err(Error::config(format!("Security scheme '{}' not found", scheme_name)))
548 }
549 };
550
551 let scheme = match scheme {
552 ReferenceOr::Item(s) => s,
553 ReferenceOr::Reference { reference } => {
554 let ref_name =
556 reference.strip_prefix("#/components/securitySchemes/").ok_or_else(|| {
557 Error::config(format!(
558 "Unsupported security scheme reference format: {}",
559 reference
560 ))
561 })?;
562 match security_schemes.get(ref_name) {
563 Some(ReferenceOr::Item(resolved)) => resolved,
564 Some(ReferenceOr::Reference { .. }) => {
565 return Err(Error::config(format!(
566 "Nested security scheme reference not supported: {}",
567 ref_name
568 )))
569 }
570 None => {
571 return Err(Error::config(format!(
572 "Security scheme '{}' not found",
573 ref_name
574 )))
575 }
576 }
577 }
578 };
579
580 match scheme {
581 openapiv3::SecurityScheme::HTTP { scheme, .. } => {
582 match scheme.as_str() {
583 "bearer" => match auth_header {
584 Some(header) if header.starts_with("Bearer ") => Ok(true),
585 _ => Ok(false),
586 },
587 "basic" => match auth_header {
588 Some(header) if header.starts_with("Basic ") => Ok(true),
589 _ => Ok(false),
590 },
591 _ => Ok(false), }
593 }
594 openapiv3::SecurityScheme::APIKey { location, .. } => match location {
595 openapiv3::APIKeyLocation::Header => Ok(auth_header.is_some()),
596 openapiv3::APIKeyLocation::Query => Ok(api_key.is_some()),
597 openapiv3::APIKeyLocation::Cookie => Ok(api_key.is_some()),
598 },
599 openapiv3::SecurityScheme::OpenIDConnect { .. } => {
600 match auth_header {
602 Some(header) if header.starts_with("Bearer ") => Ok(true),
603 _ => Ok(false),
604 }
605 }
606 openapiv3::SecurityScheme::OAuth2 { .. } => {
607 match auth_header {
609 Some(header) if header.starts_with("Bearer ") => Ok(true),
610 _ => Ok(false),
611 }
612 }
613 }
614 }
615
616 pub fn get_global_security_requirements(&self) -> Vec<openapiv3::SecurityRequirement> {
618 self.spec.security.clone().unwrap_or_default()
619 }
620
621 pub fn get_request_body(&self, reference: &str) -> Option<&openapiv3::RequestBody> {
623 if let Some(components) = &self.spec.components {
624 if let Some(param_name) = reference.strip_prefix("#/components/requestBodies/") {
625 if let Some(request_body_ref) = components.request_bodies.get(param_name) {
626 return request_body_ref.as_item();
627 }
628 }
629 }
630 None
631 }
632
633 pub fn get_response(&self, reference: &str) -> Option<&openapiv3::Response> {
635 if let Some(components) = &self.spec.components {
636 if let Some(response_name) = reference.strip_prefix("#/components/responses/") {
637 if let Some(response_ref) = components.responses.get(response_name) {
638 return response_ref.as_item();
639 }
640 }
641 }
642 None
643 }
644
645 pub fn get_example(&self, reference: &str) -> Option<&openapiv3::Example> {
647 if let Some(components) = &self.spec.components {
648 if let Some(example_name) = reference.strip_prefix("#/components/examples/") {
649 if let Some(example_ref) = components.examples.get(example_name) {
650 return example_ref.as_item();
651 }
652 }
653 }
654 None
655 }
656}
657
658pub(crate) fn merge_path_params_into_operation(
667 operation: &openapiv3::Operation,
668 path_level_params: &[ReferenceOr<openapiv3::Parameter>],
669) -> openapiv3::Operation {
670 use std::collections::HashSet;
671 if path_level_params.is_empty() {
672 return operation.clone();
673 }
674 let mut op_keys: HashSet<(String, String)> = HashSet::new();
675 for p_ref in &operation.parameters {
676 if let Some(key) = parameter_key(p_ref) {
677 op_keys.insert(key);
678 }
679 }
680 let mut merged: Vec<ReferenceOr<openapiv3::Parameter>> =
681 Vec::with_capacity(path_level_params.len() + operation.parameters.len());
682 for p_ref in path_level_params {
683 match parameter_key(p_ref) {
684 Some(key) if op_keys.contains(&key) => {}
685 _ => merged.push(p_ref.clone()),
686 }
687 }
688 merged.extend(operation.parameters.iter().cloned());
689 let mut cloned = operation.clone();
690 cloned.parameters = merged;
691 cloned
692}
693
694fn parameter_key(p_ref: &ReferenceOr<openapiv3::Parameter>) -> Option<(String, String)> {
695 let p = p_ref.as_item()?;
696 let (name, in_loc) = match p {
697 openapiv3::Parameter::Path { parameter_data, .. } => (parameter_data.name.clone(), "path"),
698 openapiv3::Parameter::Query { parameter_data, .. } => {
699 (parameter_data.name.clone(), "query")
700 }
701 openapiv3::Parameter::Header { parameter_data, .. } => {
702 (parameter_data.name.clone(), "header")
703 }
704 openapiv3::Parameter::Cookie { parameter_data, .. } => {
705 (parameter_data.name.clone(), "cookie")
706 }
707 };
708 Some((name, in_loc.to_string()))
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714 use openapiv3::{SchemaKind, Type};
715
716 #[test]
717 fn resolves_security_scheme_ref() {
718 let yaml = r#"
719openapi: 3.0.3
720info:
721 title: Test API
722 version: "1.0.0"
723paths:
724 /test:
725 get:
726 security:
727 - BearerRef: []
728 responses:
729 '200':
730 description: OK
731components:
732 securitySchemes:
733 BearerAuth:
734 type: http
735 scheme: bearer
736 BearerRef:
737 $ref: '#/components/securitySchemes/BearerAuth'
738 "#;
739
740 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("spec parses");
741
742 let result = spec
744 .is_security_scheme_satisfied("BearerRef", Some("Bearer token123"), None)
745 .expect("should resolve ref");
746 assert!(result);
747
748 let result = spec
750 .is_security_scheme_satisfied("BearerRef", None, None)
751 .expect("should resolve ref");
752 assert!(!result);
753 }
754
755 #[test]
756 fn resolves_nested_schema_references() {
757 let yaml = r#"
758openapi: 3.0.3
759info:
760 title: Test API
761 version: "1.0.0"
762paths: {}
763components:
764 schemas:
765 Apiary:
766 type: object
767 properties:
768 id:
769 type: string
770 hive:
771 $ref: '#/components/schemas/Hive'
772 Hive:
773 type: object
774 properties:
775 name:
776 type: string
777 HiveWrapper:
778 $ref: '#/components/schemas/Hive'
779 "#;
780
781 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("spec parses");
782
783 let apiary = spec.get_schema("#/components/schemas/Apiary").expect("resolve apiary schema");
784 assert!(matches!(apiary.schema.schema_kind, SchemaKind::Type(Type::Object(_))));
785
786 let wrapper = spec
787 .get_schema("#/components/schemas/HiveWrapper")
788 .expect("resolve wrapper schema");
789 assert!(matches!(wrapper.schema.schema_kind, SchemaKind::Type(Type::Object(_))));
790 }
791}
792
793#[cfg(test)]
794mod missing_responses_tests {
795 use super::*;
796
797 #[test]
801 fn spec_without_operation_responses_loads() {
802 let raw = r#"{
803 "openapi": "3.0.0",
804 "info": { "title": "proxy-generated", "version": "1.0.0" },
805 "paths": {
806 "/orders": {
807 "get": { "summary": "list" },
808 "post": {
809 "summary": "create",
810 "requestBody": {
811 "content": { "application/json": { "schema": { "type": "object" } } }
812 }
813 }
814 }
815 }
816 }"#;
817
818 let spec = OpenApiSpec::from_string(raw, Some("json"))
819 .expect("a spec missing `responses` should still load for bench/load use");
820
821 let item = spec.spec.paths.paths.get("/orders").expect("path present");
822 let ReferenceOr::Item(item) = item else {
823 panic!("expected inline path item");
824 };
825 assert!(item.get.is_some(), "GET survived the repair");
826 assert!(item.post.is_some(), "POST survived the repair");
827 assert!(
828 item.post.as_ref().unwrap().request_body.is_some(),
829 "requestBody must be preserved — it is what request generation actually reads"
830 );
831 }
832
833 #[test]
836 fn existing_responses_are_left_alone() {
837 let mut raw: serde_json::Value = serde_json::from_str(
838 r#"{"paths": {"/a": {"get": {"responses": {"200": {"description": "ok"}}}}}}"#,
839 )
840 .unwrap();
841 let before = raw.clone();
842 assert_eq!(fill_missing_operation_responses(&mut raw), 0);
843 assert_eq!(raw, before, "a conformant spec must be byte-identical after the pass");
844 }
845
846 #[test]
849 fn non_operation_keys_are_not_touched() {
850 let mut raw: serde_json::Value = serde_json::from_str(
851 r#"{"paths": {"/a": {"parameters": [], "servers": [], "get": {}}}}"#,
852 )
853 .unwrap();
854 assert_eq!(fill_missing_operation_responses(&mut raw), 1, "only `get` is an operation");
855 let path = &raw["paths"]["/a"];
856 assert!(path["parameters"].is_array(), "parameters untouched");
857 assert!(path["servers"].is_array(), "servers untouched");
858 assert!(path["get"]["responses"].is_object(), "get repaired");
859 }
860}