elif_openapi/
utils.rs

1/*!
2Utility functions for OpenAPI generation.
3*/
4
5use crate::{
6    error::{OpenApiError, OpenApiResult},
7    specification::OpenApiSpec,
8};
9use std::fs;
10use std::path::Path;
11
12/// Utility functions for OpenAPI operations
13pub struct OpenApiUtils;
14
15impl OpenApiUtils {
16    /// Validate an OpenAPI specification
17    pub fn validate_spec(spec: &OpenApiSpec) -> OpenApiResult<Vec<ValidationWarning>> {
18        let mut warnings = Vec::new();
19
20        // Check required fields
21        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        // Check OpenAPI version
36        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        // Check paths
47        if spec.paths.is_empty() {
48            warnings.push(ValidationWarning::new(
49                "No paths defined in specification",
50                ValidationLevel::Warning,
51            ));
52        }
53
54        // Validate path operations
55        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            // Check if path has at least one operation
64            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            // Validate operations
81            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                    // Check for operation ID uniqueness would require global tracking
102                    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        // Validate components
115        if let Some(components) = &spec.components {
116            // Check for unused schemas
117            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    /// Check if a schema is referenced anywhere in the spec using proper recursive traversal
133    fn is_schema_referenced(spec: &OpenApiSpec, reference: &str) -> bool {
134        // Check in paths and operations
135        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        // Check in components
142        if let Some(components) = &spec.components {
143            // Check in schema definitions themselves (for nested references)
144            for schema in components.schemas.values() {
145                if Self::is_schema_in_schema(schema, reference) {
146                    return true;
147                }
148            }
149
150            // Check in responses
151            for response in components.responses.values() {
152                if Self::is_schema_in_response(response, reference) {
153                    return true;
154                }
155            }
156
157            // Check in request bodies
158            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            // Check in parameters
165            for parameter in components.parameters.values() {
166                if Self::is_schema_in_parameter(parameter, reference) {
167                    return true;
168                }
169            }
170
171            // Check in headers
172            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    /// Check if schema is referenced in a path item
183    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        // Check path-level parameters
202        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    /// Check if schema is referenced in an operation
212    fn is_schema_in_operation(
213        operation: &crate::specification::Operation,
214        reference: &str,
215    ) -> bool {
216        // Check parameters
217        for parameter in &operation.parameters {
218            if Self::is_schema_in_parameter(parameter, reference) {
219                return true;
220            }
221        }
222
223        // Check request body
224        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        // Check responses
231        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    /// Check if schema is referenced in a parameter
241    fn is_schema_in_parameter(
242        parameter: &crate::specification::Parameter,
243        reference: &str,
244    ) -> bool {
245        if let Some(schema) = &parameter.schema {
246            Self::is_schema_in_schema(schema, reference)
247        } else {
248            false
249        }
250    }
251
252    /// Check if schema is referenced in a request body
253    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    /// Check if schema is referenced in a response
268    fn is_schema_in_response(response: &crate::specification::Response, reference: &str) -> bool {
269        // Check response content
270        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        // Check response headers
279        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    /// Check if schema is referenced in a header
289    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    /// Check if schema is referenced within another schema (recursive)
298    fn is_schema_in_schema(schema: &crate::specification::Schema, reference: &str) -> bool {
299        // Check direct reference
300        if let Some(ref_str) = &schema.reference {
301            if ref_str == reference {
302                return true;
303            }
304        }
305
306        // Check properties (for object schemas)
307        for property_schema in schema.properties.values() {
308            if Self::is_schema_in_schema(property_schema, reference) {
309                return true;
310            }
311        }
312
313        // Check additional properties
314        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        // Check items (for array schemas)
321        if let Some(items_schema) = &schema.items {
322            if Self::is_schema_in_schema(items_schema, reference) {
323                return true;
324            }
325        }
326
327        // Check composition schemas (allOf, anyOf, oneOf)
328        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    /// Save OpenAPI specification to file
350    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    /// Load OpenAPI specification from file
373    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                // Try to detect format from content
387                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    /// Merge two OpenAPI specifications
397    pub fn merge_specs(base: &mut OpenApiSpec, other: &OpenApiSpec) -> OpenApiResult<()> {
398        // Merge paths
399        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        // Merge components
410        if let Some(other_components) = &other.components {
411            let base_components = base.components.get_or_insert_with(Default::default);
412
413            // Merge schemas
414            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            // Merge other components...
425            for (name, response) in &other_components.responses {
426                base_components
427                    .responses
428                    .insert(name.clone(), response.clone());
429            }
430        }
431
432        // Merge tags
433        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    /// Generate example request/response from schema
443    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    /// Extract operation summary from function name
500    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    /// Extract resource name from path
521    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                // Path parameter, use previous part
527                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    /// Simple singularization
541    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    /// Simple pluralization
552    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/// Output format for saving specifications
564#[derive(Debug, Clone)]
565pub enum OutputFormat {
566    Json,
567    Yaml,
568}
569
570/// Validation warning levels
571#[derive(Debug, Clone, PartialEq)]
572pub enum ValidationLevel {
573    Error,
574    Warning,
575    Info,
576}
577
578/// Validation warning
579#[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"); // shouldn't change
645    }
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        // Should have warning about no paths
684        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        // Create a spec with schemas and verify accurate detection
694        let mut spec = OpenApiSpec::new("Test API", "1.0.0");
695
696        // Add a User schema
697        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        // Add an Address schema that references User
722        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        // Add an unused schema for testing
746        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        // Set up components
763        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        // Test reference detection
774        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        // Create a spec where schema reference appears in description but not as actual reference
793        let mut spec = OpenApiSpec::new("Test API", "1.0.0");
794
795        // Add a schema with reference string in description (should NOT be detected as reference)
796        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        // Add an example with schema reference in the example value
817        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        // The old string-based approach would incorrectly detect these as references
833        // The new approach should correctly identify that User is not actually referenced
834        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        // Create a schema
851        let user_schema = Schema {
852            schema_type: Some("object".to_string()),
853            ..Default::default()
854        };
855
856        // Create an operation that uses the schema in request body
857        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        // User schema should be detected as referenced in the operation
921        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        // Create deeply nested schema structure
934        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        // Both User and Profile should be detected as referenced
986        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        // Base schema should be detected as referenced in allOf composition
1040        assert!(OpenApiUtils::is_schema_referenced(
1041            &spec,
1042            "#/components/schemas/Base"
1043        ));
1044    }
1045}