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    return removes_operation(opts, &operation.tags, operation.operation_id.as_deref());
74}
75
76/// Whether the filters in `opts` remove an operation with these `tags` and this
77/// operation id. The spec inspection reads the same rule on the untyped
78/// document, so the two cannot disagree on which operations a run keeps.
79pub(crate) fn removes_operation(opts: &OutputOptions, tags: &[String], id: Option<&str>) -> bool {
80    if !opts.exclude_tags.is_empty()
81        && tags.iter().any(|tag| {
82            return opts.exclude_tags.contains(tag);
83        })
84    {
85        return true;
86    }
87    if !opts.include_tags.is_empty()
88        && !tags.iter().any(|tag| {
89            return opts.include_tags.contains(tag);
90        })
91    {
92        return true;
93    }
94    if !opts.exclude_operation_ids.is_empty()
95        && id.is_some_and(|id| return contains_str(&opts.exclude_operation_ids, id))
96    {
97        return true;
98    }
99    if !opts.include_operation_ids.is_empty()
100        && !id.is_some_and(|id| return contains_str(&opts.include_operation_ids, id))
101    {
102        return true;
103    }
104    return false;
105}
106
107/// Whether `values` contains `needle`.
108fn contains_str(values: &[String], needle: &str) -> bool {
109    return values.iter().any(|value| {
110        return value == needle;
111    });
112}
113
114/// Drop component schemas whose names appear in `exclude`.
115fn exclude_schemas(doc: &mut OpenAPI, exclude: &[String]) {
116    if exclude.is_empty() {
117        return;
118    }
119    if let Some(components) = doc.components.as_mut() {
120        components.schemas.retain(|name, _| {
121            return !exclude.contains(name);
122        });
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    /// `operation_slots` must reference every operation verb openapiv3
131    /// recognises: nulling all slots must leave the path item with no
132    /// operations. This cross-checks our hand-listed slots against openapiv3's
133    /// authoritative `PathItem::iter`, catching drift if a verb is ever added.
134    #[test]
135    fn operation_slots_cover_every_verb() {
136        let populated = || {
137            return Some(Operation::default());
138        };
139        let mut item = PathItem {
140            get: populated(),
141            put: populated(),
142            post: populated(),
143            delete: populated(),
144            options: populated(),
145            head: populated(),
146            patch: populated(),
147            trace: populated(),
148            ..Default::default()
149        };
150        assert_eq!(
151            item.iter().count(),
152            operation_slots(&mut item).len(),
153            "every populated verb should map to one slot"
154        );
155        for slot in operation_slots(&mut item) {
156            *slot = None;
157        }
158        assert_eq!(
159            item.iter().count(),
160            0,
161            "operation_slots must cover every verb reported by openapiv3's PathItem::iter",
162        );
163    }
164}