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) | RustType::Map(inner) | RustType::Option(inner) | RustType::Boxed(inner) => {
72            named_ref(inner)
73        }
74        _ => None,
75    };
76}
77
78/// The named types a module item refers to (its fields/variants/alias).
79fn item_refs(item: &Item) -> Vec<String> {
80    return match item {
81        Item::Struct(s) => struct_refs(s),
82        Item::Enum(enumeration) => match &enumeration.kind {
83            EnumKind::Strings(_) | EnumKind::Integers { .. } => Vec::new(),
84            EnumKind::Union(variants) => variants
85                .iter()
86                .filter_map(|variant| return named_ref(&variant.ty))
87                .collect(),
88        },
89        Item::Alias(alias) => named_ref(&alias.ty).into_iter().collect(),
90    };
91}
92
93/// The named types a struct's fields and `additionalProperties` refer to.
94fn struct_refs(s: &Struct) -> Vec<String> {
95    return s
96        .fields
97        .iter()
98        .filter_map(|field| return named_ref(&field.ty))
99        .chain(s.additional_properties.as_ref().and_then(named_ref))
100        .collect();
101}
102
103/// Every named type the service's operations reference directly.
104fn service_refs(service: &Service) -> Vec<String> {
105    let mut refs = Vec::new();
106    for operation in &service.operations {
107        for param in &operation.path_params {
108            refs.extend(named_ref(&param.ty));
109        }
110        if let Some(query) = &operation.query {
111            refs.extend(struct_refs(query));
112        }
113        if let Some(headers) = &operation.headers {
114            for param in &headers.params {
115                refs.extend(named_ref(&param.ty));
116            }
117        }
118        if let Some(cookies) = &operation.cookies {
119            for param in &cookies.params {
120                refs.extend(named_ref(&param.ty));
121            }
122        }
123        if let Some(request) = &operation.request {
124            match request {
125                RequestPayload::Single(body) => refs.extend(named_ref(&body.ty)),
126                RequestPayload::Multipart(multipart) => {
127                    for field in &multipart.fields {
128                        refs.extend(named_ref(&field.ty));
129                    }
130                }
131                RequestPayload::Negotiated(negotiated) => {
132                    for variant in &negotiated.variants {
133                        refs.extend(named_ref(&variant.body.ty));
134                    }
135                }
136            }
137        }
138        for response in &operation.responses {
139            match &response.body {
140                Some(ResponseBody::Single(body)) => refs.extend(named_ref(&body.ty)),
141                Some(ResponseBody::Negotiated(negotiated)) => {
142                    for variant in &negotiated.variants {
143                        refs.extend(named_ref(&variant.body.ty));
144                    }
145                }
146                None => {}
147            }
148            for header in &response.headers {
149                refs.extend(named_ref(&header.ty));
150            }
151        }
152    }
153    return refs;
154}