openapi_trait_shared/codegen/
types.rs1use heck::ToPascalCase;
2use openapiv3::{
3 AdditionalProperties, IntegerFormat, NumberFormat, ObjectType, ReferenceOr, Schema, SchemaKind,
4 StringFormat, StringType, Type, VariantOrUnknownOrEmpty,
5};
6use proc_macro2::TokenStream;
7use quote::{format_ident, quote};
8
9#[must_use]
18pub fn schema_to_rust_type(ref_or: &ReferenceOr<Schema>, required: bool) -> TokenStream {
19 let mut sink: Vec<TokenStream> = Vec::new();
20 let models = std::collections::BTreeSet::new();
24 schema_to_rust_type_ctx(ref_or, required, None, &mut sink, &models)
25 }
27
28#[must_use]
35pub fn schema_to_rust_type_ctx(
36 ref_or: &ReferenceOr<Schema>,
37 required: bool,
38 parent_name: Option<&str>,
39 inline_types: &mut Vec<TokenStream>,
40 models: &std::collections::BTreeSet<String>,
41) -> TokenStream {
42 let inner = ref_or_to_inner_type_ctx(ref_or, parent_name, inline_types, models);
43 if required {
44 inner
45 } else {
46 quote! { ::core::option::Option<#inner> }
47 }
48}
49
50fn ref_or_to_inner_type_ctx(
53 ref_or: &ReferenceOr<Schema>,
54 parent_name: Option<&str>,
55 inline_types: &mut Vec<TokenStream>,
56 models: &std::collections::BTreeSet<String>,
57) -> TokenStream {
58 match ref_or {
59 ReferenceOr::Reference { reference } => ref_to_ident(reference),
60 ReferenceOr::Item(schema) => schema_kind_to_type(schema, parent_name, inline_types, models),
61 }
62}
63
64#[must_use]
65pub fn ref_to_ident(reference: &str) -> TokenStream {
66 let name = reference.rsplit('/').next().unwrap_or(reference);
68 let ident = format_ident!("{}", name.to_pascal_case());
69 quote! { #ident }
70}
71
72fn schema_kind_to_type(
75 schema: &Schema,
76 parent_name: Option<&str>,
77 inline_types: &mut Vec<TokenStream>,
78 models: &std::collections::BTreeSet<String>,
79) -> TokenStream {
80 match &schema.schema_kind {
81 SchemaKind::Type(Type::String(_)) if is_string_enum(schema) => {
82 synthesize_inline_string_enum(schema, parent_name, inline_types)
83 }
84 SchemaKind::Type(Type::Object(obj)) => {
85 object_schema_to_type(schema, obj, parent_name, inline_types, models)
86 }
87 SchemaKind::Type(t) => primitive_type_to_rust(t, parent_name, inline_types, models),
88 SchemaKind::OneOf { one_of } => {
89 synthesize_inline_composition(parent_name, inline_types, |name, sink| {
90 super::compositions::generate_one_of(
91 name,
92 one_of,
93 schema.schema_data.discriminator.as_ref(),
94 schema.schema_data.description.as_ref(),
95 sink,
96 models,
97 )
98 })
99 }
100 SchemaKind::AnyOf { any_of } => {
101 synthesize_inline_composition(parent_name, inline_types, |name, sink| {
102 super::compositions::generate_any_of(
103 name,
104 any_of,
105 schema.schema_data.description.as_ref(),
106 sink,
107 models,
108 )
109 })
110 }
111 SchemaKind::AllOf { all_of } => {
112 synthesize_inline_composition(parent_name, inline_types, |name, sink| {
113 super::compositions::generate_all_of(
114 name,
115 all_of,
116 schema.schema_data.description.as_ref(),
117 sink,
118 models,
119 )
120 })
121 }
122 SchemaKind::Not { .. } | SchemaKind::Any(_) => {
123 quote! { ::serde_json::Value }
125 }
126 }
127}
128
129fn synthesize_inline_string_enum(
132 schema: &Schema,
133 parent_name: Option<&str>,
134 inline_types: &mut Vec<TokenStream>,
135) -> TokenStream {
136 parent_name.map_or_else(
137 || quote! { ::std::string::String },
138 |name| {
139 let tokens = super::schemas::generate_string_enum(name, schema);
140 inline_types.push(tokens);
141 let ident = format_ident!("{}", name.to_pascal_case());
142 quote! { #ident }
143 },
144 )
145}
146
147fn synthesize_inline_composition(
151 parent_name: Option<&str>,
152 inline_types: &mut Vec<TokenStream>,
153 generate: impl FnOnce(&str, &mut Vec<TokenStream>) -> TokenStream,
154) -> TokenStream {
155 parent_name.map_or_else(
156 || quote! { ::serde_json::Value },
157 |name| {
158 let tokens = generate(name, inline_types);
159 inline_types.push(tokens);
160 let ident = format_ident!("{}", name.to_pascal_case());
161 quote! { #ident }
162 },
163 )
164}
165
166fn primitive_type_to_rust(
168 t: &Type,
169 parent_name: Option<&str>,
170 inline_types: &mut Vec<TokenStream>,
171 models: &std::collections::BTreeSet<String>,
172) -> TokenStream {
173 match t {
174 Type::Integer(i) => {
175 if i.format == openapiv3::VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32) {
176 quote! { i32 }
177 } else {
178 quote! { i64 }
179 }
180 }
181 Type::Number(n) => {
182 if n.format == openapiv3::VariantOrUnknownOrEmpty::Item(NumberFormat::Float) {
183 quote! { f32 }
184 } else {
185 quote! { f64 }
186 }
187 }
188 Type::String(s) => string_type_to_rust(s),
189 Type::Boolean(_) => quote! { bool },
190 Type::Array(a) => {
191 let item_ty = a.items.as_ref().map_or_else(
192 || quote! { ::serde_json::Value },
193 |items| {
194 ref_or_to_inner_type_ctx(
195 &items.clone().unbox(),
196 parent_name,
197 inline_types,
198 models,
199 )
200 },
201 );
202 quote! { ::std::vec::Vec<#item_ty> }
203 }
204 Type::Object(_) => quote! { ::serde_json::Value },
207 }
208}
209
210fn string_type_to_rust(s: &StringType) -> TokenStream {
221 if !s.enumeration.is_empty() {
224 return quote! { ::std::string::String };
225 }
226 match &s.format {
227 VariantOrUnknownOrEmpty::Item(StringFormat::Binary) => quote! { ::std::vec::Vec<u8> },
228 VariantOrUnknownOrEmpty::Item(StringFormat::DateTime) => {
229 quote! { ::openapi_trait::chrono::DateTime<::openapi_trait::chrono::Utc> }
230 }
231 VariantOrUnknownOrEmpty::Item(StringFormat::Date) => {
232 quote! { ::openapi_trait::chrono::NaiveDate }
233 }
234 VariantOrUnknownOrEmpty::Unknown(name) if name == "uuid" => {
235 quote! { ::openapi_trait::uuid::Uuid }
236 }
237 _ => quote! { ::std::string::String },
238 }
239}
240
241fn object_schema_to_type(
251 schema: &Schema,
252 obj: &ObjectType,
253 parent_name: Option<&str>,
254 inline_types: &mut Vec<TokenStream>,
255 models: &std::collections::BTreeSet<String>,
256) -> TokenStream {
257 if !obj.properties.is_empty() {
258 return synthesize_inline_composition(parent_name, inline_types, |name, sink| {
259 super::schemas::generate_object_struct(name, schema, obj, sink, models)
260 });
261 }
262 if let Some(ap) = &obj.additional_properties {
263 if let Some(value_ty) =
264 additional_properties_value_type(ap, parent_name, inline_types, models)
265 {
266 return quote! {
267 ::std::collections::HashMap<::std::string::String, #value_ty>
268 };
269 }
270 }
271 quote! { ::serde_json::Value }
272}
273
274#[must_use]
282pub fn additional_properties_value_type(
283 ap: &AdditionalProperties,
284 parent_name: Option<&str>,
285 inline_types: &mut Vec<TokenStream>,
286 models: &std::collections::BTreeSet<String>,
287) -> Option<TokenStream> {
288 match ap {
289 AdditionalProperties::Any(false) => None,
290 AdditionalProperties::Any(true) => Some(quote! { ::serde_json::Value }),
291 AdditionalProperties::Schema(schema) => Some(ref_or_to_inner_type_ctx(
292 schema,
293 parent_name,
294 inline_types,
295 models,
296 )),
297 }
298}
299
300#[must_use]
302pub fn is_string_enum(schema: &Schema) -> bool {
303 if let SchemaKind::Type(Type::String(s)) = &schema.schema_kind {
304 !s.enumeration.is_empty()
305 } else {
306 false
307 }
308}
309
310#[must_use]
312pub fn string_enum_values(schema: &Schema) -> Vec<String> {
313 if let SchemaKind::Type(Type::String(s)) = &schema.schema_kind {
314 s.enumeration.iter().filter_map(Clone::clone).collect()
315 } else {
316 vec![]
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 fn string_with_format(format: VariantOrUnknownOrEmpty<StringFormat>) -> StringType {
327 StringType {
328 format,
329 ..Default::default()
330 }
331 }
332
333 fn emitted(s: &StringType) -> String {
334 string_type_to_rust(s).to_string()
335 }
336
337 #[test]
338 fn date_time_maps_to_chrono_datetime() {
339 let s = string_with_format(VariantOrUnknownOrEmpty::Item(StringFormat::DateTime));
340 assert_eq!(
341 emitted(&s),
342 quote! { ::openapi_trait::chrono::DateTime<::openapi_trait::chrono::Utc> }.to_string()
343 );
344 }
345
346 #[test]
347 fn date_maps_to_chrono_naive_date() {
348 let s = string_with_format(VariantOrUnknownOrEmpty::Item(StringFormat::Date));
349 assert_eq!(
350 emitted(&s),
351 quote! { ::openapi_trait::chrono::NaiveDate }.to_string()
352 );
353 }
354
355 #[test]
356 fn uuid_unknown_format_maps_to_uuid() {
357 let s = string_with_format(VariantOrUnknownOrEmpty::Unknown("uuid".to_string()));
358 assert_eq!(
359 emitted(&s),
360 quote! { ::openapi_trait::uuid::Uuid }.to_string()
361 );
362 }
363
364 #[test]
365 fn binary_still_maps_to_byte_vec() {
366 let s = string_with_format(VariantOrUnknownOrEmpty::Item(StringFormat::Binary));
367 assert_eq!(emitted(&s), quote! { ::std::vec::Vec<u8> }.to_string());
368 }
369
370 #[test]
371 fn email_unknown_format_stays_string() {
372 let s = string_with_format(VariantOrUnknownOrEmpty::Unknown("email".to_string()));
373 assert_eq!(emitted(&s), quote! { ::std::string::String }.to_string());
374 }
375
376 #[test]
377 fn no_format_stays_string() {
378 let s = string_with_format(VariantOrUnknownOrEmpty::Empty);
379 assert_eq!(emitted(&s), quote! { ::std::string::String }.to_string());
380 }
381
382 #[test]
383 fn string_enum_stays_string_even_with_format() {
384 let s = StringType {
387 format: VariantOrUnknownOrEmpty::Unknown("uuid".to_string()),
388 enumeration: vec![Some("a".to_string()), Some("b".to_string())],
389 ..Default::default()
390 };
391 assert_eq!(emitted(&s), quote! { ::std::string::String }.to_string());
392 }
393}