Skip to main content

volga_open_api/
doc.rs

1//! Types and utils for OpenAPI documents.
2
3use super::{op::OpenApiOperation, schema::OpenApiSchema};
4use serde::{Deserialize, Serialize};
5use std::collections::{BTreeMap, BTreeSet, VecDeque};
6
7/// Represents OpenAPI document.
8#[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/// Represents OpenAPI info.
17#[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/// Represents OpenAPI components.
26#[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}