1use openapiv3::OpenAPI;
10use openapiv3::Operation;
11use openapiv3::PathItem;
12use openapiv3::ReferenceOr;
13
14use crate::config::OutputOptions;
15
16pub fn apply(doc: &mut OpenAPI, opts: &OutputOptions) {
18 filter_operations(doc, opts);
19 exclude_schemas(doc, &opts.exclude_schemas);
20}
21
22fn has_operation_filters(opts: &OutputOptions) -> bool {
24 return !opts.include_tags.is_empty()
25 || !opts.exclude_tags.is_empty()
26 || !opts.include_operation_ids.is_empty()
27 || !opts.exclude_operation_ids.is_empty();
28}
29
30fn filter_operations(doc: &mut OpenAPI, opts: &OutputOptions) {
32 if !has_operation_filters(opts) {
33 return;
34 }
35 for (_, entry) in doc.paths.paths.iter_mut() {
36 let ReferenceOr::Item(item) = entry else {
37 continue;
38 };
39 for slot in operation_slots(item) {
40 let remove = slot.as_ref().is_some_and(|op| {
41 return is_filtered_out(op, opts);
42 });
43 if remove {
44 *slot = None;
45 }
46 }
47 }
48}
49
50fn operation_slots(item: &mut PathItem) -> [&mut Option<Operation>; 8] {
54 return [
55 &mut item.get,
56 &mut item.put,
57 &mut item.post,
58 &mut item.delete,
59 &mut item.options,
60 &mut item.head,
61 &mut item.patch,
62 &mut item.trace,
63 ];
64}
65
66fn is_filtered_out(operation: &Operation, opts: &OutputOptions) -> bool {
73 if !opts.exclude_tags.is_empty()
74 && operation.tags.iter().any(|tag| {
75 return opts.exclude_tags.contains(tag);
76 })
77 {
78 return true;
79 }
80 if !opts.include_tags.is_empty()
81 && !operation.tags.iter().any(|tag| {
82 return opts.include_tags.contains(tag);
83 })
84 {
85 return true;
86 }
87 let id = operation.operation_id.as_deref();
88 if !opts.exclude_operation_ids.is_empty()
89 && id.is_some_and(|id| return contains_str(&opts.exclude_operation_ids, id))
90 {
91 return true;
92 }
93 if !opts.include_operation_ids.is_empty()
94 && !id.is_some_and(|id| return contains_str(&opts.include_operation_ids, id))
95 {
96 return true;
97 }
98 return false;
99}
100
101fn contains_str(values: &[String], needle: &str) -> bool {
103 return values.iter().any(|value| {
104 return value == needle;
105 });
106}
107
108fn exclude_schemas(doc: &mut OpenAPI, exclude: &[String]) {
110 if exclude.is_empty() {
111 return;
112 }
113 if let Some(components) = doc.components.as_mut() {
114 components.schemas.retain(|name, _| {
115 return !exclude.contains(name);
116 });
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
129 fn operation_slots_cover_every_verb() {
130 let populated = || {
131 return Some(Operation::default());
132 };
133 let mut item = PathItem {
134 get: populated(),
135 put: populated(),
136 post: populated(),
137 delete: populated(),
138 options: populated(),
139 head: populated(),
140 patch: populated(),
141 trace: populated(),
142 ..Default::default()
143 };
144 assert_eq!(
145 item.iter().count(),
146 operation_slots(&mut item).len(),
147 "every populated verb should map to one slot"
148 );
149 for slot in operation_slots(&mut item) {
150 *slot = None;
151 }
152 assert_eq!(
153 item.iter().count(),
154 0,
155 "operation_slots must cover every verb reported by openapiv3's PathItem::iter",
156 );
157 }
158}