1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct OpenApiSpec {
12 pub info: Info,
13 #[serde(default)]
14 pub paths: BTreeMap<String, PathItem>,
15 #[serde(default)]
16 pub components: Option<Components>,
17 #[serde(default)]
18 pub servers: Vec<Server>,
19 #[serde(default)]
20 pub security: Vec<BTreeMap<String, Vec<String>>>,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct Info {
28 pub title: String,
29 #[serde(default)]
30 pub description: Option<String>,
31 pub version: String,
32}
33
34impl std::fmt::Display for Info {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 write!(f, "{} v{}", self.title, self.version)
37 }
38}
39
40#[derive(Debug, Clone, Default, Serialize, Deserialize)]
44pub struct PathItem {
45 #[serde(default)]
46 pub get: Option<Operation>,
47 #[serde(default)]
48 pub post: Option<Operation>,
49 #[serde(default)]
50 pub put: Option<Operation>,
51 #[serde(default)]
52 pub delete: Option<Operation>,
53 #[serde(default)]
54 pub patch: Option<Operation>,
55 #[serde(default)]
56 pub parameters: Vec<Parameter>,
57}
58
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct Operation {
63 #[serde(default)]
64 pub operation_id: Option<String>,
65 #[serde(default)]
66 pub summary: Option<String>,
67 #[serde(default)]
68 pub description: Option<String>,
69 #[serde(default)]
70 pub parameters: Vec<Parameter>,
71 #[serde(default)]
72 pub request_body: Option<RequestBody>,
73 #[serde(default)]
74 pub responses: BTreeMap<String, Response>,
75 #[serde(default)]
76 pub security: Vec<BTreeMap<String, Vec<String>>>,
77 #[serde(default)]
78 pub tags: Vec<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Parameter {
86 pub name: String,
87 #[serde(rename = "in")]
88 pub location: String,
89 #[serde(default)]
90 pub required: bool,
91 #[serde(default)]
92 pub description: Option<String>,
93 #[serde(default)]
94 pub schema: Option<Schema>,
95 #[serde(rename = "$ref", default)]
97 pub ref_path: Option<String>,
98}
99
100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
104pub struct RequestBody {
105 #[serde(default)]
106 pub required: bool,
107 #[serde(default)]
108 pub content: BTreeMap<String, MediaType>,
109 #[serde(default)]
110 pub description: Option<String>,
111 #[serde(rename = "$ref", default)]
113 pub ref_path: Option<String>,
114}
115
116#[derive(Debug, Clone, Default, Serialize, Deserialize)]
118pub struct MediaType {
119 #[serde(default)]
120 pub schema: Option<Schema>,
121}
122
123#[derive(Debug, Clone, Default, Serialize, Deserialize)]
125pub struct Response {
126 #[serde(default)]
127 pub description: Option<String>,
128 #[serde(default)]
129 pub content: Option<BTreeMap<String, MediaType>>,
130 #[serde(rename = "$ref", default)]
132 pub ref_path: Option<String>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize, Default)]
139#[serde(rename_all = "camelCase")]
140pub struct Schema {
141 #[serde(rename = "type", default)]
142 pub schema_type: Option<String>,
143 #[serde(default)]
144 pub format: Option<String>,
145 #[serde(default)]
146 pub description: Option<String>,
147 #[serde(default)]
148 pub properties: BTreeMap<String, Schema>,
149 #[serde(default)]
150 pub items: Option<Box<Schema>>,
151 #[serde(default)]
152 pub required: Vec<String>,
153 #[serde(rename = "enum", default)]
154 pub enum_values: Option<Vec<serde_json::Value>>,
155 #[serde(rename = "$ref", default)]
157 pub ref_path: Option<String>,
158 #[serde(rename = "allOf", default)]
159 pub all_of: Vec<Schema>,
160 #[serde(rename = "oneOf", default)]
161 pub one_of: Vec<Schema>,
162 #[serde(rename = "anyOf", default)]
163 pub any_of: Vec<Schema>,
164 #[serde(default)]
165 pub default: Option<serde_json::Value>,
166 #[serde(default)]
167 pub minimum: Option<f64>,
168 #[serde(default)]
169 pub maximum: Option<f64>,
170 #[serde(rename = "minLength", default)]
171 pub min_length: Option<u64>,
172 #[serde(rename = "maxLength", default)]
173 pub max_length: Option<u64>,
174 #[serde(default)]
175 pub nullable: bool,
176 #[serde(default)]
177 pub additional_properties: Option<Box<Schema>>,
178 #[serde(default)]
179 pub title: Option<String>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize, Default)]
186#[serde(rename_all = "camelCase")]
187pub struct Components {
188 #[serde(default)]
189 pub schemas: BTreeMap<String, Schema>,
190 #[serde(default)]
191 pub security_schemes: BTreeMap<String, SecurityScheme>,
192 #[serde(default)]
193 pub parameters: BTreeMap<String, Parameter>,
194 #[serde(default)]
195 pub request_bodies: BTreeMap<String, RequestBody>,
196 #[serde(default)]
197 pub responses: BTreeMap<String, Response>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct SecurityScheme {
203 #[serde(rename = "type")]
204 pub scheme_type: String,
205 #[serde(default)]
206 pub scheme: Option<String>,
207 #[serde(default)]
208 pub description: Option<String>,
209 #[serde(rename = "in", default)]
211 pub location: Option<String>,
212 #[serde(default)]
214 pub name: Option<String>,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221pub struct Server {
222 pub url: String,
223 #[serde(default)]
224 pub description: Option<String>,
225}
226
227impl std::fmt::Display for OpenApiSpec {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 write!(
230 f,
231 "OpenAPI spec: {} ({} paths, {} operations)",
232 self.info,
233 self.paths.len(),
234 self.operation_count()
235 )
236 }
237}
238
239const HTTP_METHODS: [&str; 5] = ["get", "post", "put", "delete", "patch"];
242
243impl PathItem {
244 pub fn operations(&self) -> impl Iterator<Item = (&'static str, &Operation)> {
248 HTTP_METHODS
249 .into_iter()
250 .zip([
251 self.get.as_ref(),
252 self.post.as_ref(),
253 self.put.as_ref(),
254 self.delete.as_ref(),
255 self.patch.as_ref(),
256 ])
257 .filter_map(|(method, op)| op.map(|o| (method, o)))
258 }
259}
260
261impl Schema {
264 #[must_use]
266 pub fn is_ref(&self) -> bool {
267 self.ref_path.is_some()
268 }
269
270 #[must_use]
272 pub fn is_array(&self) -> bool {
273 self.schema_type.as_deref() == Some("array")
274 }
275
276 #[must_use]
278 pub fn is_object(&self) -> bool {
279 self.schema_type.as_deref() == Some("object")
280 }
281
282 #[must_use]
284 pub fn is_primitive(&self) -> bool {
285 matches!(
286 self.schema_type.as_deref(),
287 Some("string" | "integer" | "number" | "boolean")
288 )
289 }
290
291 #[must_use]
293 pub fn is_enum(&self) -> bool {
294 self.enum_values.as_ref().is_some_and(|v| !v.is_empty())
295 }
296
297 #[must_use]
299 pub fn is_composed(&self) -> bool {
300 !self.all_of.is_empty() || !self.one_of.is_empty() || !self.any_of.is_empty()
301 }
302
303 #[must_use]
305 pub fn ref_name(&self) -> Option<&str> {
306 self.ref_path.as_deref().map(ref_name)
307 }
308}
309
310impl Operation {
313 #[must_use]
315 pub fn json_body_schema(&self) -> Option<&Schema> {
316 self.request_body
317 .as_ref()?
318 .content
319 .get("application/json")?
320 .schema
321 .as_ref()
322 }
323
324 #[must_use]
326 pub fn success_response_schema(&self) -> Option<&Schema> {
327 let resp = self
328 .responses
329 .get("200")
330 .or_else(|| self.responses.get("201"))?;
331 resp.content
332 .as_ref()?
333 .get("application/json")?
334 .schema
335 .as_ref()
336 }
337}
338
339#[must_use]
348pub fn ref_name(ref_path: &str) -> &str {
349 ref_path.rsplit('/').next().unwrap_or(ref_path)
350}
351
352impl OpenApiSpec {
353 #[must_use]
355 pub fn resolve_schema_ref(&self, ref_path: &str) -> Option<&Schema> {
356 let name = ref_name(ref_path);
357 self.components.as_ref()?.schemas.get(name)
358 }
359
360 #[must_use]
362 pub fn resolve_parameter_ref(&self, ref_path: &str) -> Option<&Parameter> {
363 let name = ref_name(ref_path);
364 self.components.as_ref()?.parameters.get(name)
365 }
366
367 #[must_use]
369 pub fn resolve_request_body_ref(&self, ref_path: &str) -> Option<&RequestBody> {
370 let name = ref_name(ref_path);
371 self.components.as_ref()?.request_bodies.get(name)
372 }
373
374 #[must_use]
376 pub fn resolve_response_ref(&self, ref_path: &str) -> Option<&Response> {
377 let name = ref_name(ref_path);
378 self.components.as_ref()?.responses.get(name)
379 }
380}
381
382#[must_use]
387pub fn all_operations(spec: &OpenApiSpec) -> Vec<(String, String, &Operation)> {
388 spec.all_operations().collect()
389}
390
391impl OpenApiSpec {
392 pub fn all_operations(&self) -> impl Iterator<Item = (String, String, &Operation)> {
394 self.paths.iter().flat_map(|(path, item)| {
395 let path = path.clone();
396 item.operations()
397 .map(move |(method, op)| (method.to_string(), path.clone(), op))
398 })
399 }
400
401 #[must_use]
403 pub fn schema_count(&self) -> usize {
404 self.components
405 .as_ref()
406 .map_or(0, |c| c.schemas.len())
407 }
408
409 #[must_use]
411 pub fn operation_count(&self) -> usize {
412 self.paths.values().map(|item| item.operations().count()).sum()
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::test_fixtures::{FULL_SPEC_YAML, MINIMAL_SPEC_YAML};
420
421 #[test]
422 fn parse_minimal_yaml() {
423 let spec: OpenApiSpec = serde_yaml_ng::from_str(MINIMAL_SPEC_YAML).unwrap();
424 assert_eq!(spec.info.title, "Test API");
425 assert_eq!(spec.info.version, "1.0.0");
426 assert!(spec.paths.is_empty());
427 assert!(spec.components.is_none());
428 assert!(spec.servers.is_empty());
429 }
430
431 #[test]
432 fn parse_full_yaml() {
433 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
434 assert_eq!(spec.info.title, "Pet Store");
435 assert_eq!(
436 spec.info.description.as_deref(),
437 Some("A sample pet store API")
438 );
439 assert_eq!(spec.info.version, "2.0.0");
440 assert_eq!(spec.servers.len(), 1);
441 assert_eq!(spec.servers[0].url, "https://api.petstore.example.com/v2");
442 assert_eq!(spec.paths.len(), 2);
443 assert!(spec.paths.contains_key("/pets"));
444 assert!(spec.paths.contains_key("/pets/{petId}"));
445 }
446
447 #[test]
448 fn parse_path_item_methods() {
449 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
450 let pets = &spec.paths["/pets"];
451 assert!(pets.get.is_some());
452 assert!(pets.post.is_some());
453 assert!(pets.put.is_none());
454 assert!(pets.delete.is_none());
455
456 let pet_by_id = &spec.paths["/pets/{petId}"];
457 assert!(pet_by_id.get.is_some());
458 assert!(pet_by_id.delete.is_some());
459 assert!(pet_by_id.post.is_none());
460 }
461
462 #[test]
463 fn parse_operation_fields() {
464 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
465 let list_op = spec.paths["/pets"].get.as_ref().unwrap();
466 assert_eq!(list_op.operation_id.as_deref(), Some("listPets"));
467 assert_eq!(list_op.summary.as_deref(), Some("List all pets"));
468 assert_eq!(list_op.parameters.len(), 1);
469 assert_eq!(list_op.parameters[0].name, "limit");
470 assert_eq!(list_op.parameters[0].location, "query");
471 assert!(!list_op.parameters[0].required);
472 }
473
474 #[test]
475 fn parse_parameters_with_schema() {
476 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
477 let limit_param = &spec.paths["/pets"].get.as_ref().unwrap().parameters[0];
478 let schema = limit_param.schema.as_ref().unwrap();
479 assert_eq!(schema.schema_type.as_deref(), Some("integer"));
480 assert_eq!(schema.format.as_deref(), Some("int64"));
481 }
482
483 #[test]
484 fn parse_path_level_parameters() {
485 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
486 let pet_item = &spec.paths["/pets/{petId}"];
487 assert_eq!(pet_item.parameters.len(), 1);
488 assert_eq!(pet_item.parameters[0].name, "petId");
489 assert_eq!(pet_item.parameters[0].location, "path");
490 assert!(pet_item.parameters[0].required);
491 }
492
493 #[test]
494 fn parse_request_body() {
495 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
496 let create_op = spec.paths["/pets"].post.as_ref().unwrap();
497 let body = create_op.request_body.as_ref().unwrap();
498 assert!(body.required);
499 assert!(body.content.contains_key("application/json"));
500 let schema = body.content["application/json"].schema.as_ref().unwrap();
501 assert_eq!(
502 schema.ref_path.as_deref(),
503 Some("#/components/schemas/CreatePetRequest")
504 );
505 }
506
507 #[test]
508 fn parse_responses() {
509 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
510 let get_op = spec.paths["/pets/{petId}"].get.as_ref().unwrap();
511 assert!(get_op.responses.contains_key("200"));
512 assert!(get_op.responses.contains_key("404"));
513 assert_eq!(
514 get_op.responses["404"].description.as_deref(),
515 Some("Pet not found")
516 );
517 }
518
519 #[test]
520 fn parse_component_schemas() {
521 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
522 let components = spec.components.as_ref().unwrap();
523 assert!(components.schemas.contains_key("Pet"));
524 assert!(components.schemas.contains_key("PetStatus"));
525 assert!(components.schemas.contains_key("CreatePetRequest"));
526
527 let pet = &components.schemas["Pet"];
528 assert_eq!(pet.schema_type.as_deref(), Some("object"));
529 assert_eq!(pet.required, vec!["id", "name"]);
530 assert!(pet.properties.contains_key("id"));
531 assert!(pet.properties.contains_key("name"));
532 assert!(pet.properties.contains_key("tag"));
533 assert!(pet.properties.contains_key("status"));
534 }
535
536 #[test]
537 fn parse_enum_schema() {
538 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
539 let status = &spec.components.as_ref().unwrap().schemas["PetStatus"];
540 assert_eq!(status.schema_type.as_deref(), Some("string"));
541 let variants = status.enum_values.as_ref().unwrap();
542 assert_eq!(variants.len(), 3);
543 assert_eq!(variants[0].as_str(), Some("available"));
544 assert_eq!(variants[1].as_str(), Some("pending"));
545 assert_eq!(variants[2].as_str(), Some("sold"));
546 }
547
548 #[test]
549 fn parse_security_schemes() {
550 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
551 let schemes = &spec.components.as_ref().unwrap().security_schemes;
552 assert!(schemes.contains_key("bearerAuth"));
553 let bearer = &schemes["bearerAuth"];
554 assert_eq!(bearer.scheme_type, "http");
555 assert_eq!(bearer.scheme.as_deref(), Some("bearer"));
556 }
557
558 #[test]
559 fn parse_json_format() {
560 let json = r#"{
561 "info": { "title": "JSON API", "version": "0.1.0" },
562 "paths": {}
563 }"#;
564 let spec: OpenApiSpec = serde_json::from_str(json).unwrap();
565 assert_eq!(spec.info.title, "JSON API");
566 assert_eq!(spec.info.version, "0.1.0");
567 }
568
569 #[test]
572 fn ref_name_extracts_last_segment() {
573 assert_eq!(ref_name("#/components/schemas/Pet"), "Pet");
574 assert_eq!(
575 ref_name("#/components/parameters/LimitParam"),
576 "LimitParam"
577 );
578 assert_eq!(ref_name("#/components/requestBodies/PetBody"), "PetBody");
579 }
580
581 #[test]
582 fn ref_name_handles_no_slash() {
583 assert_eq!(ref_name("Standalone"), "Standalone");
584 }
585
586 #[test]
587 fn ref_name_handles_trailing_slash() {
588 assert_eq!(ref_name("#/components/schemas/"), "");
589 }
590
591 #[test]
594 fn resolve_schema_ref_found() {
595 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
596 let pet = spec.resolve_schema_ref("#/components/schemas/Pet");
597 assert!(pet.is_some());
598 assert_eq!(pet.unwrap().schema_type.as_deref(), Some("object"));
599 }
600
601 #[test]
602 fn resolve_schema_ref_not_found() {
603 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
604 assert!(
605 spec.resolve_schema_ref("#/components/schemas/Nonexistent")
606 .is_none()
607 );
608 }
609
610 #[test]
611 fn resolve_schema_ref_no_components() {
612 let spec: OpenApiSpec = serde_yaml_ng::from_str(MINIMAL_SPEC_YAML).unwrap();
613 assert!(
614 spec.resolve_schema_ref("#/components/schemas/Anything")
615 .is_none()
616 );
617 }
618
619 #[test]
620 fn resolve_parameter_ref_found() {
621 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
622 let param = spec.resolve_parameter_ref("#/components/parameters/LimitParam");
623 assert!(param.is_some());
624 assert_eq!(param.unwrap().name, "limit");
625 }
626
627 #[test]
628 fn resolve_parameter_ref_not_found() {
629 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
630 assert!(
631 spec.resolve_parameter_ref("#/components/parameters/Missing")
632 .is_none()
633 );
634 }
635
636 #[test]
637 fn resolve_request_body_ref_found() {
638 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
639 let body = spec.resolve_request_body_ref("#/components/requestBodies/PetBody");
640 assert!(body.is_some());
641 assert!(body.unwrap().required);
642 }
643
644 #[test]
645 fn resolve_response_ref_found() {
646 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
647 let resp = spec.resolve_response_ref("#/components/responses/NotFound");
648 assert!(resp.is_some());
649 assert_eq!(
650 resp.unwrap().description.as_deref(),
651 Some("The requested resource was not found")
652 );
653 }
654
655 #[test]
656 fn resolve_response_ref_not_found() {
657 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
658 assert!(
659 spec.resolve_response_ref("#/components/responses/Gone")
660 .is_none()
661 );
662 }
663
664 #[test]
667 fn parse_schema_with_all_of() {
668 let yaml = r##"
669info:
670 title: AllOf Test
671 version: "1.0.0"
672paths: {}
673components:
674 schemas:
675 Extended:
676 allOf:
677 - $ref: "#/components/schemas/Base"
678 - type: object
679 properties:
680 extra:
681 type: string
682 Base:
683 type: object
684 required:
685 - id
686 properties:
687 id:
688 type: integer
689"##;
690 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
691 let extended = &spec.components.as_ref().unwrap().schemas["Extended"];
692 assert_eq!(extended.all_of.len(), 2);
693 assert_eq!(
694 extended.all_of[0].ref_path.as_deref(),
695 Some("#/components/schemas/Base")
696 );
697 }
698
699 #[test]
700 fn parse_schema_with_nullable() {
701 let yaml = r#"
702info:
703 title: Nullable Test
704 version: "1.0.0"
705paths: {}
706components:
707 schemas:
708 NullableField:
709 type: object
710 properties:
711 value:
712 type: string
713 nullable: true
714"#;
715 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
716 let schema = &spec.components.as_ref().unwrap().schemas["NullableField"];
717 assert!(schema.properties["value"].nullable);
718 }
719
720 #[test]
721 fn parse_schema_with_additional_properties() {
722 let yaml = r#"
723info:
724 title: AdditionalProps Test
725 version: "1.0.0"
726paths: {}
727components:
728 schemas:
729 FreeForm:
730 type: object
731 additionalProperties:
732 type: string
733"#;
734 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
735 let schema = &spec.components.as_ref().unwrap().schemas["FreeForm"];
736 assert!(schema.additional_properties.is_some());
737 let inner = schema.additional_properties.as_ref().unwrap();
738 assert_eq!(inner.schema_type.as_deref(), Some("string"));
739 }
740
741 #[test]
742 fn parse_schema_array_with_items() {
743 let yaml = r#"
744info:
745 title: Array Test
746 version: "1.0.0"
747paths: {}
748components:
749 schemas:
750 StringList:
751 type: array
752 items:
753 type: string
754"#;
755 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
756 let schema = &spec.components.as_ref().unwrap().schemas["StringList"];
757 assert_eq!(schema.schema_type.as_deref(), Some("array"));
758 let items = schema.items.as_ref().unwrap();
759 assert_eq!(items.schema_type.as_deref(), Some("string"));
760 }
761
762 #[test]
763 fn parse_servers() {
764 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
765 assert_eq!(spec.servers.len(), 1);
766 assert_eq!(spec.servers[0].url, "https://api.petstore.example.com/v2");
767 assert_eq!(
768 spec.servers[0].description.as_deref(),
769 Some("Production server")
770 );
771 }
772
773 #[test]
774 fn parse_api_key_security_scheme() {
775 let yaml = r#"
776info:
777 title: ApiKey Test
778 version: "1.0.0"
779paths: {}
780components:
781 securitySchemes:
782 apiKey:
783 type: apiKey
784 in: header
785 name: X-API-Key
786"#;
787 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
788 let schemes = &spec.components.as_ref().unwrap().security_schemes;
789 let key = &schemes["apiKey"];
790 assert_eq!(key.scheme_type, "apiKey");
791 assert_eq!(key.location.as_deref(), Some("header"));
792 assert_eq!(key.name.as_deref(), Some("X-API-Key"));
793 }
794
795 #[test]
798 fn all_operations_returns_correct_tuples() {
799 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
800 let ops = all_operations(&spec);
801 assert_eq!(ops.len(), 4);
803
804 let methods_and_paths: Vec<(&str, &str)> =
805 ops.iter().map(|(m, p, _)| (m.as_str(), p.as_str())).collect();
806 assert!(methods_and_paths.contains(&("get", "/pets")));
807 assert!(methods_and_paths.contains(&("post", "/pets")));
808 assert!(methods_and_paths.contains(&("get", "/pets/{petId}")));
809 assert!(methods_and_paths.contains(&("delete", "/pets/{petId}")));
810 }
811
812 #[test]
813 fn all_operations_empty_paths() {
814 let spec: OpenApiSpec = serde_yaml_ng::from_str(MINIMAL_SPEC_YAML).unwrap();
815 let ops = all_operations(&spec);
816 assert!(ops.is_empty());
817 }
818
819 #[test]
820 fn all_operations_preserves_operation_ids() {
821 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
822 let ops = all_operations(&spec);
823 let ids: Vec<Option<&str>> = ops
824 .iter()
825 .map(|(_, _, op)| op.operation_id.as_deref())
826 .collect();
827 assert!(ids.contains(&Some("listPets")));
828 assert!(ids.contains(&Some("createPet")));
829 assert!(ids.contains(&Some("getPet")));
830 assert!(ids.contains(&Some("deletePet")));
831 }
832
833 #[test]
836 fn parse_global_security() {
837 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
838 assert_eq!(spec.security.len(), 1);
839 assert!(spec.security[0].contains_key("bearerAuth"));
840 assert!(spec.security[0]["bearerAuth"].is_empty());
841 }
842
843 #[test]
846 fn parse_schema_with_min_max() {
847 let yaml = r#"
848info:
849 title: MinMax Test
850 version: "1.0.0"
851paths: {}
852components:
853 schemas:
854 Bounded:
855 type: integer
856 minimum: 1
857 maximum: 100
858"#;
859 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
860 let schema = &spec.components.as_ref().unwrap().schemas["Bounded"];
861 assert_eq!(schema.minimum, Some(1.0));
862 assert_eq!(schema.maximum, Some(100.0));
863 }
864
865 #[test]
866 fn parse_schema_with_string_length_constraints() {
867 let yaml = r#"
868info:
869 title: StringLength Test
870 version: "1.0.0"
871paths: {}
872components:
873 schemas:
874 BoundedString:
875 type: string
876 minLength: 3
877 maxLength: 255
878"#;
879 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
880 let schema = &spec.components.as_ref().unwrap().schemas["BoundedString"];
881 assert_eq!(schema.min_length, Some(3));
882 assert_eq!(schema.max_length, Some(255));
883 }
884
885 #[test]
886 fn parse_schema_with_default_value() {
887 let yaml = r#"
888info:
889 title: Default Test
890 version: "1.0.0"
891paths: {}
892components:
893 schemas:
894 WithDefault:
895 type: string
896 default: "hello"
897"#;
898 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
899 let schema = &spec.components.as_ref().unwrap().schemas["WithDefault"];
900 assert_eq!(
901 schema.default.as_ref().and_then(|v| v.as_str()),
902 Some("hello")
903 );
904 }
905
906 #[test]
907 fn parse_schema_with_title() {
908 let yaml = r#"
909info:
910 title: Title Test
911 version: "1.0.0"
912paths: {}
913components:
914 schemas:
915 Named:
916 type: object
917 title: A Named Schema
918"#;
919 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
920 let schema = &spec.components.as_ref().unwrap().schemas["Named"];
921 assert_eq!(schema.title.as_deref(), Some("A Named Schema"));
922 }
923
924 #[test]
925 fn parse_schema_with_one_of() {
926 let yaml = r##"
927info:
928 title: OneOf Test
929 version: "1.0.0"
930paths: {}
931components:
932 schemas:
933 StringOrInt:
934 oneOf:
935 - type: string
936 - type: integer
937"##;
938 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
939 let schema = &spec.components.as_ref().unwrap().schemas["StringOrInt"];
940 assert_eq!(schema.one_of.len(), 2);
941 assert_eq!(schema.one_of[0].schema_type.as_deref(), Some("string"));
942 assert_eq!(schema.one_of[1].schema_type.as_deref(), Some("integer"));
943 }
944
945 #[test]
946 fn parse_schema_with_any_of() {
947 let yaml = r##"
948info:
949 title: AnyOf Test
950 version: "1.0.0"
951paths: {}
952components:
953 schemas:
954 Flexible:
955 anyOf:
956 - type: string
957 - type: number
958"##;
959 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
960 let schema = &spec.components.as_ref().unwrap().schemas["Flexible"];
961 assert_eq!(schema.any_of.len(), 2);
962 }
963
964 #[test]
967 fn parse_property_with_ref() {
968 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
969 let pet = &spec.components.as_ref().unwrap().schemas["Pet"];
970 let status_prop = &pet.properties["status"];
971 assert_eq!(
972 status_prop.ref_path.as_deref(),
973 Some("#/components/schemas/PetStatus")
974 );
975 }
976
977 #[test]
980 fn parse_component_parameters() {
981 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
982 let params = &spec.components.as_ref().unwrap().parameters;
983 assert!(params.contains_key("LimitParam"));
984 let limit = ¶ms["LimitParam"];
985 assert_eq!(limit.name, "limit");
986 assert_eq!(limit.location, "query");
987 assert!(!limit.required);
988 }
989
990 #[test]
993 fn parse_component_request_bodies() {
994 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
995 let bodies = &spec.components.as_ref().unwrap().request_bodies;
996 assert!(bodies.contains_key("PetBody"));
997 let pet_body = &bodies["PetBody"];
998 assert!(pet_body.required);
999 assert!(pet_body.content.contains_key("application/json"));
1000 }
1001
1002 #[test]
1005 fn parse_component_responses() {
1006 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1007 let responses = &spec.components.as_ref().unwrap().responses;
1008 assert!(responses.contains_key("NotFound"));
1009 assert_eq!(
1010 responses["NotFound"].description.as_deref(),
1011 Some("The requested resource was not found")
1012 );
1013 }
1014
1015 #[test]
1018 fn roundtrip_json_serialization() {
1019 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1020 let json = serde_json::to_string(&spec).unwrap();
1021 let roundtrip: OpenApiSpec = serde_json::from_str(&json).unwrap();
1022 assert_eq!(roundtrip.info.title, spec.info.title);
1023 assert_eq!(roundtrip.paths.len(), spec.paths.len());
1024 }
1025
1026 #[test]
1029 fn schema_default_is_empty() {
1030 let schema = Schema::default();
1031 assert!(schema.schema_type.is_none());
1032 assert!(schema.properties.is_empty());
1033 assert!(schema.required.is_empty());
1034 assert!(!schema.nullable);
1035 assert!(schema.all_of.is_empty());
1036 assert!(schema.one_of.is_empty());
1037 assert!(schema.any_of.is_empty());
1038 }
1039
1040 #[test]
1043 fn components_default_is_empty() {
1044 let components = Components::default();
1045 assert!(components.schemas.is_empty());
1046 assert!(components.security_schemes.is_empty());
1047 assert!(components.parameters.is_empty());
1048 assert!(components.request_bodies.is_empty());
1049 assert!(components.responses.is_empty());
1050 }
1051
1052 #[test]
1055 fn parse_operation_with_tags() {
1056 let yaml = r#"
1057info:
1058 title: Tags Test
1059 version: "1.0.0"
1060paths:
1061 /items:
1062 get:
1063 operationId: listItems
1064 tags:
1065 - items
1066 - public
1067 responses:
1068 "200":
1069 description: OK
1070"#;
1071 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1072 let op = spec.paths["/items"].get.as_ref().unwrap();
1073 assert_eq!(op.tags, vec!["items", "public"]);
1074 }
1075
1076 #[test]
1079 fn schema_is_ref() {
1080 let s = Schema {
1081 ref_path: Some("#/components/schemas/Pet".to_string()),
1082 ..Default::default()
1083 };
1084 assert!(s.is_ref());
1085 assert_eq!(s.ref_name(), Some("Pet"));
1086 }
1087
1088 #[test]
1089 fn schema_is_not_ref() {
1090 let s = Schema::default();
1091 assert!(!s.is_ref());
1092 assert_eq!(s.ref_name(), None);
1093 }
1094
1095 #[test]
1096 fn schema_is_array() {
1097 let s = Schema {
1098 schema_type: Some("array".to_string()),
1099 ..Default::default()
1100 };
1101 assert!(s.is_array());
1102 assert!(!s.is_object());
1103 assert!(!s.is_primitive());
1104 }
1105
1106 #[test]
1107 fn schema_is_object() {
1108 let s = Schema {
1109 schema_type: Some("object".to_string()),
1110 ..Default::default()
1111 };
1112 assert!(s.is_object());
1113 assert!(!s.is_array());
1114 assert!(!s.is_primitive());
1115 }
1116
1117 #[test]
1118 fn schema_is_primitive_string() {
1119 let s = Schema {
1120 schema_type: Some("string".to_string()),
1121 ..Default::default()
1122 };
1123 assert!(s.is_primitive());
1124 assert!(!s.is_object());
1125 assert!(!s.is_array());
1126 }
1127
1128 #[test]
1129 fn schema_is_primitive_integer() {
1130 let s = Schema {
1131 schema_type: Some("integer".to_string()),
1132 ..Default::default()
1133 };
1134 assert!(s.is_primitive());
1135 }
1136
1137 #[test]
1138 fn schema_is_primitive_number() {
1139 let s = Schema {
1140 schema_type: Some("number".to_string()),
1141 ..Default::default()
1142 };
1143 assert!(s.is_primitive());
1144 }
1145
1146 #[test]
1147 fn schema_is_primitive_boolean() {
1148 let s = Schema {
1149 schema_type: Some("boolean".to_string()),
1150 ..Default::default()
1151 };
1152 assert!(s.is_primitive());
1153 }
1154
1155 #[test]
1156 fn schema_is_enum() {
1157 let s = Schema {
1158 enum_values: Some(vec![serde_json::Value::String("a".to_string())]),
1159 ..Default::default()
1160 };
1161 assert!(s.is_enum());
1162 }
1163
1164 #[test]
1165 fn schema_is_not_enum_empty() {
1166 let s = Schema {
1167 enum_values: Some(vec![]),
1168 ..Default::default()
1169 };
1170 assert!(!s.is_enum());
1171 }
1172
1173 #[test]
1174 fn schema_is_not_enum_none() {
1175 let s = Schema::default();
1176 assert!(!s.is_enum());
1177 }
1178
1179 #[test]
1180 fn schema_is_composed_all_of() {
1181 let s = Schema {
1182 all_of: vec![Schema::default()],
1183 ..Default::default()
1184 };
1185 assert!(s.is_composed());
1186 }
1187
1188 #[test]
1189 fn schema_is_composed_one_of() {
1190 let s = Schema {
1191 one_of: vec![Schema::default()],
1192 ..Default::default()
1193 };
1194 assert!(s.is_composed());
1195 }
1196
1197 #[test]
1198 fn schema_is_not_composed_empty() {
1199 assert!(!Schema::default().is_composed());
1200 }
1201
1202 #[test]
1203 fn schema_ref_name_nested_path() {
1204 let s = Schema {
1205 ref_path: Some("#/components/schemas/deeply/Nested".to_string()),
1206 ..Default::default()
1207 };
1208 assert_eq!(s.ref_name(), Some("Nested"));
1209 }
1210
1211 #[test]
1212 fn schema_no_type_is_not_primitive_or_array_or_object() {
1213 let s = Schema::default();
1214 assert!(!s.is_primitive());
1215 assert!(!s.is_array());
1216 assert!(!s.is_object());
1217 }
1218
1219 #[test]
1222 fn operation_json_body_schema() {
1223 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1224 let post_op = spec.paths["/pets"].post.as_ref().unwrap();
1225 let body_schema = post_op.json_body_schema();
1226 assert!(body_schema.is_some());
1227 assert!(body_schema.unwrap().is_ref());
1228 }
1229
1230 #[test]
1231 fn operation_json_body_schema_none_when_no_body() {
1232 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1233 let get_op = spec.paths["/pets"].get.as_ref().unwrap();
1234 assert!(get_op.json_body_schema().is_none());
1235 }
1236
1237 #[test]
1238 fn operation_success_response_200() {
1239 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1240 let get_op = spec.paths["/pets"].get.as_ref().unwrap();
1241 let resp_schema = get_op.success_response_schema();
1242 assert!(resp_schema.is_some());
1243 assert!(resp_schema.unwrap().is_array());
1244 }
1245
1246 #[test]
1247 fn operation_success_response_201() {
1248 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1249 let post_op = spec.paths["/pets"].post.as_ref().unwrap();
1250 let resp_schema = post_op.success_response_schema();
1251 assert!(resp_schema.is_some());
1252 assert!(resp_schema.unwrap().is_ref());
1253 }
1254
1255 #[test]
1256 fn operation_success_response_none_when_no_content() {
1257 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1258 let delete_op = spec.paths["/pets/{petId}"].delete.as_ref().unwrap();
1259 assert!(delete_op.success_response_schema().is_none());
1260 }
1261
1262 #[test]
1265 fn all_operations_returns_correct_methods() {
1266 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1267 let ops = all_operations(&spec);
1268 let methods: Vec<&str> = ops.iter().map(|(m, _, _)| m.as_str()).collect();
1269 assert!(methods.contains(&"get"));
1270 assert!(methods.contains(&"post"));
1271 assert!(methods.contains(&"delete"));
1272 }
1273
1274 #[test]
1275 fn all_operations_empty_spec() {
1276 let spec: OpenApiSpec =
1277 serde_yaml_ng::from_str("info:\n title: E\n version: '1'\npaths: {}")
1278 .unwrap();
1279 assert!(all_operations(&spec).is_empty());
1280 }
1281
1282 #[test]
1285 fn parsed_schema_helpers_on_pet() {
1286 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1287 let pet = &spec.components.as_ref().unwrap().schemas["Pet"];
1288 assert!(pet.is_object());
1289 assert!(!pet.is_array());
1290 assert!(!pet.is_primitive());
1291 assert!(!pet.is_ref());
1292 assert!(!pet.is_enum());
1293 }
1294
1295 #[test]
1296 fn parsed_schema_helpers_on_pet_status() {
1297 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1298 let status = &spec.components.as_ref().unwrap().schemas["PetStatus"];
1299 assert!(status.is_primitive());
1300 assert!(status.is_enum());
1301 assert!(!status.is_object());
1302 }
1303
1304 #[test]
1305 fn parsed_schema_helpers_on_ref_property() {
1306 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1307 let pet = &spec.components.as_ref().unwrap().schemas["Pet"];
1308 let status_prop = &pet.properties["status"];
1309 assert!(status_prop.is_ref());
1310 assert_eq!(status_prop.ref_name(), Some("PetStatus"));
1311 }
1312
1313 #[test]
1316 fn roundtrip_yaml_to_json_to_yaml() {
1317 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1318 let json = serde_json::to_string_pretty(&spec).unwrap();
1319 let from_json: OpenApiSpec = serde_json::from_str(&json).unwrap();
1320 let yaml_again = serde_yaml_ng::to_string(&from_json).unwrap();
1321 let from_yaml: OpenApiSpec = serde_yaml_ng::from_str(&yaml_again).unwrap();
1322
1323 assert_eq!(from_yaml.info.title, "Pet Store");
1324 assert_eq!(from_yaml.info.version, "2.0.0");
1325 assert_eq!(from_yaml.paths.len(), 2);
1326 assert_eq!(
1327 from_yaml.components.as_ref().unwrap().schemas.len(),
1328 3
1329 );
1330 }
1331
1332 #[test]
1333 fn roundtrip_preserves_all_operations() {
1334 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1335 let json = serde_json::to_string(&spec).unwrap();
1336 let roundtrip: OpenApiSpec = serde_json::from_str(&json).unwrap();
1337 let orig_ops = all_operations(&spec);
1338 let rt_ops = all_operations(&roundtrip);
1339 assert_eq!(orig_ops.len(), rt_ops.len());
1340 for (orig, rt) in orig_ops.iter().zip(rt_ops.iter()) {
1341 assert_eq!(orig.0, rt.0);
1342 assert_eq!(orig.1, rt.1);
1343 assert_eq!(orig.2.operation_id, rt.2.operation_id);
1344 }
1345 }
1346
1347 #[test]
1348 fn roundtrip_preserves_schema_fields() {
1349 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1350 let json = serde_json::to_string(&spec).unwrap();
1351 let roundtrip: OpenApiSpec = serde_json::from_str(&json).unwrap();
1352
1353 let orig_pet = &spec.components.as_ref().unwrap().schemas["Pet"];
1354 let rt_pet = &roundtrip.components.as_ref().unwrap().schemas["Pet"];
1355 assert_eq!(orig_pet.schema_type, rt_pet.schema_type);
1356 assert_eq!(orig_pet.required, rt_pet.required);
1357 assert_eq!(orig_pet.properties.len(), rt_pet.properties.len());
1358 }
1359
1360 #[test]
1361 fn roundtrip_preserves_servers() {
1362 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1363 let json = serde_json::to_string(&spec).unwrap();
1364 let roundtrip: OpenApiSpec = serde_json::from_str(&json).unwrap();
1365 assert_eq!(spec.servers.len(), roundtrip.servers.len());
1366 assert_eq!(spec.servers[0].url, roundtrip.servers[0].url);
1367 assert_eq!(spec.servers[0].description, roundtrip.servers[0].description);
1368 }
1369
1370 #[test]
1371 fn roundtrip_preserves_security() {
1372 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1373 let json = serde_json::to_string(&spec).unwrap();
1374 let roundtrip: OpenApiSpec = serde_json::from_str(&json).unwrap();
1375 assert_eq!(spec.security, roundtrip.security);
1376 }
1377
1378 #[test]
1381 fn parse_deeply_nested_schema() {
1382 let yaml = r#"
1383info:
1384 title: Nested Test
1385 version: "1.0.0"
1386paths: {}
1387components:
1388 schemas:
1389 Outer:
1390 type: object
1391 properties:
1392 inner:
1393 type: object
1394 properties:
1395 deep:
1396 type: array
1397 items:
1398 type: string
1399"#;
1400 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1401 let outer = &spec.components.as_ref().unwrap().schemas["Outer"];
1402 let inner = &outer.properties["inner"];
1403 assert!(inner.is_object());
1404 let deep = &inner.properties["deep"];
1405 assert!(deep.is_array());
1406 let items = deep.items.as_ref().unwrap();
1407 assert!(items.is_primitive());
1408 assert_eq!(items.schema_type.as_deref(), Some("string"));
1409 }
1410
1411 #[test]
1412 fn parse_all_of_with_override_properties() {
1413 let yaml = r##"
1414info:
1415 title: AllOf Override
1416 version: "1.0.0"
1417paths: {}
1418components:
1419 schemas:
1420 Base:
1421 type: object
1422 properties:
1423 id:
1424 type: integer
1425 Extended:
1426 allOf:
1427 - $ref: "#/components/schemas/Base"
1428 - type: object
1429 required:
1430 - name
1431 properties:
1432 name:
1433 type: string
1434 role:
1435 type: string
1436 enum:
1437 - admin
1438 - user
1439"##;
1440 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1441 let extended = &spec.components.as_ref().unwrap().schemas["Extended"];
1442 assert_eq!(extended.all_of.len(), 2);
1443 let override_part = &extended.all_of[1];
1444 assert_eq!(override_part.required, vec!["name"]);
1445 assert!(override_part.properties["role"].is_enum());
1446 }
1447
1448 #[test]
1451 fn resolve_schema_ref_chain() {
1452 let yaml = r##"
1453info:
1454 title: Ref Chain
1455 version: "1.0.0"
1456paths: {}
1457components:
1458 schemas:
1459 Alias:
1460 $ref: "#/components/schemas/Real"
1461 Real:
1462 type: string
1463"##;
1464 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1465 let alias = spec.resolve_schema_ref("#/components/schemas/Alias").unwrap();
1466 assert!(alias.is_ref());
1467 let real = spec.resolve_schema_ref("#/components/schemas/Real").unwrap();
1468 assert!(real.is_primitive());
1469 }
1470
1471 #[test]
1472 fn resolve_refs_with_empty_components() {
1473 let yaml = r#"
1474info:
1475 title: Empty Components
1476 version: "1.0.0"
1477paths: {}
1478components:
1479 schemas: {}
1480"#;
1481 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1482 assert!(spec.resolve_schema_ref("#/components/schemas/Missing").is_none());
1483 assert!(spec.resolve_parameter_ref("#/components/parameters/Missing").is_none());
1484 assert!(spec.resolve_request_body_ref("#/components/requestBodies/Missing").is_none());
1485 assert!(spec.resolve_response_ref("#/components/responses/Missing").is_none());
1486 }
1487
1488 #[test]
1491 fn parse_operation_level_security() {
1492 let yaml = r#"
1493info:
1494 title: OpSecurity
1495 version: "1.0.0"
1496paths:
1497 /secure:
1498 get:
1499 operationId: secureEndpoint
1500 security:
1501 - apiKey: []
1502 - oauth2:
1503 - read
1504 - write
1505 responses:
1506 "200":
1507 description: OK
1508"#;
1509 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1510 let op = spec.paths["/secure"].get.as_ref().unwrap();
1511 assert_eq!(op.security.len(), 2);
1512 assert!(op.security[0].contains_key("apiKey"));
1513 let oauth_scopes = &op.security[1]["oauth2"];
1514 assert_eq!(oauth_scopes, &vec!["read".to_string(), "write".to_string()]);
1515 }
1516
1517 #[test]
1520 fn parse_multiple_content_types() {
1521 let yaml = r#"
1522info:
1523 title: MultiContent
1524 version: "1.0.0"
1525paths:
1526 /upload:
1527 post:
1528 operationId: upload
1529 requestBody:
1530 required: true
1531 content:
1532 application/json:
1533 schema:
1534 type: object
1535 multipart/form-data:
1536 schema:
1537 type: object
1538 responses:
1539 "200":
1540 description: OK
1541"#;
1542 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1543 let body = spec.paths["/upload"]
1544 .post
1545 .as_ref()
1546 .unwrap()
1547 .request_body
1548 .as_ref()
1549 .unwrap();
1550 assert_eq!(body.content.len(), 2);
1551 assert!(body.content.contains_key("application/json"));
1552 assert!(body.content.contains_key("multipart/form-data"));
1553 }
1554
1555 #[test]
1558 fn parse_operation_without_operation_id() {
1559 let yaml = r#"
1560info:
1561 title: NoOpId
1562 version: "1.0.0"
1563paths:
1564 /items:
1565 get:
1566 responses:
1567 "200":
1568 description: OK
1569"#;
1570 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1571 let op = spec.paths["/items"].get.as_ref().unwrap();
1572 assert!(op.operation_id.is_none());
1573 assert!(op.summary.is_none());
1574 assert!(op.description.is_none());
1575 assert!(op.tags.is_empty());
1576 assert!(op.parameters.is_empty());
1577 assert!(op.request_body.is_none());
1578 assert!(op.security.is_empty());
1579 }
1580
1581 #[test]
1584 fn all_operations_includes_put_and_patch() {
1585 let yaml = r#"
1586info:
1587 title: PutPatch
1588 version: "1.0.0"
1589paths:
1590 /items/{id}:
1591 put:
1592 operationId: replaceItem
1593 responses:
1594 "200":
1595 description: OK
1596 patch:
1597 operationId: updateItem
1598 responses:
1599 "200":
1600 description: OK
1601"#;
1602 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1603 let ops = all_operations(&spec);
1604 assert_eq!(ops.len(), 2);
1605 let methods: Vec<&str> = ops.iter().map(|(m, _, _)| m.as_str()).collect();
1606 assert!(methods.contains(&"put"));
1607 assert!(methods.contains(&"patch"));
1608 }
1609
1610 #[test]
1613 fn parse_schema_with_all_constraints() {
1614 let yaml = r#"
1615info:
1616 title: Constraints
1617 version: "1.0.0"
1618paths: {}
1619components:
1620 schemas:
1621 ConstrainedString:
1622 type: string
1623 minLength: 1
1624 maxLength: 100
1625 default: "hello"
1626 title: A constrained string
1627 description: Must be between 1 and 100 chars
1628 nullable: true
1629"#;
1630 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1631 let schema = &spec.components.as_ref().unwrap().schemas["ConstrainedString"];
1632 assert_eq!(schema.min_length, Some(1));
1633 assert_eq!(schema.max_length, Some(100));
1634 assert_eq!(
1635 schema.default.as_ref().and_then(|v| v.as_str()),
1636 Some("hello")
1637 );
1638 assert_eq!(schema.title.as_deref(), Some("A constrained string"));
1639 assert_eq!(
1640 schema.description.as_deref(),
1641 Some("Must be between 1 and 100 chars")
1642 );
1643 assert!(schema.nullable);
1644 assert!(schema.is_primitive());
1645 }
1646
1647 #[test]
1650 fn ref_name_empty_string() {
1651 assert_eq!(ref_name(""), "");
1652 }
1653
1654 #[test]
1655 fn ref_name_single_slash() {
1656 assert_eq!(ref_name("/"), "");
1657 }
1658
1659 #[test]
1660 fn ref_name_multiple_trailing_slashes() {
1661 assert_eq!(ref_name("a/b/c/"), "");
1662 }
1663
1664 #[test]
1667 fn parameter_ref_path_field_deserialized() {
1668 let json = r##"{
1669 "name": "limit",
1670 "in": "query",
1671 "$ref": "#/components/parameters/LimitParam"
1672 }"##;
1673 let param: Parameter = serde_json::from_str(json).unwrap();
1674 assert_eq!(
1675 param.ref_path.as_deref(),
1676 Some("#/components/parameters/LimitParam")
1677 );
1678 assert_eq!(param.name, "limit");
1679 }
1680
1681 #[test]
1684 fn parse_request_body_with_ref() {
1685 let yaml = r##"
1686info:
1687 title: BodyRef
1688 version: "1.0.0"
1689paths:
1690 /items:
1691 post:
1692 requestBody:
1693 $ref: "#/components/requestBodies/ItemBody"
1694 responses:
1695 "201":
1696 description: Created
1697components:
1698 requestBodies:
1699 ItemBody:
1700 required: true
1701 content:
1702 application/json:
1703 schema:
1704 type: object
1705"##;
1706 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1707 let body = spec.paths["/items"]
1708 .post
1709 .as_ref()
1710 .unwrap()
1711 .request_body
1712 .as_ref()
1713 .unwrap();
1714 assert_eq!(
1715 body.ref_path.as_deref(),
1716 Some("#/components/requestBodies/ItemBody")
1717 );
1718 }
1719
1720 #[test]
1723 fn parse_response_with_ref() {
1724 let yaml = r##"
1725info:
1726 title: RespRef
1727 version: "1.0.0"
1728paths:
1729 /items:
1730 get:
1731 responses:
1732 "404":
1733 $ref: "#/components/responses/NotFound"
1734components:
1735 responses:
1736 NotFound:
1737 description: Not found
1738"##;
1739 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1740 let resp = &spec.paths["/items"].get.as_ref().unwrap().responses["404"];
1741 assert_eq!(
1742 resp.ref_path.as_deref(),
1743 Some("#/components/responses/NotFound")
1744 );
1745 }
1746
1747 #[test]
1750 fn multiple_servers_parsed() {
1751 let yaml = r#"
1752info:
1753 title: MultiServer
1754 version: "1.0.0"
1755paths: {}
1756servers:
1757 - url: https://prod.example.com
1758 description: Production
1759 - url: https://staging.example.com
1760 description: Staging
1761 - url: http://localhost:8080
1762"#;
1763 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1764 assert_eq!(spec.servers.len(), 3);
1765 assert_eq!(spec.servers[0].url, "https://prod.example.com");
1766 assert_eq!(spec.servers[2].url, "http://localhost:8080");
1767 assert!(spec.servers[2].description.is_none());
1768 }
1769
1770 #[test]
1773 fn info_without_description() {
1774 let spec: OpenApiSpec = serde_yaml_ng::from_str(MINIMAL_SPEC_YAML).unwrap();
1775 assert!(spec.info.description.is_none());
1776 }
1777
1778 #[test]
1781 fn parse_schema_additional_properties_nested() {
1782 let yaml = r#"
1783info:
1784 title: NestedAdditional
1785 version: "1.0.0"
1786paths: {}
1787components:
1788 schemas:
1789 Map:
1790 type: object
1791 additionalProperties:
1792 type: array
1793 items:
1794 type: integer
1795"#;
1796 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1797 let map = &spec.components.as_ref().unwrap().schemas["Map"];
1798 let ap = map.additional_properties.as_ref().unwrap();
1799 assert!(ap.is_array());
1800 let items = ap.items.as_ref().unwrap();
1801 assert_eq!(items.schema_type.as_deref(), Some("integer"));
1802 }
1803
1804 #[test]
1807 fn json_body_schema_none_when_xml_only() {
1808 let yaml = r#"
1809info:
1810 title: XmlBody
1811 version: "1.0.0"
1812paths:
1813 /items:
1814 post:
1815 requestBody:
1816 content:
1817 application/xml:
1818 schema:
1819 type: object
1820 responses:
1821 "201":
1822 description: Created
1823"#;
1824 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1825 let op = spec.paths["/items"].post.as_ref().unwrap();
1826 assert!(op.json_body_schema().is_none());
1827 }
1828
1829 #[test]
1832 fn success_response_schema_none_when_no_json() {
1833 let yaml = r#"
1834info:
1835 title: NoJsonResp
1836 version: "1.0.0"
1837paths:
1838 /items:
1839 get:
1840 responses:
1841 "200":
1842 description: OK
1843 content:
1844 text/plain:
1845 schema:
1846 type: string
1847"#;
1848 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1849 let op = spec.paths["/items"].get.as_ref().unwrap();
1850 assert!(op.success_response_schema().is_none());
1851 }
1852
1853 #[test]
1854 fn success_response_schema_none_when_only_404() {
1855 let yaml = r#"
1856info:
1857 title: Only404
1858 version: "1.0.0"
1859paths:
1860 /items:
1861 get:
1862 responses:
1863 "404":
1864 description: Not found
1865"#;
1866 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
1867 let op = spec.paths["/items"].get.as_ref().unwrap();
1868 assert!(op.success_response_schema().is_none());
1869 }
1870
1871 #[test]
1876 fn spec_display_format() {
1877 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1878 let display = spec.to_string();
1879 assert_eq!(display, "OpenAPI spec: Pet Store v2.0.0 (2 paths, 4 operations)");
1880 }
1881
1882 #[test]
1883 fn spec_display_minimal() {
1884 let spec: OpenApiSpec = serde_yaml_ng::from_str(MINIMAL_SPEC_YAML).unwrap();
1885 let display = spec.to_string();
1886 assert_eq!(display, "OpenAPI spec: Test API v1.0.0 (0 paths, 0 operations)");
1887 }
1888
1889 #[test]
1890 fn info_display_format() {
1891 let info = Info {
1892 title: "Pet Store".to_string(),
1893 description: None,
1894 version: "2.0.0".to_string(),
1895 };
1896 assert_eq!(info.to_string(), "Pet Store v2.0.0");
1897 }
1898
1899 #[test]
1900 fn info_partial_eq() {
1901 let a = Info {
1902 title: "API".to_string(),
1903 description: None,
1904 version: "1.0".to_string(),
1905 };
1906 let b = a.clone();
1907 assert_eq!(a, b);
1908 }
1909
1910 #[test]
1911 fn server_partial_eq() {
1912 let a = Server {
1913 url: "https://example.com".to_string(),
1914 description: Some("prod".to_string()),
1915 };
1916 let b = a.clone();
1917 assert_eq!(a, b);
1918 }
1919
1920 #[test]
1923 fn path_item_default_has_no_operations() {
1924 let item = PathItem::default();
1925 assert!(item.get.is_none());
1926 assert!(item.post.is_none());
1927 assert!(item.put.is_none());
1928 assert!(item.delete.is_none());
1929 assert!(item.patch.is_none());
1930 assert!(item.parameters.is_empty());
1931 assert_eq!(item.operations().count(), 0);
1932 }
1933
1934 #[test]
1935 fn operation_default_has_empty_fields() {
1936 let op = Operation::default();
1937 assert!(op.operation_id.is_none());
1938 assert!(op.summary.is_none());
1939 assert!(op.description.is_none());
1940 assert!(op.parameters.is_empty());
1941 assert!(op.request_body.is_none());
1942 assert!(op.responses.is_empty());
1943 assert!(op.security.is_empty());
1944 assert!(op.tags.is_empty());
1945 }
1946
1947 #[test]
1948 fn request_body_default_is_empty() {
1949 let body = RequestBody::default();
1950 assert!(!body.required);
1951 assert!(body.content.is_empty());
1952 assert!(body.description.is_none());
1953 assert!(body.ref_path.is_none());
1954 }
1955
1956 #[test]
1957 fn media_type_default_has_no_schema() {
1958 let mt = MediaType::default();
1959 assert!(mt.schema.is_none());
1960 }
1961
1962 #[test]
1963 fn response_default_is_empty() {
1964 let resp = Response::default();
1965 assert!(resp.description.is_none());
1966 assert!(resp.content.is_none());
1967 assert!(resp.ref_path.is_none());
1968 }
1969
1970 #[test]
1971 fn spec_all_operations_iterator() {
1972 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1973 let ops: Vec<_> = spec.all_operations().collect();
1974 assert_eq!(ops.len(), 4);
1975 let methods_and_paths: Vec<(&str, &str)> =
1976 ops.iter().map(|(m, p, _)| (m.as_str(), p.as_str())).collect();
1977 assert!(methods_and_paths.contains(&("get", "/pets")));
1978 assert!(methods_and_paths.contains(&("post", "/pets")));
1979 assert!(methods_and_paths.contains(&("get", "/pets/{petId}")));
1980 assert!(methods_and_paths.contains(&("delete", "/pets/{petId}")));
1981 }
1982
1983 #[test]
1984 fn spec_all_operations_empty() {
1985 let spec: OpenApiSpec = serde_yaml_ng::from_str(MINIMAL_SPEC_YAML).unwrap();
1986 assert_eq!(spec.all_operations().count(), 0);
1987 }
1988
1989 #[test]
1990 fn spec_schema_count() {
1991 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
1992 assert_eq!(spec.schema_count(), 3);
1993 }
1994
1995 #[test]
1996 fn spec_schema_count_no_components() {
1997 let spec: OpenApiSpec = serde_yaml_ng::from_str(MINIMAL_SPEC_YAML).unwrap();
1998 assert_eq!(spec.schema_count(), 0);
1999 }
2000
2001 #[test]
2002 fn spec_operation_count() {
2003 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
2004 assert_eq!(spec.operation_count(), 4);
2005 }
2006
2007 #[test]
2008 fn spec_operation_count_empty() {
2009 let spec: OpenApiSpec = serde_yaml_ng::from_str(MINIMAL_SPEC_YAML).unwrap();
2010 assert_eq!(spec.operation_count(), 0);
2011 }
2012
2013 #[test]
2014 fn path_item_operations_returns_defined_methods() {
2015 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
2016 let pets = &spec.paths["/pets"];
2017 let methods: Vec<&str> = pets.operations().map(|(m, _)| m).collect();
2018 assert_eq!(methods, vec!["get", "post"]);
2019 }
2020
2021 #[test]
2022 fn path_item_operations_empty_when_no_methods() {
2023 let item = PathItem {
2024 get: None,
2025 post: None,
2026 put: None,
2027 delete: None,
2028 patch: None,
2029 parameters: vec![],
2030 };
2031 assert_eq!(item.operations().count(), 0);
2032 }
2033
2034 #[test]
2035 fn path_item_operations_all_five_methods() {
2036 let yaml = r#"
2037info:
2038 title: AllMethods
2039 version: "1.0.0"
2040paths:
2041 /resource:
2042 get:
2043 responses:
2044 "200":
2045 description: OK
2046 post:
2047 responses:
2048 "201":
2049 description: Created
2050 put:
2051 responses:
2052 "200":
2053 description: OK
2054 delete:
2055 responses:
2056 "204":
2057 description: Deleted
2058 patch:
2059 responses:
2060 "200":
2061 description: OK
2062"#;
2063 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2064 let item = &spec.paths["/resource"];
2065 let methods: Vec<&str> = item.operations().map(|(m, _)| m).collect();
2066 assert_eq!(methods, vec!["get", "post", "put", "delete", "patch"]);
2067 }
2068
2069 #[test]
2072 fn schema_programmatic_construction_roundtrip_string() {
2073 let schema = Schema {
2074 schema_type: Some("string".to_string()),
2075 format: Some("date-time".to_string()),
2076 description: Some("A timestamp".to_string()),
2077 min_length: Some(1),
2078 max_length: Some(64),
2079 ..Default::default()
2080 };
2081 let json = serde_json::to_string(&schema).unwrap();
2082 let roundtrip: Schema = serde_json::from_str(&json).unwrap();
2083 assert_eq!(roundtrip.schema_type.as_deref(), Some("string"));
2084 assert_eq!(roundtrip.format.as_deref(), Some("date-time"));
2085 assert_eq!(roundtrip.description.as_deref(), Some("A timestamp"));
2086 assert_eq!(roundtrip.min_length, Some(1));
2087 assert_eq!(roundtrip.max_length, Some(64));
2088 }
2089
2090 #[test]
2091 fn schema_programmatic_construction_roundtrip_integer() {
2092 let schema = Schema {
2093 schema_type: Some("integer".to_string()),
2094 format: Some("int32".to_string()),
2095 minimum: Some(0.0),
2096 maximum: Some(999.0),
2097 default: Some(serde_json::Value::Number(42.into())),
2098 ..Default::default()
2099 };
2100 let json = serde_json::to_string(&schema).unwrap();
2101 let roundtrip: Schema = serde_json::from_str(&json).unwrap();
2102 assert_eq!(roundtrip.schema_type.as_deref(), Some("integer"));
2103 assert_eq!(roundtrip.format.as_deref(), Some("int32"));
2104 assert_eq!(roundtrip.minimum, Some(0.0));
2105 assert_eq!(roundtrip.maximum, Some(999.0));
2106 assert_eq!(roundtrip.default.unwrap().as_i64(), Some(42));
2107 }
2108
2109 #[test]
2110 fn schema_programmatic_construction_roundtrip_number() {
2111 let schema = Schema {
2112 schema_type: Some("number".to_string()),
2113 format: Some("double".to_string()),
2114 ..Default::default()
2115 };
2116 let json = serde_json::to_string(&schema).unwrap();
2117 let roundtrip: Schema = serde_json::from_str(&json).unwrap();
2118 assert_eq!(roundtrip.schema_type.as_deref(), Some("number"));
2119 assert_eq!(roundtrip.format.as_deref(), Some("double"));
2120 }
2121
2122 #[test]
2123 fn schema_programmatic_construction_roundtrip_boolean() {
2124 let schema = Schema {
2125 schema_type: Some("boolean".to_string()),
2126 default: Some(serde_json::Value::Bool(true)),
2127 ..Default::default()
2128 };
2129 let json = serde_json::to_string(&schema).unwrap();
2130 let roundtrip: Schema = serde_json::from_str(&json).unwrap();
2131 assert_eq!(roundtrip.schema_type.as_deref(), Some("boolean"));
2132 assert_eq!(roundtrip.default.unwrap().as_bool(), Some(true));
2133 }
2134
2135 #[test]
2136 fn schema_programmatic_construction_roundtrip_array_with_ref_items() {
2137 let schema = Schema {
2138 schema_type: Some("array".to_string()),
2139 items: Some(Box::new(Schema {
2140 ref_path: Some("#/components/schemas/Item".to_string()),
2141 ..Default::default()
2142 })),
2143 ..Default::default()
2144 };
2145 let json = serde_json::to_string(&schema).unwrap();
2146 let roundtrip: Schema = serde_json::from_str(&json).unwrap();
2147 assert!(roundtrip.is_array());
2148 let items = roundtrip.items.as_ref().unwrap();
2149 assert!(items.is_ref());
2150 assert_eq!(items.ref_name(), Some("Item"));
2151 }
2152
2153 #[test]
2154 fn schema_programmatic_construction_roundtrip_object_with_properties() {
2155 let mut props = BTreeMap::new();
2156 props.insert(
2157 "name".to_string(),
2158 Schema {
2159 schema_type: Some("string".to_string()),
2160 ..Default::default()
2161 },
2162 );
2163 props.insert(
2164 "age".to_string(),
2165 Schema {
2166 schema_type: Some("integer".to_string()),
2167 ..Default::default()
2168 },
2169 );
2170 let schema = Schema {
2171 schema_type: Some("object".to_string()),
2172 properties: props,
2173 required: vec!["name".to_string()],
2174 ..Default::default()
2175 };
2176 let json = serde_json::to_string(&schema).unwrap();
2177 let roundtrip: Schema = serde_json::from_str(&json).unwrap();
2178 assert!(roundtrip.is_object());
2179 assert_eq!(roundtrip.properties.len(), 2);
2180 assert_eq!(roundtrip.required, vec!["name"]);
2181 assert_eq!(
2182 roundtrip.properties["name"].schema_type.as_deref(),
2183 Some("string")
2184 );
2185 assert_eq!(
2186 roundtrip.properties["age"].schema_type.as_deref(),
2187 Some("integer")
2188 );
2189 }
2190
2191 #[test]
2192 fn schema_programmatic_construction_roundtrip_nullable() {
2193 let schema = Schema {
2194 schema_type: Some("string".to_string()),
2195 nullable: true,
2196 ..Default::default()
2197 };
2198 let json = serde_json::to_string(&schema).unwrap();
2199 let roundtrip: Schema = serde_json::from_str(&json).unwrap();
2200 assert!(roundtrip.nullable);
2201 }
2202
2203 #[test]
2204 fn schema_programmatic_construction_roundtrip_enum() {
2205 let schema = Schema {
2206 schema_type: Some("string".to_string()),
2207 enum_values: Some(vec![
2208 serde_json::Value::String("active".to_string()),
2209 serde_json::Value::String("inactive".to_string()),
2210 serde_json::Value::String("pending".to_string()),
2211 ]),
2212 ..Default::default()
2213 };
2214 let json = serde_json::to_string(&schema).unwrap();
2215 let roundtrip: Schema = serde_json::from_str(&json).unwrap();
2216 assert!(roundtrip.is_enum());
2217 let values = roundtrip.enum_values.unwrap();
2218 assert_eq!(values.len(), 3);
2219 assert_eq!(values[0].as_str(), Some("active"));
2220 }
2221
2222 #[test]
2223 fn schema_programmatic_construction_roundtrip_composition() {
2224 let schema = Schema {
2225 all_of: vec![
2226 Schema {
2227 ref_path: Some("#/components/schemas/Base".to_string()),
2228 ..Default::default()
2229 },
2230 Schema {
2231 schema_type: Some("object".to_string()),
2232 properties: {
2233 let mut p = BTreeMap::new();
2234 p.insert(
2235 "extra".to_string(),
2236 Schema {
2237 schema_type: Some("string".to_string()),
2238 ..Default::default()
2239 },
2240 );
2241 p
2242 },
2243 ..Default::default()
2244 },
2245 ],
2246 ..Default::default()
2247 };
2248 let json = serde_json::to_string(&schema).unwrap();
2249 let roundtrip: Schema = serde_json::from_str(&json).unwrap();
2250 assert!(roundtrip.is_composed());
2251 assert_eq!(roundtrip.all_of.len(), 2);
2252 assert!(roundtrip.all_of[0].is_ref());
2253 assert!(roundtrip.all_of[1].is_object());
2254 }
2255
2256 #[test]
2257 fn schema_programmatic_construction_roundtrip_additional_properties() {
2258 let schema = Schema {
2259 schema_type: Some("object".to_string()),
2260 additional_properties: Some(Box::new(Schema {
2261 schema_type: Some("integer".to_string()),
2262 ..Default::default()
2263 })),
2264 ..Default::default()
2265 };
2266 let json = serde_json::to_string(&schema).unwrap();
2267 let roundtrip: Schema = serde_json::from_str(&json).unwrap();
2268 let ap = roundtrip.additional_properties.as_ref().unwrap();
2269 assert_eq!(ap.schema_type.as_deref(), Some("integer"));
2270 }
2271
2272 #[test]
2275 fn parse_header_parameter() {
2276 let yaml = r#"
2277info:
2278 title: HeaderParam
2279 version: "1.0.0"
2280paths:
2281 /items:
2282 get:
2283 parameters:
2284 - name: X-Request-ID
2285 in: header
2286 required: true
2287 description: Unique request identifier
2288 schema:
2289 type: string
2290 format: uuid
2291 responses:
2292 "200":
2293 description: OK
2294"#;
2295 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2296 let param = &spec.paths["/items"].get.as_ref().unwrap().parameters[0];
2297 assert_eq!(param.name, "X-Request-ID");
2298 assert_eq!(param.location, "header");
2299 assert!(param.required);
2300 assert_eq!(
2301 param.description.as_deref(),
2302 Some("Unique request identifier")
2303 );
2304 let schema = param.schema.as_ref().unwrap();
2305 assert_eq!(schema.schema_type.as_deref(), Some("string"));
2306 assert_eq!(schema.format.as_deref(), Some("uuid"));
2307 }
2308
2309 #[test]
2310 fn parse_cookie_parameter() {
2311 let yaml = r#"
2312info:
2313 title: CookieParam
2314 version: "1.0.0"
2315paths:
2316 /items:
2317 get:
2318 parameters:
2319 - name: session_id
2320 in: cookie
2321 required: false
2322 description: Session cookie
2323 schema:
2324 type: string
2325 responses:
2326 "200":
2327 description: OK
2328"#;
2329 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2330 let param = &spec.paths["/items"].get.as_ref().unwrap().parameters[0];
2331 assert_eq!(param.name, "session_id");
2332 assert_eq!(param.location, "cookie");
2333 assert!(!param.required);
2334 assert_eq!(param.description.as_deref(), Some("Session cookie"));
2335 }
2336
2337 #[test]
2338 fn parse_all_four_parameter_locations() {
2339 let yaml = r#"
2340info:
2341 title: AllParamLocations
2342 version: "1.0.0"
2343paths:
2344 /items/{id}:
2345 get:
2346 parameters:
2347 - name: id
2348 in: path
2349 required: true
2350 schema:
2351 type: string
2352 - name: filter
2353 in: query
2354 schema:
2355 type: string
2356 - name: Authorization
2357 in: header
2358 schema:
2359 type: string
2360 - name: tracking
2361 in: cookie
2362 schema:
2363 type: string
2364 responses:
2365 "200":
2366 description: OK
2367"#;
2368 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2369 let params = &spec.paths["/items/{id}"].get.as_ref().unwrap().parameters;
2370 assert_eq!(params.len(), 4);
2371 let locations: Vec<&str> = params.iter().map(|p| p.location.as_str()).collect();
2372 assert_eq!(locations, vec!["path", "query", "header", "cookie"]);
2373 }
2374
2375 #[test]
2378 fn parse_schema_format_int32() {
2379 let yaml = r#"
2380info:
2381 title: Formats
2382 version: "1.0.0"
2383paths: {}
2384components:
2385 schemas:
2386 Int32Field:
2387 type: integer
2388 format: int32
2389"#;
2390 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2391 let schema = &spec.components.as_ref().unwrap().schemas["Int32Field"];
2392 assert_eq!(schema.format.as_deref(), Some("int32"));
2393 }
2394
2395 #[test]
2396 fn parse_schema_format_float() {
2397 let yaml = r#"
2398info:
2399 title: Formats
2400 version: "1.0.0"
2401paths: {}
2402components:
2403 schemas:
2404 FloatField:
2405 type: number
2406 format: float
2407"#;
2408 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2409 let schema = &spec.components.as_ref().unwrap().schemas["FloatField"];
2410 assert_eq!(schema.format.as_deref(), Some("float"));
2411 }
2412
2413 #[test]
2414 fn parse_schema_format_date_and_datetime() {
2415 let yaml = r#"
2416info:
2417 title: Formats
2418 version: "1.0.0"
2419paths: {}
2420components:
2421 schemas:
2422 DateField:
2423 type: string
2424 format: date
2425 DateTimeField:
2426 type: string
2427 format: date-time
2428"#;
2429 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2430 let components = spec.components.as_ref().unwrap();
2431 assert_eq!(
2432 components.schemas["DateField"].format.as_deref(),
2433 Some("date")
2434 );
2435 assert_eq!(
2436 components.schemas["DateTimeField"].format.as_deref(),
2437 Some("date-time")
2438 );
2439 }
2440
2441 #[test]
2442 fn parse_schema_format_byte_and_binary() {
2443 let yaml = r#"
2444info:
2445 title: Formats
2446 version: "1.0.0"
2447paths: {}
2448components:
2449 schemas:
2450 ByteField:
2451 type: string
2452 format: byte
2453 BinaryField:
2454 type: string
2455 format: binary
2456"#;
2457 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2458 let components = spec.components.as_ref().unwrap();
2459 assert_eq!(
2460 components.schemas["ByteField"].format.as_deref(),
2461 Some("byte")
2462 );
2463 assert_eq!(
2464 components.schemas["BinaryField"].format.as_deref(),
2465 Some("binary")
2466 );
2467 }
2468
2469 #[test]
2470 fn parse_schema_format_password() {
2471 let yaml = r#"
2472info:
2473 title: Formats
2474 version: "1.0.0"
2475paths: {}
2476components:
2477 schemas:
2478 PasswordField:
2479 type: string
2480 format: password
2481"#;
2482 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2483 let schema = &spec.components.as_ref().unwrap().schemas["PasswordField"];
2484 assert_eq!(schema.format.as_deref(), Some("password"));
2485 }
2486
2487 #[test]
2490 fn ref_name_extracts_from_various_component_types() {
2491 assert_eq!(ref_name("#/components/schemas/User"), "User");
2492 assert_eq!(ref_name("#/components/parameters/PageSize"), "PageSize");
2493 assert_eq!(
2494 ref_name("#/components/requestBodies/CreateUser"),
2495 "CreateUser"
2496 );
2497 assert_eq!(ref_name("#/components/responses/ErrorResponse"), "ErrorResponse");
2498 assert_eq!(
2499 ref_name("#/components/securitySchemes/BearerAuth"),
2500 "BearerAuth"
2501 );
2502 }
2503
2504 #[test]
2505 fn schema_ref_name_returns_none_for_non_ref() {
2506 let s = Schema {
2507 schema_type: Some("string".to_string()),
2508 ..Default::default()
2509 };
2510 assert!(!s.is_ref());
2511 assert_eq!(s.ref_name(), None);
2512 }
2513
2514 #[test]
2515 fn resolve_schema_ref_follows_simple_pointer() {
2516 let yaml = r##"
2517info:
2518 title: RefResolve
2519 version: "1.0.0"
2520paths: {}
2521components:
2522 schemas:
2523 Address:
2524 type: object
2525 properties:
2526 street:
2527 type: string
2528 city:
2529 type: string
2530 required:
2531 - street
2532 - city
2533"##;
2534 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2535 let address = spec
2536 .resolve_schema_ref("#/components/schemas/Address")
2537 .unwrap();
2538 assert!(address.is_object());
2539 assert_eq!(address.properties.len(), 2);
2540 assert_eq!(address.required, vec!["street", "city"]);
2541 }
2542
2543 #[test]
2546 fn parse_operation_with_description() {
2547 let yaml = r#"
2548info:
2549 title: DescTest
2550 version: "1.0.0"
2551paths:
2552 /items:
2553 get:
2554 operationId: listItems
2555 summary: List items
2556 description: Returns a paginated list of all items in the system
2557 responses:
2558 "200":
2559 description: OK
2560"#;
2561 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2562 let op = spec.paths["/items"].get.as_ref().unwrap();
2563 assert_eq!(
2564 op.description.as_deref(),
2565 Some("Returns a paginated list of all items in the system")
2566 );
2567 }
2568
2569 #[test]
2572 fn parse_request_body_with_description() {
2573 let yaml = r#"
2574info:
2575 title: BodyDesc
2576 version: "1.0.0"
2577paths:
2578 /items:
2579 post:
2580 requestBody:
2581 description: The item to create
2582 required: true
2583 content:
2584 application/json:
2585 schema:
2586 type: object
2587 responses:
2588 "201":
2589 description: Created
2590"#;
2591 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2592 let body = spec.paths["/items"]
2593 .post
2594 .as_ref()
2595 .unwrap()
2596 .request_body
2597 .as_ref()
2598 .unwrap();
2599 assert_eq!(body.description.as_deref(), Some("The item to create"));
2600 }
2601
2602 #[test]
2605 fn roundtrip_preserves_enum_values() {
2606 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
2607 let json = serde_json::to_string(&spec).unwrap();
2608 let roundtrip: OpenApiSpec = serde_json::from_str(&json).unwrap();
2609 let status = &roundtrip.components.as_ref().unwrap().schemas["PetStatus"];
2610 assert!(status.is_enum());
2611 let values = status.enum_values.as_ref().unwrap();
2612 assert_eq!(values.len(), 3);
2613 assert_eq!(values[0].as_str(), Some("available"));
2614 assert_eq!(values[1].as_str(), Some("pending"));
2615 assert_eq!(values[2].as_str(), Some("sold"));
2616 }
2617
2618 #[test]
2619 fn roundtrip_preserves_parameter_details() {
2620 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
2621 let json = serde_json::to_string(&spec).unwrap();
2622 let roundtrip: OpenApiSpec = serde_json::from_str(&json).unwrap();
2623 let param = &roundtrip.paths["/pets"].get.as_ref().unwrap().parameters[0];
2624 assert_eq!(param.name, "limit");
2625 assert_eq!(param.location, "query");
2626 assert!(!param.required);
2627 let schema = param.schema.as_ref().unwrap();
2628 assert_eq!(schema.schema_type.as_deref(), Some("integer"));
2629 assert_eq!(schema.format.as_deref(), Some("int64"));
2630 }
2631
2632 #[test]
2633 fn roundtrip_preserves_request_body() {
2634 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
2635 let json = serde_json::to_string(&spec).unwrap();
2636 let roundtrip: OpenApiSpec = serde_json::from_str(&json).unwrap();
2637 let body = roundtrip.paths["/pets"]
2638 .post
2639 .as_ref()
2640 .unwrap()
2641 .request_body
2642 .as_ref()
2643 .unwrap();
2644 assert!(body.required);
2645 assert!(body.content.contains_key("application/json"));
2646 }
2647
2648 #[test]
2649 fn roundtrip_preserves_path_level_parameters() {
2650 let spec: OpenApiSpec = serde_yaml_ng::from_str(FULL_SPEC_YAML).unwrap();
2651 let json = serde_json::to_string(&spec).unwrap();
2652 let roundtrip: OpenApiSpec = serde_json::from_str(&json).unwrap();
2653 let params = &roundtrip.paths["/pets/{petId}"].parameters;
2654 assert_eq!(params.len(), 1);
2655 assert_eq!(params[0].name, "petId");
2656 assert_eq!(params[0].location, "path");
2657 assert!(params[0].required);
2658 }
2659
2660 #[test]
2663 fn parse_any_of_with_refs() {
2664 let yaml = r##"
2665info:
2666 title: AnyOfRefs
2667 version: "1.0.0"
2668paths: {}
2669components:
2670 schemas:
2671 Cat:
2672 type: object
2673 properties:
2674 meow:
2675 type: boolean
2676 Dog:
2677 type: object
2678 properties:
2679 bark:
2680 type: boolean
2681 Pet:
2682 anyOf:
2683 - $ref: "#/components/schemas/Cat"
2684 - $ref: "#/components/schemas/Dog"
2685"##;
2686 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2687 let pet = &spec.components.as_ref().unwrap().schemas["Pet"];
2688 assert!(pet.is_composed());
2689 assert_eq!(pet.any_of.len(), 2);
2690 assert!(pet.any_of[0].is_ref());
2691 assert_eq!(pet.any_of[0].ref_name(), Some("Cat"));
2692 assert!(pet.any_of[1].is_ref());
2693 assert_eq!(pet.any_of[1].ref_name(), Some("Dog"));
2694 }
2695
2696 #[test]
2697 fn parse_one_of_with_discriminator_like_pattern() {
2698 let yaml = r##"
2699info:
2700 title: OneOfDiscriminator
2701 version: "1.0.0"
2702paths: {}
2703components:
2704 schemas:
2705 Circle:
2706 type: object
2707 required:
2708 - radius
2709 properties:
2710 shape_type:
2711 type: string
2712 radius:
2713 type: number
2714 Square:
2715 type: object
2716 required:
2717 - side
2718 properties:
2719 shape_type:
2720 type: string
2721 side:
2722 type: number
2723 Shape:
2724 oneOf:
2725 - $ref: "#/components/schemas/Circle"
2726 - $ref: "#/components/schemas/Square"
2727"##;
2728 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2729 let shape = &spec.components.as_ref().unwrap().schemas["Shape"];
2730 assert!(shape.is_composed());
2731 assert_eq!(shape.one_of.len(), 2);
2732 assert_eq!(shape.one_of[0].ref_name(), Some("Circle"));
2733 assert_eq!(shape.one_of[1].ref_name(), Some("Square"));
2734 let circle = spec
2736 .resolve_schema_ref("#/components/schemas/Circle")
2737 .unwrap();
2738 assert!(circle.is_object());
2739 assert_eq!(circle.required, vec!["radius"]);
2740 }
2741
2742 #[test]
2745 fn schema_with_null_json_values_deserializes() {
2746 let json = r#"{
2747 "type": "object",
2748 "format": null,
2749 "description": null,
2750 "title": null,
2751 "default": null
2752 }"#;
2753 let schema: Schema = serde_json::from_str(json).unwrap();
2754 assert!(schema.is_object());
2755 assert!(schema.format.is_none());
2756 assert!(schema.description.is_none());
2757 assert!(schema.title.is_none());
2758 assert!(schema.default.is_none());
2759 }
2760
2761 #[test]
2762 fn schema_from_empty_json_object() {
2763 let schema: Schema = serde_json::from_str("{}").unwrap();
2764 assert!(schema.schema_type.is_none());
2765 assert!(schema.format.is_none());
2766 assert!(schema.properties.is_empty());
2767 assert!(schema.items.is_none());
2768 assert!(schema.required.is_empty());
2769 assert!(schema.enum_values.is_none());
2770 assert!(schema.ref_path.is_none());
2771 assert!(schema.all_of.is_empty());
2772 assert!(schema.one_of.is_empty());
2773 assert!(schema.any_of.is_empty());
2774 assert!(!schema.nullable);
2775 assert!(schema.additional_properties.is_none());
2776 assert!(schema.title.is_none());
2777 }
2778
2779 #[test]
2780 fn operation_from_minimal_json() {
2781 let json = r#"{"responses":{}}"#;
2782 let op: Operation = serde_json::from_str(json).unwrap();
2783 assert!(op.operation_id.is_none());
2784 assert!(op.summary.is_none());
2785 assert!(op.parameters.is_empty());
2786 assert!(op.tags.is_empty());
2787 }
2788
2789 #[test]
2792 fn parse_api_key_security_scheme_in_cookie() {
2793 let yaml = r#"
2794info:
2795 title: CookieApiKey
2796 version: "1.0.0"
2797paths: {}
2798components:
2799 securitySchemes:
2800 cookieAuth:
2801 type: apiKey
2802 in: cookie
2803 name: JSESSIONID
2804 description: Session cookie authentication
2805"#;
2806 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2807 let scheme = &spec.components.as_ref().unwrap().security_schemes["cookieAuth"];
2808 assert_eq!(scheme.scheme_type, "apiKey");
2809 assert_eq!(scheme.location.as_deref(), Some("cookie"));
2810 assert_eq!(scheme.name.as_deref(), Some("JSESSIONID"));
2811 assert_eq!(
2812 scheme.description.as_deref(),
2813 Some("Session cookie authentication")
2814 );
2815 }
2816
2817 #[test]
2820 fn all_operations_with_refs_in_request_and_response() {
2821 let yaml = r##"
2822info:
2823 title: RefOps
2824 version: "1.0.0"
2825paths:
2826 /users:
2827 get:
2828 operationId: listUsers
2829 responses:
2830 "200":
2831 description: OK
2832 content:
2833 application/json:
2834 schema:
2835 type: array
2836 items:
2837 $ref: "#/components/schemas/User"
2838 post:
2839 operationId: createUser
2840 requestBody:
2841 required: true
2842 content:
2843 application/json:
2844 schema:
2845 $ref: "#/components/schemas/CreateUserRequest"
2846 responses:
2847 "201":
2848 description: Created
2849 content:
2850 application/json:
2851 schema:
2852 $ref: "#/components/schemas/User"
2853components:
2854 schemas:
2855 User:
2856 type: object
2857 properties:
2858 id:
2859 type: integer
2860 name:
2861 type: string
2862 CreateUserRequest:
2863 type: object
2864 required:
2865 - name
2866 properties:
2867 name:
2868 type: string
2869"##;
2870 let spec: OpenApiSpec = serde_yaml_ng::from_str(yaml).unwrap();
2871 let ops: Vec<_> = spec.all_operations().collect();
2872 assert_eq!(ops.len(), 2);
2873
2874 let get_op = ops.iter().find(|(m, _, _)| m == "get").unwrap();
2876 let resp_schema = get_op.2.success_response_schema().unwrap();
2877 assert!(resp_schema.is_array());
2878 let items = resp_schema.items.as_ref().unwrap();
2879 assert!(items.is_ref());
2880 assert_eq!(items.ref_name(), Some("User"));
2881
2882 let post_op = ops.iter().find(|(m, _, _)| m == "post").unwrap();
2884 let body_schema = post_op.2.json_body_schema().unwrap();
2885 assert!(body_schema.is_ref());
2886 assert_eq!(body_schema.ref_name(), Some("CreateUserRequest"));
2887
2888 let post_resp = post_op.2.success_response_schema().unwrap();
2890 assert!(post_resp.is_ref());
2891 assert_eq!(post_resp.ref_name(), Some("User"));
2892
2893 let user = spec
2895 .resolve_schema_ref("#/components/schemas/User")
2896 .unwrap();
2897 assert!(user.is_object());
2898 assert_eq!(user.properties.len(), 2);
2899 }
2900
2901 #[test]
2904 fn info_partial_eq_different_versions() {
2905 let a = Info {
2906 title: "API".to_string(),
2907 description: None,
2908 version: "1.0".to_string(),
2909 };
2910 let b = Info {
2911 title: "API".to_string(),
2912 description: None,
2913 version: "2.0".to_string(),
2914 };
2915 assert_ne!(a, b);
2916 }
2917
2918 #[test]
2919 fn info_partial_eq_with_description() {
2920 let a = Info {
2921 title: "API".to_string(),
2922 description: Some("desc".to_string()),
2923 version: "1.0".to_string(),
2924 };
2925 let b = Info {
2926 title: "API".to_string(),
2927 description: None,
2928 version: "1.0".to_string(),
2929 };
2930 assert_ne!(a, b);
2931 }
2932}