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::Object(obj)) => {
75 object_schema_to_type(schema, obj, parent_name, inline_types)
76 }
77 SchemaKind::Type(t) => primitive_type_to_rust(t, parent_name, inline_types),
78 SchemaKind::OneOf { one_of } => {
79 synthesize_inline_composition(parent_name, inline_types, |name, sink| {
80 super::compositions::generate_one_of(
81 name,
82 one_of,
83 schema.schema_data.discriminator.as_ref(),
84 schema.schema_data.description.as_ref(),
85 sink,
86 )
87 })
88 }
89 SchemaKind::AnyOf { any_of } => {
90 synthesize_inline_composition(parent_name, inline_types, |name, sink| {
91 super::compositions::generate_any_of(
92 name,
93 any_of,
94 schema.schema_data.description.as_ref(),
95 sink,
96 )
97 })
98 }
99 SchemaKind::AllOf { all_of } => {
100 synthesize_inline_composition(parent_name, inline_types, |name, sink| {
101 super::compositions::generate_all_of(
102 name,
103 all_of,
104 schema.schema_data.description.as_ref(),
105 sink,
106 )
107 })
108 }
109 SchemaKind::Not { .. } | SchemaKind::Any(_) => {
110 quote! { ::serde_json::Value }
112 }
113 }
114}
115
116fn synthesize_inline_composition(
120 parent_name: Option<&str>,
121 inline_types: &mut Vec<TokenStream>,
122 generate: impl FnOnce(&str, &mut Vec<TokenStream>) -> TokenStream,
123) -> TokenStream {
124 parent_name.map_or_else(
125 || quote! { ::serde_json::Value },
126 |name| {
127 let tokens = generate(name, inline_types);
128 inline_types.push(tokens);
129 let ident = format_ident!("{}", name.to_pascal_case());
130 quote! { #ident }
131 },
132 )
133}
134
135fn primitive_type_to_rust(
137 t: &Type,
138 parent_name: Option<&str>,
139 inline_types: &mut Vec<TokenStream>,
140) -> TokenStream {
141 match t {
142 Type::Integer(i) => {
143 if i.format == openapiv3::VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32) {
144 quote! { i32 }
145 } else {
146 quote! { i64 }
147 }
148 }
149 Type::Number(n) => {
150 if n.format == openapiv3::VariantOrUnknownOrEmpty::Item(NumberFormat::Float) {
151 quote! { f32 }
152 } else {
153 quote! { f64 }
154 }
155 }
156 Type::String(s) => string_type_to_rust(s),
157 Type::Boolean(_) => quote! { bool },
158 Type::Array(a) => {
159 let item_ty = a.items.as_ref().map_or_else(
160 || quote! { ::serde_json::Value },
161 |items| ref_or_to_inner_type_ctx(&items.clone().unbox(), parent_name, inline_types),
162 );
163 quote! { ::std::vec::Vec<#item_ty> }
164 }
165 Type::Object(_) => quote! { ::serde_json::Value },
168 }
169}
170
171fn string_type_to_rust(s: &StringType) -> TokenStream {
182 if !s.enumeration.is_empty() {
185 return quote! { ::std::string::String };
186 }
187 match &s.format {
188 VariantOrUnknownOrEmpty::Item(StringFormat::Binary) => quote! { ::std::vec::Vec<u8> },
189 VariantOrUnknownOrEmpty::Item(StringFormat::DateTime) => {
190 quote! { ::openapi_trait::chrono::DateTime<::openapi_trait::chrono::Utc> }
191 }
192 VariantOrUnknownOrEmpty::Item(StringFormat::Date) => {
193 quote! { ::openapi_trait::chrono::NaiveDate }
194 }
195 VariantOrUnknownOrEmpty::Unknown(name) if name == "uuid" => {
196 quote! { ::openapi_trait::uuid::Uuid }
197 }
198 _ => quote! { ::std::string::String },
199 }
200}
201
202fn object_schema_to_type(
212 schema: &Schema,
213 obj: &ObjectType,
214 parent_name: Option<&str>,
215 inline_types: &mut Vec<TokenStream>,
216) -> TokenStream {
217 if !obj.properties.is_empty() {
218 return synthesize_inline_composition(parent_name, inline_types, |name, sink| {
219 super::schemas::generate_object_struct(name, schema, obj, sink)
220 });
221 }
222 if let Some(ap) = &obj.additional_properties {
223 if let Some(value_ty) = additional_properties_value_type(ap, parent_name, inline_types) {
224 return quote! {
225 ::std::collections::HashMap<::std::string::String, #value_ty>
226 };
227 }
228 }
229 quote! { ::serde_json::Value }
230}
231
232#[must_use]
240pub fn additional_properties_value_type(
241 ap: &AdditionalProperties,
242 parent_name: Option<&str>,
243 inline_types: &mut Vec<TokenStream>,
244) -> Option<TokenStream> {
245 match ap {
246 AdditionalProperties::Any(false) => None,
247 AdditionalProperties::Any(true) => Some(quote! { ::serde_json::Value }),
248 AdditionalProperties::Schema(schema) => {
249 Some(ref_or_to_inner_type_ctx(schema, parent_name, inline_types))
250 }
251 }
252}
253
254#[must_use]
256pub fn is_string_enum(schema: &Schema) -> bool {
257 if let SchemaKind::Type(Type::String(s)) = &schema.schema_kind {
258 !s.enumeration.is_empty()
259 } else {
260 false
261 }
262}
263
264#[must_use]
266pub fn string_enum_values(schema: &Schema) -> Vec<String> {
267 if let SchemaKind::Type(Type::String(s)) = &schema.schema_kind {
268 s.enumeration.iter().filter_map(Clone::clone).collect()
269 } else {
270 vec![]
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 fn string_with_format(format: VariantOrUnknownOrEmpty<StringFormat>) -> StringType {
281 StringType {
282 format,
283 ..Default::default()
284 }
285 }
286
287 fn emitted(s: &StringType) -> String {
288 string_type_to_rust(s).to_string()
289 }
290
291 #[test]
292 fn date_time_maps_to_chrono_datetime() {
293 let s = string_with_format(VariantOrUnknownOrEmpty::Item(StringFormat::DateTime));
294 assert_eq!(
295 emitted(&s),
296 quote! { ::openapi_trait::chrono::DateTime<::openapi_trait::chrono::Utc> }.to_string()
297 );
298 }
299
300 #[test]
301 fn date_maps_to_chrono_naive_date() {
302 let s = string_with_format(VariantOrUnknownOrEmpty::Item(StringFormat::Date));
303 assert_eq!(
304 emitted(&s),
305 quote! { ::openapi_trait::chrono::NaiveDate }.to_string()
306 );
307 }
308
309 #[test]
310 fn uuid_unknown_format_maps_to_uuid() {
311 let s = string_with_format(VariantOrUnknownOrEmpty::Unknown("uuid".to_string()));
312 assert_eq!(
313 emitted(&s),
314 quote! { ::openapi_trait::uuid::Uuid }.to_string()
315 );
316 }
317
318 #[test]
319 fn binary_still_maps_to_byte_vec() {
320 let s = string_with_format(VariantOrUnknownOrEmpty::Item(StringFormat::Binary));
321 assert_eq!(emitted(&s), quote! { ::std::vec::Vec<u8> }.to_string());
322 }
323
324 #[test]
325 fn email_unknown_format_stays_string() {
326 let s = string_with_format(VariantOrUnknownOrEmpty::Unknown("email".to_string()));
327 assert_eq!(emitted(&s), quote! { ::std::string::String }.to_string());
328 }
329
330 #[test]
331 fn no_format_stays_string() {
332 let s = string_with_format(VariantOrUnknownOrEmpty::Empty);
333 assert_eq!(emitted(&s), quote! { ::std::string::String }.to_string());
334 }
335
336 #[test]
337 fn string_enum_stays_string_even_with_format() {
338 let s = StringType {
341 format: VariantOrUnknownOrEmpty::Unknown("uuid".to_string()),
342 enumeration: vec![Some("a".to_string()), Some("b".to_string())],
343 ..Default::default()
344 };
345 assert_eq!(emitted(&s), quote! { ::std::string::String }.to_string());
346 }
347}