Skip to main content

oapi_codegen/lower/
prune.rs

1//! Pruning component-schema models that no generated operation uses.
2//!
3//! This mirrors `oapi-codegen`'s default behaviour of pruning unused
4//! `#/components` entries before generation: only the schema models reachable
5//! from an operation survive. Roots are the types an operation references
6//! directly (path/query/header/cookie parameters, request and response bodies,
7//! and response headers). Reachability then follows each retained model's own
8//! type references to a fixpoint, so a chain `A -> B -> C` rooted at an
9//! operation keeps all three while a model referenced by nothing is dropped.
10//!
11//! Import-mapped (`External`) types are emitted into other modules and never
12//! appear as items here, so they are naturally excluded from the reachable set.
13//! Pruning is only applied when a server or client is generated (operations
14//! provide the roots). Models-only generation keeps every schema.
15
16use std::collections::BTreeSet;
17use std::collections::HashMap;
18
19use crate::ir::EnumKind;
20use crate::ir::Item;
21use crate::ir::Module;
22use crate::ir::RequestPayload;
23use crate::ir::ResponseBody;
24use crate::ir::RustType;
25use crate::ir::Service;
26use crate::ir::Struct;
27use crate::naming::Case;
28use crate::naming::to_ident;
29
30/// Drop every module item not reachable from `service`'s operations.
31pub fn prune_unused_models(module: &mut Module, service: &Service) {
32    let mut reachable: BTreeSet<String> = BTreeSet::new();
33    let mut worklist: Vec<String> = Vec::new();
34
35    for name in service_refs(service) {
36        if reachable.insert(name.clone()) {
37            worklist.push(name);
38        }
39    }
40
41    let index: HashMap<&str, &Item> = module.items.iter().map(|item| return (item.name(), item)).collect();
42
43    while let Some(name) = worklist.pop() {
44        let Some(item) = index.get(name.as_str()) else {
45            continue;
46        };
47        for referenced in item_refs(item) {
48            if reachable.insert(referenced.clone()) {
49                worklist.push(referenced);
50            }
51        }
52    }
53
54    module.items.retain(|item| return reachable.contains(item.name()));
55}
56
57/// The canonical reachability key for a named type: its `PascalCase` identifier,
58/// matching how [`Item::name`] and the emitter derive a type's Rust name.
59fn canonical(name: &str) -> String {
60    return to_ident(name, Case::Pascal).logical().to_owned();
61}
62
63/// The named type a `ty` references, if any (recursing through containers).
64///
65/// A `RustType` names at most one component model: containers (`Vec`, `Map`,
66/// `Option`) wrap a single inner type, and every other variant is either a
67/// leaf `Named` or carries no model reference.
68fn named_ref(ty: &RustType) -> Option<String> {
69    return match ty {
70        RustType::Named(name) => Some(canonical(name)),
71        RustType::Vec(inner)
72        | RustType::Map(inner)
73        | RustType::Option(inner)
74        | RustType::Nullable(inner)
75        | RustType::Boxed(inner) => named_ref(inner),
76        _ => None,
77    };
78}
79
80/// The named types a module item refers to (its fields/variants/alias).
81fn item_refs(item: &Item) -> Vec<String> {
82    return match item {
83        Item::Struct(s) => struct_refs(s),
84        Item::Enum(enumeration) => match &enumeration.kind {
85            EnumKind::Strings(_) | EnumKind::Integers { .. } => Vec::new(),
86            EnumKind::Union(variants) | EnumKind::AnyOf(variants) => variants
87                .iter()
88                .filter_map(|variant| return named_ref(&variant.ty))
89                .collect(),
90        },
91        Item::Alias(alias) => named_ref(&alias.ty).into_iter().collect(),
92    };
93}
94
95/// The named types a struct's fields and `additionalProperties` refer to.
96fn struct_refs(s: &Struct) -> Vec<String> {
97    return s
98        .fields
99        .iter()
100        .filter_map(|field| return named_ref(&field.ty))
101        .chain(s.additional_properties.as_ref().and_then(named_ref))
102        .collect();
103}
104
105/// Every named type the service's operations reference directly.
106fn service_refs(service: &Service) -> Vec<String> {
107    let mut refs = Vec::new();
108    for operation in &service.operations {
109        for param in &operation.path_params {
110            refs.extend(named_ref(&param.ty));
111        }
112        if let Some(query) = &operation.query {
113            refs.extend(struct_refs(query));
114        }
115        if let Some(headers) = &operation.headers {
116            for param in &headers.params {
117                refs.extend(named_ref(&param.ty));
118            }
119        }
120        if let Some(cookies) = &operation.cookies {
121            for param in &cookies.params {
122                refs.extend(named_ref(&param.ty));
123            }
124        }
125        if let Some(request) = &operation.request {
126            match request {
127                RequestPayload::Single(body) => refs.extend(named_ref(&body.ty)),
128                RequestPayload::Multipart(multipart) => {
129                    for field in &multipart.fields {
130                        refs.extend(named_ref(&field.ty));
131                    }
132                }
133                RequestPayload::Negotiated(negotiated) => {
134                    for variant in &negotiated.variants {
135                        refs.extend(named_ref(&variant.body.ty));
136                    }
137                }
138            }
139        }
140        for response in &operation.responses {
141            match &response.body {
142                Some(ResponseBody::Single(body)) => refs.extend(named_ref(&body.ty)),
143                Some(ResponseBody::Negotiated(negotiated)) => {
144                    for variant in &negotiated.variants {
145                        refs.extend(named_ref(&variant.body.ty));
146                    }
147                }
148                None => {}
149            }
150            for header in &response.headers {
151                refs.extend(named_ref(&header.ty));
152            }
153        }
154    }
155    return refs;
156}