openapi_nexus/generators/
response_headers.rs1use std::collections::{BTreeMap, HashSet};
8
9use crate::ir::types::{
10 IrEnumValueType, IrPrimitive, IrResponse, IrSchemaKind, IrSpec, IrTypeExpr,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ResponseHeaderValueKind {
15 String,
16 Integer,
17 Number,
18 Boolean,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ResponseHeaderPlan {
23 pub wire_name: String,
24 pub value_kind: ResponseHeaderValueKind,
25}
26
27pub fn unique_response_header_accessor_names(
33 headers: &[ResponseHeaderPlan],
34 preferred_name: impl FnMut(&str) -> String,
35) -> Vec<String> {
36 let mut used = HashSet::new();
37 unique_response_header_accessor_names_with_used(headers, preferred_name, &mut used)
38}
39
40pub fn unique_response_header_accessor_names_with_used(
42 headers: &[ResponseHeaderPlan],
43 mut preferred_name: impl FnMut(&str) -> String,
44 used: &mut HashSet<String>,
45) -> Vec<String> {
46 headers
47 .iter()
48 .map(|header| {
49 let preferred = preferred_name(&header.wire_name);
50 if used.insert(preferred.clone()) {
51 return preferred;
52 }
53 for index in 2..=u32::MAX {
54 let candidate = format!("{preferred}{index}");
55 if used.insert(candidate.clone()) {
56 return candidate;
57 }
58 }
59 unreachable!("response header accessor name space exhausted")
60 })
61 .collect()
62}
63
64pub fn collect_response_headers<'a>(
70 responses: impl IntoIterator<Item = &'a IrResponse>,
71 ir: &IrSpec,
72) -> Vec<ResponseHeaderPlan> {
73 let mut headers: BTreeMap<String, ResponseHeaderPlan> = BTreeMap::new();
74 for response in responses {
75 for (wire_name, header) in &response.headers {
76 let key = wire_name.to_ascii_lowercase();
77 let value_kind = response_header_value_kind(&header.type_expr, ir);
78 headers
79 .entry(key)
80 .and_modify(|existing| {
81 if existing.value_kind != value_kind {
82 existing.value_kind = ResponseHeaderValueKind::String;
83 }
84 })
85 .or_insert_with(|| ResponseHeaderPlan {
86 wire_name: wire_name.clone(),
87 value_kind,
88 });
89 }
90 }
91 headers.into_values().collect()
92}
93
94fn response_header_value_kind(expr: &IrTypeExpr, ir: &IrSpec) -> ResponseHeaderValueKind {
95 response_header_value_kind_inner(expr, ir, &mut HashSet::new())
96}
97
98fn response_header_value_kind_inner(
99 expr: &IrTypeExpr,
100 ir: &IrSpec,
101 seen: &mut HashSet<String>,
102) -> ResponseHeaderValueKind {
103 match expr {
104 IrTypeExpr::Primitive(IrPrimitive::Integer | IrPrimitive::IntegerWithFormat(_)) => {
105 ResponseHeaderValueKind::Integer
106 }
107 IrTypeExpr::Primitive(IrPrimitive::Number | IrPrimitive::NumberWithFormat(_)) => {
108 ResponseHeaderValueKind::Number
109 }
110 IrTypeExpr::Primitive(IrPrimitive::Boolean) => ResponseHeaderValueKind::Boolean,
111 IrTypeExpr::Primitive(_)
112 | IrTypeExpr::StringLiteral(_)
113 | IrTypeExpr::StringEnum(_)
114 | IrTypeExpr::Array(_)
115 | IrTypeExpr::Map(_)
116 | IrTypeExpr::Any => ResponseHeaderValueKind::String,
117 IrTypeExpr::Nullable(inner) => response_header_value_kind_inner(inner, ir, seen),
118 IrTypeExpr::Union(members) => {
119 let mut kinds = members
120 .iter()
121 .map(|member| response_header_value_kind_inner(member, ir, seen));
122 let Some(first) = kinds.next() else {
123 return ResponseHeaderValueKind::String;
124 };
125 if kinds.all(|kind| kind == first) {
126 first
127 } else {
128 ResponseHeaderValueKind::String
129 }
130 }
131 IrTypeExpr::Named(name) => {
132 if !seen.insert(name.clone()) {
133 return ResponseHeaderValueKind::String;
134 }
135 let kind = match ir.schemas.get(name).map(|schema| &schema.kind) {
136 Some(IrSchemaKind::Alias(inner)) => {
137 response_header_value_kind_inner(inner, ir, seen)
138 }
139 Some(IrSchemaKind::Enum(enumeration)) => match enumeration.value_type {
140 IrEnumValueType::Integer => ResponseHeaderValueKind::Integer,
141 IrEnumValueType::Number => ResponseHeaderValueKind::Number,
142 IrEnumValueType::String | IrEnumValueType::Mixed => {
143 ResponseHeaderValueKind::String
144 }
145 },
146 _ => ResponseHeaderValueKind::String,
147 };
148 seen.remove(name);
149 kind
150 }
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn accessor_names_are_unique_after_identifier_normalization() {
160 let headers = vec![
161 ResponseHeaderPlan {
162 wire_name: "X-Foo".to_string(),
163 value_kind: ResponseHeaderValueKind::String,
164 },
165 ResponseHeaderPlan {
166 wire_name: "X-Foo-Header2".to_string(),
167 value_kind: ResponseHeaderValueKind::String,
168 },
169 ResponseHeaderPlan {
170 wire_name: "X_Foo".to_string(),
171 value_kind: ResponseHeaderValueKind::String,
172 },
173 ];
174
175 let names = unique_response_header_accessor_names(&headers, |wire_name| {
176 format!("{}Header", wire_name.replace(['-', '_'], ""))
177 });
178
179 assert_eq!(names, ["XFooHeader", "XFooHeader2Header", "XFooHeader2"]);
180 }
181
182 #[test]
183 fn accessor_names_can_share_a_generated_module_namespace() {
184 let headers = vec![ResponseHeaderPlan {
185 wire_name: "Bar".to_string(),
186 value_kind: ResponseHeaderValueKind::String,
187 }];
188 let mut used = HashSet::new();
189
190 let first = unique_response_header_accessor_names_with_used(
191 &headers,
192 |_| "getGetFooBarHeader".to_string(),
193 &mut used,
194 );
195 let second = unique_response_header_accessor_names_with_used(
196 &headers,
197 |_| "getGetFooBarHeader".to_string(),
198 &mut used,
199 );
200
201 assert_eq!(first, ["getGetFooBarHeader"]);
202 assert_eq!(second, ["getGetFooBarHeader2"]);
203 }
204}