1use crate::{
6 error::{OpenApiError, OpenApiResult},
7 specification::OpenApiSpec,
8};
9use std::fs;
10use std::path::Path;
11
12pub struct OpenApiUtils;
14
15impl OpenApiUtils {
16 pub fn validate_spec(spec: &OpenApiSpec) -> OpenApiResult<Vec<ValidationWarning>> {
18 let mut warnings = Vec::new();
19
20 if spec.info.title.is_empty() {
22 warnings.push(ValidationWarning::new(
23 "info.title is required but empty",
24 ValidationLevel::Error,
25 ));
26 }
27
28 if spec.info.version.is_empty() {
29 warnings.push(ValidationWarning::new(
30 "info.version is required but empty",
31 ValidationLevel::Error,
32 ));
33 }
34
35 if spec.openapi != "3.0.3" && !spec.openapi.starts_with("3.0") {
37 warnings.push(ValidationWarning::new(
38 &format!(
39 "OpenAPI version {} may not be fully supported",
40 spec.openapi
41 ),
42 ValidationLevel::Warning,
43 ));
44 }
45
46 if spec.paths.is_empty() {
48 warnings.push(ValidationWarning::new(
49 "No paths defined in specification",
50 ValidationLevel::Warning,
51 ));
52 }
53
54 for (path, path_item) in &spec.paths {
56 if !path.starts_with('/') {
57 warnings.push(ValidationWarning::new(
58 &format!("Path '{}' should start with '/'", path),
59 ValidationLevel::Warning,
60 ));
61 }
62
63 let has_operations = path_item.get.is_some()
65 || path_item.post.is_some()
66 || path_item.put.is_some()
67 || path_item.delete.is_some()
68 || path_item.patch.is_some()
69 || path_item.options.is_some()
70 || path_item.head.is_some()
71 || path_item.trace.is_some();
72
73 if !has_operations {
74 warnings.push(ValidationWarning::new(
75 &format!("Path '{}' has no operations defined", path),
76 ValidationLevel::Warning,
77 ));
78 }
79
80 let operations = vec![
82 ("GET", &path_item.get),
83 ("POST", &path_item.post),
84 ("PUT", &path_item.put),
85 ("DELETE", &path_item.delete),
86 ("PATCH", &path_item.patch),
87 ("OPTIONS", &path_item.options),
88 ("HEAD", &path_item.head),
89 ("TRACE", &path_item.trace),
90 ];
91
92 for (method, operation) in operations {
93 if let Some(op) = operation {
94 if op.responses.is_empty() {
95 warnings.push(ValidationWarning::new(
96 &format!("{} {} has no responses defined", method, path),
97 ValidationLevel::Error,
98 ));
99 }
100
101 if let Some(op_id) = &op.operation_id {
103 if op_id.is_empty() {
104 warnings.push(ValidationWarning::new(
105 &format!("{} {} has empty operationId", method, path),
106 ValidationLevel::Warning,
107 ));
108 }
109 }
110 }
111 }
112 }
113
114 if let Some(components) = &spec.components {
116 for schema_name in components.schemas.keys() {
118 let reference = format!("#/components/schemas/{}", schema_name);
119 let is_used = Self::is_schema_referenced(spec, &reference);
120 if !is_used {
121 warnings.push(ValidationWarning::new(
122 &format!("Schema '{}' is defined but never referenced", schema_name),
123 ValidationLevel::Info,
124 ));
125 }
126 }
127 }
128
129 Ok(warnings)
130 }
131
132 fn is_schema_referenced(spec: &OpenApiSpec, reference: &str) -> bool {
134 for path_item in spec.paths.values() {
136 if Self::is_schema_in_path_item(path_item, reference) {
137 return true;
138 }
139 }
140
141 if let Some(components) = &spec.components {
143 for schema in components.schemas.values() {
145 if Self::is_schema_in_schema(schema, reference) {
146 return true;
147 }
148 }
149
150 for response in components.responses.values() {
152 if Self::is_schema_in_response(response, reference) {
153 return true;
154 }
155 }
156
157 for request_body in components.request_bodies.values() {
159 if Self::is_schema_in_request_body(request_body, reference) {
160 return true;
161 }
162 }
163
164 for parameter in components.parameters.values() {
166 if Self::is_schema_in_parameter(parameter, reference) {
167 return true;
168 }
169 }
170
171 for header in components.headers.values() {
173 if Self::is_schema_in_header(header, reference) {
174 return true;
175 }
176 }
177 }
178
179 false
180 }
181
182 fn is_schema_in_path_item(path_item: &crate::specification::PathItem, reference: &str) -> bool {
184 let operations = vec![
185 &path_item.get,
186 &path_item.post,
187 &path_item.put,
188 &path_item.delete,
189 &path_item.patch,
190 &path_item.options,
191 &path_item.head,
192 &path_item.trace,
193 ];
194
195 for operation in operations.into_iter().flatten() {
196 if Self::is_schema_in_operation(operation, reference) {
197 return true;
198 }
199 }
200
201 for parameter in &path_item.parameters {
203 if Self::is_schema_in_parameter(parameter, reference) {
204 return true;
205 }
206 }
207
208 false
209 }
210
211 fn is_schema_in_operation(
213 operation: &crate::specification::Operation,
214 reference: &str,
215 ) -> bool {
216 for parameter in &operation.parameters {
218 if Self::is_schema_in_parameter(parameter, reference) {
219 return true;
220 }
221 }
222
223 if let Some(request_body) = &operation.request_body {
225 if Self::is_schema_in_request_body(request_body, reference) {
226 return true;
227 }
228 }
229
230 for response in operation.responses.values() {
232 if Self::is_schema_in_response(response, reference) {
233 return true;
234 }
235 }
236
237 false
238 }
239
240 fn is_schema_in_parameter(
242 parameter: &crate::specification::Parameter,
243 reference: &str,
244 ) -> bool {
245 if let Some(schema) = ¶meter.schema {
246 Self::is_schema_in_schema(schema, reference)
247 } else {
248 false
249 }
250 }
251
252 fn is_schema_in_request_body(
254 request_body: &crate::specification::RequestBody,
255 reference: &str,
256 ) -> bool {
257 for media_type in request_body.content.values() {
258 if let Some(schema) = &media_type.schema {
259 if Self::is_schema_in_schema(schema, reference) {
260 return true;
261 }
262 }
263 }
264 false
265 }
266
267 fn is_schema_in_response(response: &crate::specification::Response, reference: &str) -> bool {
269 for media_type in response.content.values() {
271 if let Some(schema) = &media_type.schema {
272 if Self::is_schema_in_schema(schema, reference) {
273 return true;
274 }
275 }
276 }
277
278 for header in response.headers.values() {
280 if Self::is_schema_in_header(header, reference) {
281 return true;
282 }
283 }
284
285 false
286 }
287
288 fn is_schema_in_header(header: &crate::specification::Header, reference: &str) -> bool {
290 if let Some(schema) = &header.schema {
291 Self::is_schema_in_schema(schema, reference)
292 } else {
293 false
294 }
295 }
296
297 fn is_schema_in_schema(schema: &crate::specification::Schema, reference: &str) -> bool {
299 if let Some(ref_str) = &schema.reference {
301 if ref_str == reference {
302 return true;
303 }
304 }
305
306 for property_schema in schema.properties.values() {
308 if Self::is_schema_in_schema(property_schema, reference) {
309 return true;
310 }
311 }
312
313 if let Some(additional_properties) = &schema.additional_properties {
315 if Self::is_schema_in_schema(additional_properties, reference) {
316 return true;
317 }
318 }
319
320 if let Some(items_schema) = &schema.items {
322 if Self::is_schema_in_schema(items_schema, reference) {
323 return true;
324 }
325 }
326
327 for composed_schema in &schema.all_of {
329 if Self::is_schema_in_schema(composed_schema, reference) {
330 return true;
331 }
332 }
333
334 for composed_schema in &schema.any_of {
335 if Self::is_schema_in_schema(composed_schema, reference) {
336 return true;
337 }
338 }
339
340 for composed_schema in &schema.one_of {
341 if Self::is_schema_in_schema(composed_schema, reference) {
342 return true;
343 }
344 }
345
346 false
347 }
348
349 pub fn save_spec_to_file<P: AsRef<Path>>(
351 spec: &OpenApiSpec,
352 path: P,
353 format: OutputFormat,
354 pretty: bool,
355 ) -> OpenApiResult<()> {
356 let content = match format {
357 OutputFormat::Json => {
358 if pretty {
359 serde_json::to_string_pretty(spec)?
360 } else {
361 serde_json::to_string(spec)?
362 }
363 }
364 OutputFormat::Yaml => serde_yaml::to_string(spec)?,
365 };
366
367 fs::write(path.as_ref(), content).map_err(OpenApiError::Io)?;
368
369 Ok(())
370 }
371
372 pub fn load_spec_from_file<P: AsRef<Path>>(path: P) -> OpenApiResult<OpenApiSpec> {
374 let content = fs::read_to_string(path.as_ref()).map_err(OpenApiError::Io)?;
375
376 let extension = path
377 .as_ref()
378 .extension()
379 .and_then(|ext| ext.to_str())
380 .unwrap_or("");
381
382 match extension.to_lowercase().as_str() {
383 "json" => serde_json::from_str(&content).map_err(OpenApiError::from),
384 "yaml" | "yml" => serde_yaml::from_str(&content).map_err(OpenApiError::from),
385 _ => {
386 if content.trim_start().starts_with('{') {
388 serde_json::from_str(&content).map_err(OpenApiError::from)
389 } else {
390 serde_yaml::from_str(&content).map_err(OpenApiError::from)
391 }
392 }
393 }
394 }
395
396 pub fn merge_specs(base: &mut OpenApiSpec, other: &OpenApiSpec) -> OpenApiResult<()> {
398 for (path, path_item) in &other.paths {
400 if base.paths.contains_key(path) {
401 return Err(OpenApiError::validation_error(format!(
402 "Path '{}' already exists in base specification",
403 path
404 )));
405 }
406 base.paths.insert(path.clone(), path_item.clone());
407 }
408
409 if let Some(other_components) = &other.components {
411 let base_components = base.components.get_or_insert_with(Default::default);
412
413 for (name, schema) in &other_components.schemas {
415 if base_components.schemas.contains_key(name) {
416 return Err(OpenApiError::validation_error(format!(
417 "Schema '{}' already exists in base specification",
418 name
419 )));
420 }
421 base_components.schemas.insert(name.clone(), schema.clone());
422 }
423
424 for (name, response) in &other_components.responses {
426 base_components
427 .responses
428 .insert(name.clone(), response.clone());
429 }
430 }
431
432 for tag in &other.tags {
434 if !base.tags.iter().any(|t| t.name == tag.name) {
435 base.tags.push(tag.clone());
436 }
437 }
438
439 Ok(())
440 }
441
442 pub fn generate_example_from_schema(
444 schema: &crate::specification::Schema,
445 ) -> OpenApiResult<serde_json::Value> {
446 use serde_json::{Map, Value};
447
448 match schema.schema_type.as_deref() {
449 Some("object") => {
450 let mut obj = Map::new();
451 for (prop_name, prop_schema) in &schema.properties {
452 let example = Self::generate_example_from_schema(prop_schema)?;
453 obj.insert(prop_name.clone(), example);
454 }
455 Ok(Value::Object(obj))
456 }
457 Some("array") => {
458 if let Some(items_schema) = &schema.items {
459 let item_example = Self::generate_example_from_schema(items_schema)?;
460 Ok(Value::Array(vec![item_example]))
461 } else {
462 Ok(Value::Array(vec![]))
463 }
464 }
465 Some("string") => {
466 if !schema.enum_values.is_empty() {
467 Ok(schema.enum_values[0].clone())
468 } else {
469 match schema.format.as_deref() {
470 Some("email") => Ok(Value::String("user@example.com".to_string())),
471 Some("uri") => Ok(Value::String("https://example.com".to_string())),
472 Some("date") => Ok(Value::String("2023-12-01".to_string())),
473 Some("date-time") => Ok(Value::String("2023-12-01T12:00:00Z".to_string())),
474 Some("uuid") => Ok(Value::String(
475 "123e4567-e89b-12d3-a456-426614174000".to_string(),
476 )),
477 _ => Ok(Value::String("string".to_string())),
478 }
479 }
480 }
481 Some("integer") => match schema.format.as_deref() {
482 Some("int64") => Ok(Value::Number(serde_json::Number::from(42i64))),
483 _ => Ok(Value::Number(serde_json::Number::from(42i32))),
484 },
485 Some("number") => Ok(Value::Number(
486 serde_json::Number::from_f64(std::f64::consts::PI).unwrap(),
487 )),
488 Some("boolean") => Ok(Value::Bool(true)),
489 _ => {
490 if let Some(example) = &schema.example {
491 Ok(example.clone())
492 } else {
493 Ok(Value::Null)
494 }
495 }
496 }
497 }
498
499 pub fn generate_operation_summary(method: &str, path: &str) -> String {
501 let verb = method.to_lowercase();
502 let resource = Self::extract_resource_from_path(path);
503
504 match verb.as_str() {
505 "get" => {
506 if path.contains('{') {
507 format!("Get {}", resource)
508 } else {
509 format!("List {}", Self::pluralize(&resource))
510 }
511 }
512 "post" => format!("Create {}", resource),
513 "put" => format!("Update {}", resource),
514 "patch" => format!("Partially update {}", resource),
515 "delete" => format!("Delete {}", resource),
516 _ => format!("{} {}", verb, resource),
517 }
518 }
519
520 fn extract_resource_from_path(path: &str) -> String {
522 let parts: Vec<&str> = path.split('/').filter(|p| !p.is_empty()).collect();
523
524 if let Some(last_part) = parts.last() {
525 if last_part.starts_with('{') {
526 if parts.len() > 1 {
528 Self::singularize(parts[parts.len() - 2])
529 } else {
530 "resource".to_string()
531 }
532 } else {
533 Self::singularize(last_part)
534 }
535 } else {
536 "resource".to_string()
537 }
538 }
539
540 fn singularize(word: &str) -> String {
542 if word.ends_with("ies") {
543 word.trim_end_matches("ies").to_string() + "y"
544 } else if word.ends_with('s') && !word.ends_with("ss") {
545 word.trim_end_matches('s').to_string()
546 } else {
547 word.to_string()
548 }
549 }
550
551 fn pluralize(word: &str) -> String {
553 if word.ends_with('y') {
554 word.trim_end_matches('y').to_string() + "ies"
555 } else if word.ends_with("s") || word.ends_with("sh") || word.ends_with("ch") {
556 word.to_string() + "es"
557 } else {
558 word.to_string() + "s"
559 }
560 }
561}
562
563#[derive(Debug, Clone)]
565pub enum OutputFormat {
566 Json,
567 Yaml,
568}
569
570#[derive(Debug, Clone, PartialEq)]
572pub enum ValidationLevel {
573 Error,
574 Warning,
575 Info,
576}
577
578#[derive(Debug, Clone)]
580pub struct ValidationWarning {
581 pub message: String,
582 pub level: ValidationLevel,
583}
584
585impl ValidationWarning {
586 pub fn new(message: &str, level: ValidationLevel) -> Self {
587 Self {
588 message: message.to_string(),
589 level,
590 }
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 use crate::specification::Schema;
598 use std::collections::HashMap;
599
600 #[test]
601 fn test_operation_summary_generation() {
602 assert_eq!(
603 OpenApiUtils::generate_operation_summary("GET", "/users"),
604 "List users"
605 );
606 assert_eq!(
607 OpenApiUtils::generate_operation_summary("GET", "/users/{id}"),
608 "Get user"
609 );
610 assert_eq!(
611 OpenApiUtils::generate_operation_summary("POST", "/users"),
612 "Create user"
613 );
614 assert_eq!(
615 OpenApiUtils::generate_operation_summary("PUT", "/users/{id}"),
616 "Update user"
617 );
618 assert_eq!(
619 OpenApiUtils::generate_operation_summary("DELETE", "/users/{id}"),
620 "Delete user"
621 );
622 }
623
624 #[test]
625 fn test_resource_extraction() {
626 assert_eq!(OpenApiUtils::extract_resource_from_path("/users"), "user");
627 assert_eq!(
628 OpenApiUtils::extract_resource_from_path("/users/{id}"),
629 "user"
630 );
631 assert_eq!(
632 OpenApiUtils::extract_resource_from_path("/api/v1/posts/{id}/comments"),
633 "comment"
634 );
635 assert_eq!(OpenApiUtils::extract_resource_from_path("/"), "resource");
636 }
637
638 #[test]
639 fn test_singularization() {
640 assert_eq!(OpenApiUtils::singularize("users"), "user");
641 assert_eq!(OpenApiUtils::singularize("posts"), "post");
642 assert_eq!(OpenApiUtils::singularize("categories"), "category");
643 assert_eq!(OpenApiUtils::singularize("companies"), "company");
644 assert_eq!(OpenApiUtils::singularize("class"), "class"); }
646
647 #[test]
648 fn test_pluralization() {
649 assert_eq!(OpenApiUtils::pluralize("user"), "users");
650 assert_eq!(OpenApiUtils::pluralize("post"), "posts");
651 assert_eq!(OpenApiUtils::pluralize("category"), "categories");
652 assert_eq!(OpenApiUtils::pluralize("company"), "companies");
653 assert_eq!(OpenApiUtils::pluralize("class"), "classes");
654 }
655
656 #[test]
657 fn test_example_generation() {
658 let string_schema = Schema {
659 schema_type: Some("string".to_string()),
660 ..Default::default()
661 };
662 let example = OpenApiUtils::generate_example_from_schema(&string_schema).unwrap();
663 assert_eq!(example, serde_json::Value::String("string".to_string()));
664
665 let integer_schema = Schema {
666 schema_type: Some("integer".to_string()),
667 ..Default::default()
668 };
669 let example = OpenApiUtils::generate_example_from_schema(&integer_schema).unwrap();
670 assert_eq!(
671 example,
672 serde_json::Value::Number(serde_json::Number::from(42))
673 );
674 }
675
676 #[test]
677 fn test_spec_validation() {
678 let mut spec = OpenApiSpec::new("Test API", "1.0.0");
679 spec.paths = HashMap::new();
680
681 let warnings = OpenApiUtils::validate_spec(&spec).unwrap();
682
683 assert!(warnings
685 .iter()
686 .any(|w| w.message.contains("No paths defined")));
687 }
688
689 #[test]
690 fn test_schema_reference_detection_accurate() {
691 use crate::specification::*;
692
693 let mut spec = OpenApiSpec::new("Test API", "1.0.0");
695
696 let user_schema = Schema {
698 schema_type: Some("object".to_string()),
699 properties: {
700 let mut props = HashMap::new();
701 props.insert(
702 "id".to_string(),
703 Schema {
704 schema_type: Some("integer".to_string()),
705 ..Default::default()
706 },
707 );
708 props.insert(
709 "name".to_string(),
710 Schema {
711 schema_type: Some("string".to_string()),
712 ..Default::default()
713 },
714 );
715 props
716 },
717 required: vec!["id".to_string(), "name".to_string()],
718 ..Default::default()
719 };
720
721 let address_schema = Schema {
723 schema_type: Some("object".to_string()),
724 properties: {
725 let mut props = HashMap::new();
726 props.insert(
727 "street".to_string(),
728 Schema {
729 schema_type: Some("string".to_string()),
730 ..Default::default()
731 },
732 );
733 props.insert(
734 "owner".to_string(),
735 Schema {
736 reference: Some("#/components/schemas/User".to_string()),
737 ..Default::default()
738 },
739 );
740 props
741 },
742 ..Default::default()
743 };
744
745 let unused_schema = Schema {
747 schema_type: Some("object".to_string()),
748 properties: {
749 let mut props = HashMap::new();
750 props.insert(
751 "value".to_string(),
752 Schema {
753 schema_type: Some("string".to_string()),
754 ..Default::default()
755 },
756 );
757 props
758 },
759 ..Default::default()
760 };
761
762 let mut components = Components::default();
764 components.schemas.insert("User".to_string(), user_schema);
765 components
766 .schemas
767 .insert("Address".to_string(), address_schema);
768 components
769 .schemas
770 .insert("UnusedSchema".to_string(), unused_schema);
771 spec.components = Some(components);
772
773 assert!(OpenApiUtils::is_schema_referenced(
775 &spec,
776 "#/components/schemas/User"
777 ));
778 assert!(!OpenApiUtils::is_schema_referenced(
779 &spec,
780 "#/components/schemas/UnusedSchema"
781 ));
782 assert!(!OpenApiUtils::is_schema_referenced(
783 &spec,
784 "#/components/schemas/NonExistent"
785 ));
786 }
787
788 #[test]
789 fn test_schema_reference_false_positive_prevention() {
790 use crate::specification::*;
791
792 let mut spec = OpenApiSpec::new("Test API", "1.0.0");
794
795 let user_schema = Schema {
797 schema_type: Some("object".to_string()),
798 description: Some(
799 "This schema represents a user. See also #/components/schemas/User for details."
800 .to_string(),
801 ),
802 properties: {
803 let mut props = HashMap::new();
804 props.insert(
805 "name".to_string(),
806 Schema {
807 schema_type: Some("string".to_string()),
808 ..Default::default()
809 },
810 );
811 props
812 },
813 ..Default::default()
814 };
815
816 let example_schema = Schema {
818 schema_type: Some("string".to_string()),
819 example: Some(serde_json::Value::String(
820 "#/components/schemas/User".to_string(),
821 )),
822 ..Default::default()
823 };
824
825 let mut components = Components::default();
826 components.schemas.insert("User".to_string(), user_schema);
827 components
828 .schemas
829 .insert("Example".to_string(), example_schema);
830 spec.components = Some(components);
831
832 assert!(!OpenApiUtils::is_schema_referenced(
835 &spec,
836 "#/components/schemas/User"
837 ));
838 assert!(!OpenApiUtils::is_schema_referenced(
839 &spec,
840 "#/components/schemas/Example"
841 ));
842 }
843
844 #[test]
845 fn test_schema_reference_in_operations() {
846 use crate::specification::*;
847
848 let mut spec = OpenApiSpec::new("Test API", "1.0.0");
849
850 let user_schema = Schema {
852 schema_type: Some("object".to_string()),
853 ..Default::default()
854 };
855
856 let request_body = RequestBody {
858 description: Some("User data".to_string()),
859 content: {
860 let mut content = HashMap::new();
861 content.insert(
862 "application/json".to_string(),
863 MediaType {
864 schema: Some(Schema {
865 reference: Some("#/components/schemas/User".to_string()),
866 ..Default::default()
867 }),
868 example: None,
869 examples: HashMap::new(),
870 },
871 );
872 content
873 },
874 required: Some(true),
875 };
876
877 let operation = Operation {
878 request_body: Some(request_body),
879 responses: {
880 let mut responses = HashMap::new();
881 responses.insert(
882 "200".to_string(),
883 Response {
884 description: "Success".to_string(),
885 content: {
886 let mut content = HashMap::new();
887 content.insert(
888 "application/json".to_string(),
889 MediaType {
890 schema: Some(Schema {
891 reference: Some("#/components/schemas/User".to_string()),
892 ..Default::default()
893 }),
894 example: None,
895 examples: HashMap::new(),
896 },
897 );
898 content
899 },
900 headers: HashMap::new(),
901 links: HashMap::new(),
902 },
903 );
904 responses
905 },
906 ..Default::default()
907 };
908
909 let path_item = PathItem {
910 post: Some(operation),
911 ..Default::default()
912 };
913
914 spec.paths.insert("/users".to_string(), path_item);
915
916 let mut components = Components::default();
917 components.schemas.insert("User".to_string(), user_schema);
918 spec.components = Some(components);
919
920 assert!(OpenApiUtils::is_schema_referenced(
922 &spec,
923 "#/components/schemas/User"
924 ));
925 }
926
927 #[test]
928 fn test_schema_reference_in_nested_schemas() {
929 use crate::specification::*;
930
931 let mut spec = OpenApiSpec::new("Test API", "1.0.0");
932
933 let user_schema = Schema {
935 schema_type: Some("object".to_string()),
936 ..Default::default()
937 };
938
939 let profile_schema = Schema {
940 schema_type: Some("object".to_string()),
941 properties: {
942 let mut props = HashMap::new();
943 props.insert(
944 "user".to_string(),
945 Schema {
946 reference: Some("#/components/schemas/User".to_string()),
947 ..Default::default()
948 },
949 );
950 props
951 },
952 ..Default::default()
953 };
954
955 let response_schema = Schema {
956 schema_type: Some("object".to_string()),
957 properties: {
958 let mut props = HashMap::new();
959 props.insert(
960 "data".to_string(),
961 Schema {
962 schema_type: Some("array".to_string()),
963 items: Some(Box::new(Schema {
964 reference: Some("#/components/schemas/Profile".to_string()),
965 ..Default::default()
966 })),
967 ..Default::default()
968 },
969 );
970 props
971 },
972 ..Default::default()
973 };
974
975 let mut components = Components::default();
976 components.schemas.insert("User".to_string(), user_schema);
977 components
978 .schemas
979 .insert("Profile".to_string(), profile_schema);
980 components
981 .schemas
982 .insert("Response".to_string(), response_schema);
983 spec.components = Some(components);
984
985 assert!(OpenApiUtils::is_schema_referenced(
987 &spec,
988 "#/components/schemas/User"
989 ));
990 assert!(OpenApiUtils::is_schema_referenced(
991 &spec,
992 "#/components/schemas/Profile"
993 ));
994 }
995
996 #[test]
997 fn test_schema_reference_in_composition() {
998 use crate::specification::*;
999
1000 let mut spec = OpenApiSpec::new("Test API", "1.0.0");
1001
1002 let base_schema = Schema {
1003 schema_type: Some("object".to_string()),
1004 ..Default::default()
1005 };
1006
1007 let extended_schema = Schema {
1008 all_of: vec![
1009 Schema {
1010 reference: Some("#/components/schemas/Base".to_string()),
1011 ..Default::default()
1012 },
1013 Schema {
1014 schema_type: Some("object".to_string()),
1015 properties: {
1016 let mut props = HashMap::new();
1017 props.insert(
1018 "extra".to_string(),
1019 Schema {
1020 schema_type: Some("string".to_string()),
1021 ..Default::default()
1022 },
1023 );
1024 props
1025 },
1026 ..Default::default()
1027 },
1028 ],
1029 ..Default::default()
1030 };
1031
1032 let mut components = Components::default();
1033 components.schemas.insert("Base".to_string(), base_schema);
1034 components
1035 .schemas
1036 .insert("Extended".to_string(), extended_schema);
1037 spec.components = Some(components);
1038
1039 assert!(OpenApiUtils::is_schema_referenced(
1041 &spec,
1042 "#/components/schemas/Base"
1043 ));
1044 }
1045}