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 schema_to_rust_type_ctx(ref_or, required, None, &mut sink)
21 }
23
24#[must_use]
31pub fn schema_to_rust_type_ctx(
32 ref_or: &ReferenceOr<Schema>,
33 required: bool,
34 parent_name: Option<&str>,
35 inline_types: &mut Vec<TokenStream>,
36) -> TokenStream {
37 let inner = ref_or_to_inner_type_ctx(ref_or, parent_name, inline_types);
38 if required {
39 inner
40 } else {
41 quote! { ::core::option::Option<#inner> }
42 }
43}
44
45fn ref_or_to_inner_type_ctx(
48 ref_or: &ReferenceOr<Schema>,
49 parent_name: Option<&str>,
50 inline_types: &mut Vec<TokenStream>,
51) -> TokenStream {
52 match ref_or {
53 ReferenceOr::Reference { reference } => ref_to_ident(reference),
54 ReferenceOr::Item(schema) => schema_kind_to_type(schema, parent_name, inline_types),
55 }
56}
57
58#[must_use]
59pub fn ref_to_ident(reference: &str) -> TokenStream {
60 let name = reference.rsplit('/').next().unwrap_or(reference);
62 let ident = format_ident!("{}", name.to_pascal_case());
63 quote! { #ident }
64}
65
66fn schema_kind_to_type(
69 schema: &Schema,
70 parent_name: Option<&str>,
71 inline_types: &mut Vec<TokenStream>,
72) -> TokenStream {
73 match &schema.schema_kind {
74 SchemaKind::Type(Type::String(_)) if is_string_enum(schema) => {
75 synthesize_inline_string_enum(schema, parent_name, inline_types)
76 }
77 SchemaKind::Type(Type::Object(obj)) => {
78 object_schema_to_type(schema, obj, parent_name, inline_types)
79 }
80 SchemaKind::Type(t) => primitive_type_to_rust(t, parent_name, inline_types),
81 SchemaKind::OneOf { one_of } => {
82 synthesize_inline_composition(parent_name, inline_types, |name, sink| {
83 super::compositions::generate_one_of(
84 name,
85 one_of,
86 schema.schema_data.discriminator.as_ref(),
87 schema.schema_data.description.as_ref(),
88 sink,
89 )
90 })
91 }
92 SchemaKind::AnyOf { any_of } => {
93 synthesize_inline_composition(parent_name, inline_types, |name, sink| {
94 super::compositions::generate_any_of(
95 name,
96 any_of,
97 schema.schema_data.description.as_ref(),
98 sink,
99 )
100 })
101 }
102 SchemaKind::AllOf { all_of } => {
103 synthesize_inline_composition(parent_name, inline_types, |name, sink| {
104 super::compositions::generate_all_of(
105 name,
106 all_of,
107 schema.schema_data.description.as_ref(),
108 sink,
109 )
110 })
111 }
112 SchemaKind::Not { .. } | SchemaKind::Any(_) => {
113 quote! { ::serde_json::Value }
115 }
116 }
117}
118
119fn synthesize_inline_string_enum(
122 schema: &Schema,
123 parent_name: Option<&str>,
124 inline_types: &mut Vec<TokenStream>,
125) -> TokenStream {
126 parent_name.map_or_else(
127 || quote! { ::std::string::String },
128 |name| {
129 let tokens = super::schemas::generate_string_enum(name, schema);
130 inline_types.push(tokens);
131 let ident = format_ident!("{}", name.to_pascal_case());
132 quote! { #ident }
133 },
134 )
135}
136
137fn synthesize_inline_composition(
141 parent_name: Option<&str>,
142 inline_types: &mut Vec<TokenStream>,
143 generate: impl FnOnce(&str, &mut Vec<TokenStream>) -> TokenStream,
144) -> TokenStream {
145 parent_name.map_or_else(
146 || quote! { ::serde_json::Value },
147 |name| {
148 let tokens = generate(name, inline_types);
149 inline_types.push(tokens);
150 let ident = format_ident!("{}", name.to_pascal_case());
151 quote! { #ident }
152 },
153 )
154}
155
156fn primitive_type_to_rust(
158 t: &Type,
159 parent_name: Option<&str>,
160 inline_types: &mut Vec<TokenStream>,
161) -> TokenStream {
162 match t {
163 Type::Integer(i) => {
164 if i.format == openapiv3::VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32) {
165 quote! { i32 }
166 } else {
167 quote! { i64 }
168 }
169 }
170 Type::Number(n) => {
171 if n.format == openapiv3::VariantOrUnknownOrEmpty::Item(NumberFormat::Float) {
172 quote! { f32 }
173 } else {
174 quote! { f64 }
175 }
176 }
177 Type::String(s) => string_type_to_rust(s),
178 Type::Boolean(_) => quote! { bool },
179 Type::Array(a) => {
180 let item_ty = a.items.as_ref().map_or_else(
181 || quote! { ::serde_json::Value },
182 |items| ref_or_to_inner_type_ctx(&items.clone().unbox(), parent_name, inline_types),
183 );
184 quote! { ::std::vec::Vec<#item_ty> }
185 }
186 Type::Object(_) => quote! { ::serde_json::Value },
189 }
190}
191
192fn string_type_to_rust(s: &StringType) -> TokenStream {
203 if !s.enumeration.is_empty() {
206 return quote! { ::std::string::String };
207 }
208 match &s.format {
209 VariantOrUnknownOrEmpty::Item(StringFormat::Binary) => quote! { ::std::vec::Vec<u8> },
210 VariantOrUnknownOrEmpty::Item(StringFormat::DateTime) => {
211 quote! { ::openapi_trait::chrono::DateTime<::openapi_trait::chrono::Utc> }
212 }
213 VariantOrUnknownOrEmpty::Item(StringFormat::Date) => {
214 quote! { ::openapi_trait::chrono::NaiveDate }
215 }
216 VariantOrUnknownOrEmpty::Unknown(name) if name == "uuid" => {
217 quote! { ::openapi_trait::uuid::Uuid }
218 }
219 _ => quote! { ::std::string::String },
220 }
221}
222
223fn object_schema_to_type(
233 schema: &Schema,
234 obj: &ObjectType,
235 parent_name: Option<&str>,
236 inline_types: &mut Vec<TokenStream>,
237) -> TokenStream {
238 if !obj.properties.is_empty() {
239 return synthesize_inline_composition(parent_name, inline_types, |name, sink| {
240 super::schemas::generate_object_struct(name, schema, obj, sink)
241 });
242 }
243 if let Some(ap) = &obj.additional_properties {
244 if let Some(value_ty) = additional_properties_value_type(ap, parent_name, inline_types) {
245 return quote! {
246 ::std::collections::HashMap<::std::string::String, #value_ty>
247 };
248 }
249 }
250 quote! { ::serde_json::Value }
251}
252
253#[must_use]
261pub fn additional_properties_value_type(
262 ap: &AdditionalProperties,
263 parent_name: Option<&str>,
264 inline_types: &mut Vec<TokenStream>,
265) -> Option<TokenStream> {
266 match ap {
267 AdditionalProperties::Any(false) => None,
268 AdditionalProperties::Any(true) => Some(quote! { ::serde_json::Value }),
269 AdditionalProperties::Schema(schema) => {
270 Some(ref_or_to_inner_type_ctx(schema, parent_name, inline_types))
271 }
272 }
273}
274
275#[must_use]
277pub fn is_string_enum(schema: &Schema) -> bool {
278 if let SchemaKind::Type(Type::String(s)) = &schema.schema_kind {
279 !s.enumeration.is_empty()
280 } else {
281 false
282 }
283}
284
285#[must_use]
287pub fn string_enum_values(schema: &Schema) -> Vec<String> {
288 if let SchemaKind::Type(Type::String(s)) = &schema.schema_kind {
289 s.enumeration.iter().filter_map(Clone::clone).collect()
290 } else {
291 vec![]
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 fn string_with_format(format: VariantOrUnknownOrEmpty<StringFormat>) -> StringType {
302 StringType {
303 format,
304 ..Default::default()
305 }
306 }
307
308 fn emitted(s: &StringType) -> String {
309 string_type_to_rust(s).to_string()
310 }
311
312 #[test]
313 fn date_time_maps_to_chrono_datetime() {
314 let s = string_with_format(VariantOrUnknownOrEmpty::Item(StringFormat::DateTime));
315 assert_eq!(
316 emitted(&s),
317 quote! { ::openapi_trait::chrono::DateTime<::openapi_trait::chrono::Utc> }.to_string()
318 );
319 }
320
321 #[test]
322 fn date_maps_to_chrono_naive_date() {
323 let s = string_with_format(VariantOrUnknownOrEmpty::Item(StringFormat::Date));
324 assert_eq!(
325 emitted(&s),
326 quote! { ::openapi_trait::chrono::NaiveDate }.to_string()
327 );
328 }
329
330 #[test]
331 fn uuid_unknown_format_maps_to_uuid() {
332 let s = string_with_format(VariantOrUnknownOrEmpty::Unknown("uuid".to_string()));
333 assert_eq!(
334 emitted(&s),
335 quote! { ::openapi_trait::uuid::Uuid }.to_string()
336 );
337 }
338
339 #[test]
340 fn binary_still_maps_to_byte_vec() {
341 let s = string_with_format(VariantOrUnknownOrEmpty::Item(StringFormat::Binary));
342 assert_eq!(emitted(&s), quote! { ::std::vec::Vec<u8> }.to_string());
343 }
344
345 #[test]
346 fn email_unknown_format_stays_string() {
347 let s = string_with_format(VariantOrUnknownOrEmpty::Unknown("email".to_string()));
348 assert_eq!(emitted(&s), quote! { ::std::string::String }.to_string());
349 }
350
351 #[test]
352 fn no_format_stays_string() {
353 let s = string_with_format(VariantOrUnknownOrEmpty::Empty);
354 assert_eq!(emitted(&s), quote! { ::std::string::String }.to_string());
355 }
356
357 #[test]
358 fn string_enum_stays_string_even_with_format() {
359 let s = StringType {
362 format: VariantOrUnknownOrEmpty::Unknown("uuid".to_string()),
363 enumeration: vec![Some("a".to_string()), Some("b".to_string())],
364 ..Default::default()
365 };
366 assert_eq!(emitted(&s), quote! { ::std::string::String }.to_string());
367 }
368}