openapi_trait_shared/codegen/
schemas.rs1use heck::{ToPascalCase, ToSnakeCase};
2use openapiv3::{OpenAPI, ReferenceOr, Schema, SchemaKind, Type};
3use proc_macro2::TokenStream;
4use quote::{format_ident, quote};
5
6use super::compositions::{generate_all_of, generate_any_of, generate_one_of};
7use super::types::{
8 additional_properties_value_type, is_string_enum, ref_to_ident, schema_to_rust_type_ctx,
9 string_enum_values,
10};
11
12#[must_use]
18pub fn generate_schemas(openapi: &OpenAPI) -> TokenStream {
19 let Some(components) = &openapi.components else {
20 return quote! {};
21 };
22
23 #[cfg(feature = "validation")]
27 let models: std::collections::BTreeSet<String> =
28 super::validation::validatable_model_names(openapi);
29 #[cfg(not(feature = "validation"))]
30 let models: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
31
32 let mut inline_types: Vec<TokenStream> = Vec::new();
33 let items: Vec<TokenStream> = components
34 .schemas
35 .iter()
36 .map(|(name, ref_or)| generate_schema_item(name, ref_or, &mut inline_types, &models))
37 .collect();
38
39 quote! {
40 #(#items)*
41 #(#inline_types)*
42 }
43}
44
45fn generate_schema_item(
47 name: &str,
48 ref_or: &ReferenceOr<Schema>,
49 inline_types: &mut Vec<TokenStream>,
50 models: &std::collections::BTreeSet<String>,
51) -> TokenStream {
52 let schema = match ref_or {
53 ReferenceOr::Item(s) => s,
54 ReferenceOr::Reference { reference } => {
55 let ident = format_ident!("{}", name.to_pascal_case());
57 let target = ref_to_ident(reference);
58 return quote! { pub type #ident = #target; };
59 }
60 };
61
62 if is_string_enum(schema) {
63 return generate_string_enum(name, schema);
64 }
65
66 match &schema.schema_kind {
67 SchemaKind::OneOf { one_of } => generate_one_of(
68 name,
69 one_of,
70 schema.schema_data.discriminator.as_ref(),
71 schema.schema_data.description.as_ref(),
72 inline_types,
73 models,
74 ),
75 SchemaKind::AnyOf { any_of } => generate_any_of(
76 name,
77 any_of,
78 schema.schema_data.description.as_ref(),
79 inline_types,
80 models,
81 ),
82 SchemaKind::AllOf { all_of } => generate_all_of(
83 name,
84 all_of,
85 schema.schema_data.description.as_ref(),
86 inline_types,
87 models,
88 ),
89 SchemaKind::Type(Type::Object(obj)) => {
90 generate_object_struct(name, schema, obj, inline_types, models)
91 }
92 _ => {
93 let ident = format_ident!("{}", name.to_pascal_case());
95 let inner = schema_to_rust_type_ctx(ref_or, true, Some(name), inline_types, models);
96 let doc = doc_attr(&schema.schema_data.description);
97 quote! {
98 #doc
99 pub type #ident = #inner;
100 }
101 }
102 }
103}
104
105pub(crate) fn generate_string_enum(name: &str, schema: &Schema) -> TokenStream {
107 let ident = format_ident!("{}", name.to_pascal_case());
108 let doc = doc_attr(&schema.schema_data.description);
109 let variants = string_enum_values(schema)
110 .into_iter()
111 .map(|v| {
112 let variant_ident = format_ident!("{}", v.to_pascal_case());
113 if variant_ident == v {
114 quote! { #variant_ident }
115 } else {
116 let rename = &v;
117 quote! {
118 #[serde(rename = #rename)]
119 #variant_ident
120 }
121 }
122 })
123 .collect::<Vec<_>>();
124
125 let derives = model_derives();
126 quote! {
127 #doc
128 #derives
129
130 pub enum #ident {
131 #(#variants,)*
132 }
133 }
134}
135
136#[must_use]
143pub fn generate_object_struct(
144 name: &str,
145 schema: &Schema,
146 obj: &openapiv3::ObjectType,
147 inline_types: &mut Vec<TokenStream>,
148 models: &std::collections::BTreeSet<String>,
149) -> TokenStream {
150 let ident = format_ident!("{}", name.to_pascal_case());
151 let doc = doc_attr(&schema.schema_data.description);
152
153 if obj.properties.is_empty() {
156 if let Some(ap) = &obj.additional_properties {
157 if let Some(value_ty) =
158 additional_properties_value_type(ap, Some(name), inline_types, models)
159 {
160 return quote! {
161 #doc
162 pub type #ident =
163 ::std::collections::HashMap<::std::string::String, #value_ty>;
164 };
165 }
166 }
167 }
168
169 let fields: Vec<TokenStream> = obj
170 .properties
171 .iter()
172 .map(|(prop_name, prop_ref_or)| {
173 let is_required = obj.required.iter().any(|r| r == prop_name);
174 object_field_tokens(
175 prop_name,
176 &prop_ref_or.clone().unbox(),
177 is_required,
178 name,
179 inline_types,
180 models,
181 )
182 })
183 .collect();
184
185 let additional_field = obj.additional_properties.as_ref().and_then(|ap| {
188 let synth_name = format!("{name}AdditionalProperties");
189 additional_properties_value_type(ap, Some(&synth_name), inline_types, models).map(
190 |value_ty| {
191 quote! {
192 #[serde(flatten)]
193 pub additional_properties:
194 ::std::collections::HashMap<::std::string::String, #value_ty>,
195 }
196 },
197 )
198 });
199
200 let derives = model_derives();
201 quote! {
202 #doc
203 #derives
204
205 pub struct #ident {
206 #(#fields)*
207 #additional_field
208 }
209 }
210}
211
212#[must_use]
220pub fn object_field_tokens(
221 prop_name: &str,
222 prop_ref_or: &ReferenceOr<Schema>,
223 is_required: bool,
224 parent_struct_name: &str,
225 inline_types: &mut Vec<TokenStream>,
226 models: &std::collections::BTreeSet<String>,
227) -> TokenStream {
228 let snake = prop_name.to_snake_case();
229 let field_ident = super::idents::keyword_safe_ident(&snake);
230 let rename_attr = {
231 let n = prop_name;
232 quote! { #[serde(rename = #n)] }
233 };
234
235 let field_doc = match prop_ref_or {
236 ReferenceOr::Item(s) => doc_attr(&s.schema_data.description),
237 ReferenceOr::Reference { .. } => quote! {},
238 };
239
240 let synth_name = format!("{parent_struct_name}{}", prop_name.to_pascal_case());
241 let field_type = schema_to_rust_type_ctx(
242 prop_ref_or,
243 is_required,
244 Some(&synth_name),
245 inline_types,
246 models,
247 );
248
249 #[cfg(feature = "validation")]
253 let validate_attrs = super::validation::validation_attrs(prop_ref_or, models);
254 #[cfg(not(feature = "validation"))]
255 let validate_attrs = {
256 let _ = models;
259 quote! {}
260 };
261
262 quote! {
263 #field_doc
264 #rename_attr
265 #validate_attrs
266 pub #field_ident: #field_type,
267 }
268}
269
270#[must_use]
272pub fn doc_attr(description: &Option<String>) -> TokenStream {
273 description
274 .as_ref()
275 .map_or_else(|| quote! {}, |d| quote! { #[doc = #d] })
276}
277
278#[must_use]
287pub fn model_derives() -> TokenStream {
288 #[cfg(feature = "validation")]
289 let extra = quote! { , ::openapi_trait::serde_valid::Validate };
290 #[cfg(not(feature = "validation"))]
291 let extra = quote! {};
292
293 quote! {
294 #[derive(
295 ::core::fmt::Debug,
296 ::core::clone::Clone,
297 ::serde::Serialize,
298 ::serde::Deserialize
299 #extra
300 )]
301 }
302}