1use super::{op::OpenApiOperation, schema::OpenApiSchema};
4use serde::{Deserialize, Serialize};
5use std::collections::{BTreeMap, BTreeSet, VecDeque};
6
7#[derive(Clone, Debug, Serialize, Deserialize)]
9pub struct OpenApiDocument {
10 pub(super) openapi: String,
11 pub(super) info: OpenApiInfo,
12 pub(super) components: OpenApiComponents,
13 pub(super) paths: BTreeMap<String, BTreeMap<String, OpenApiOperation>>,
14}
15
16#[derive(Clone, Debug, Serialize, Deserialize)]
18pub(super) struct OpenApiInfo {
19 pub(super) title: String,
20 pub(super) version: String,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub(super) description: Option<String>,
23}
24
25#[derive(Clone, Debug, Serialize, Deserialize)]
27pub(super) struct OpenApiComponents {
28 pub(super) schemas: BTreeMap<String, OpenApiSchema>,
29}
30
31impl OpenApiDocument {
32 pub(super) fn prune_unreferenced_components(&mut self) {
33 let mut reachable = BTreeSet::new();
34
35 for methods in self.paths.values() {
36 for op in methods.values() {
37 op.collect_component_refs(&mut reachable);
38 }
39 }
40
41 let mut queue: VecDeque<String> = reachable.iter().cloned().collect();
42 while let Some(name) = queue.pop_front() {
43 let Some(schema) = self.components.schemas.get(&name) else {
44 continue;
45 };
46
47 let mut nested = BTreeSet::new();
48 schema.collect_component_refs(&mut nested);
49
50 for nested_name in nested {
51 if reachable.insert(nested_name.clone()) {
52 queue.push_back(nested_name);
53 }
54 }
55 }
56
57 self.components
58 .schemas
59 .retain(|name, _| reachable.contains(name));
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use crate::op::OpenApiOperation;
67
68 #[test]
69 fn prune_unreferenced_components_keeps_only_reachable_transitive_refs() {
70 let mut op = OpenApiOperation::default();
71 op.set_response_body(
72 200,
73 OpenApiSchema::reference("User"),
74 None,
75 "application/json",
76 );
77
78 let mut paths = BTreeMap::new();
79 paths.insert(
80 "/users".to_string(),
81 BTreeMap::from([("get".to_string(), op)]),
82 );
83
84 let mut schemas = BTreeMap::new();
85 schemas.insert(
86 "User".to_string(),
87 OpenApiSchema::object().with_property("profile", OpenApiSchema::reference("Profile")),
88 );
89 schemas.insert(
90 "Profile".to_string(),
91 OpenApiSchema::object().with_property("name", OpenApiSchema::string()),
92 );
93 schemas.insert(
94 "Unused".to_string(),
95 OpenApiSchema::object().with_property("x", OpenApiSchema::integer()),
96 );
97
98 let mut doc = OpenApiDocument {
99 openapi: "3.0.0".to_string(),
100 info: OpenApiInfo {
101 title: "Test".to_string(),
102 version: "1.0.0".to_string(),
103 description: None,
104 },
105 components: OpenApiComponents { schemas },
106 paths,
107 };
108
109 doc.prune_unreferenced_components();
110
111 assert!(doc.components.schemas.contains_key("User"));
112 assert!(doc.components.schemas.contains_key("Profile"));
113 assert!(!doc.components.schemas.contains_key("Unused"));
114 assert_eq!(doc.components.schemas.len(), 2);
115 }
116}