Skip to main content

oapi_codegen/
filter.rs

1//! Spec-level operation and schema filtering.
2//!
3//! Mirrors `oapi-codegen`'s `output-options` filters: operations are dropped
4//! when they fail any active include/exclude **tag** or **operation-id** filter,
5//! and component schemas named in `exclude-schemas` are removed before lowering.
6//! Filtering runs before pruning, so removing an operation lets the prune pass
7//! drop any component schemas it uniquely referenced.
8
9use openapiv3::OpenAPI;
10use openapiv3::Operation;
11use openapiv3::PathItem;
12use openapiv3::ReferenceOr;
13
14use crate::config::OutputOptions;
15
16/// Apply the configured operation and schema filters to `doc` in place.
17pub fn apply(doc: &mut OpenAPI, opts: &OutputOptions) {
18    filter_operations(doc, opts);
19    exclude_schemas(doc, &opts.exclude_schemas);
20}
21
22/// Whether any operation-level (tag or operation-id) filter is configured.
23fn 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
30/// Remove operations that fail any active tag or operation-id filter.
31fn 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
50/// Mutable references to every operation slot on a path item, in a stable
51/// verb order (`get`, `put`, `post`, `delete`, `options`, `head`, `patch`,
52/// `trace`).
53fn 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
66/// Whether `operation` must be dropped given the configured filters.
67///
68/// An operation is kept only when it carries none of the excluded tags, carries
69/// one of the included tags (when `include-tags` is set), is not an excluded
70/// operation-id, and is an included operation-id (when `include-operation-ids`
71/// is set) — matching `oapi-codegen`'s sequential exclude-then-include filters.
72fn 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
101/// Whether `values` contains `needle`.
102fn contains_str(values: &[String], needle: &str) -> bool {
103    return values.iter().any(|value| {
104        return value == needle;
105    });
106}
107
108/// Drop component schemas whose names appear in `exclude`.
109fn 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    /// `operation_slots` must reference every operation verb openapiv3
125    /// recognises: nulling all slots must leave the path item with no
126    /// operations. This cross-checks our hand-listed slots against openapiv3's
127    /// authoritative `PathItem::iter`, catching drift if a verb is ever added.
128    #[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}