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 let mut inline_types: Vec<TokenStream> = Vec::new();
24 let items: Vec<TokenStream> = components
25 .schemas
26 .iter()
27 .map(|(name, ref_or)| generate_schema_item(name, ref_or, &mut inline_types))
28 .collect();
29
30 quote! {
31 #(#items)*
32 #(#inline_types)*
33 }
34}
35
36fn generate_schema_item(
38 name: &str,
39 ref_or: &ReferenceOr<Schema>,
40 inline_types: &mut Vec<TokenStream>,
41) -> TokenStream {
42 let schema = match ref_or {
43 ReferenceOr::Item(s) => s,
44 ReferenceOr::Reference { reference } => {
45 let ident = format_ident!("{}", name.to_pascal_case());
47 let target = ref_to_ident(reference);
48 return quote! { pub type #ident = #target; };
49 }
50 };
51
52 if is_string_enum(schema) {
53 return generate_string_enum(name, schema);
54 }
55
56 match &schema.schema_kind {
57 SchemaKind::OneOf { one_of } => generate_one_of(
58 name,
59 one_of,
60 schema.schema_data.discriminator.as_ref(),
61 schema.schema_data.description.as_ref(),
62 inline_types,
63 ),
64 SchemaKind::AnyOf { any_of } => generate_any_of(
65 name,
66 any_of,
67 schema.schema_data.description.as_ref(),
68 inline_types,
69 ),
70 SchemaKind::AllOf { all_of } => generate_all_of(
71 name,
72 all_of,
73 schema.schema_data.description.as_ref(),
74 inline_types,
75 ),
76 SchemaKind::Type(Type::Object(obj)) => {
77 generate_object_struct(name, schema, obj, inline_types)
78 }
79 _ => {
80 let ident = format_ident!("{}", name.to_pascal_case());
82 let inner = schema_to_rust_type_ctx(ref_or, true, Some(name), inline_types);
83 let doc = doc_attr(&schema.schema_data.description);
84 quote! {
85 #doc
86 pub type #ident = #inner;
87 }
88 }
89 }
90}
91
92pub(crate) fn generate_string_enum(name: &str, schema: &Schema) -> TokenStream {
94 let ident = format_ident!("{}", name.to_pascal_case());
95 let doc = doc_attr(&schema.schema_data.description);
96 let variants = string_enum_values(schema)
97 .into_iter()
98 .map(|v| {
99 let variant_ident = format_ident!("{}", v.to_pascal_case());
100 if variant_ident == v {
101 quote! { #variant_ident }
102 } else {
103 let rename = &v;
104 quote! {
105 #[serde(rename = #rename)]
106 #variant_ident
107 }
108 }
109 })
110 .collect::<Vec<_>>();
111
112 let derives = model_derives();
113 quote! {
114 #doc
115 #derives
116
117 pub enum #ident {
118 #(#variants,)*
119 }
120 }
121}
122
123#[must_use]
130pub fn generate_object_struct(
131 name: &str,
132 schema: &Schema,
133 obj: &openapiv3::ObjectType,
134 inline_types: &mut Vec<TokenStream>,
135) -> TokenStream {
136 let ident = format_ident!("{}", name.to_pascal_case());
137 let doc = doc_attr(&schema.schema_data.description);
138
139 if obj.properties.is_empty() {
142 if let Some(ap) = &obj.additional_properties {
143 if let Some(value_ty) = additional_properties_value_type(ap, Some(name), inline_types) {
144 return quote! {
145 #doc
146 pub type #ident =
147 ::std::collections::HashMap<::std::string::String, #value_ty>;
148 };
149 }
150 }
151 }
152
153 let fields: Vec<TokenStream> = obj
154 .properties
155 .iter()
156 .map(|(prop_name, prop_ref_or)| {
157 let is_required = obj.required.iter().any(|r| r == prop_name);
158 object_field_tokens(
159 prop_name,
160 &prop_ref_or.clone().unbox(),
161 is_required,
162 name,
163 inline_types,
164 )
165 })
166 .collect();
167
168 let additional_field = obj.additional_properties.as_ref().and_then(|ap| {
171 let synth_name = format!("{name}AdditionalProperties");
172 additional_properties_value_type(ap, Some(&synth_name), inline_types).map(|value_ty| {
173 quote! {
174 #[serde(flatten)]
175 pub additional_properties:
176 ::std::collections::HashMap<::std::string::String, #value_ty>,
177 }
178 })
179 });
180
181 let derives = model_derives();
182 quote! {
183 #doc
184 #derives
185
186 pub struct #ident {
187 #(#fields)*
188 #additional_field
189 }
190 }
191}
192
193#[must_use]
201pub fn object_field_tokens(
202 prop_name: &str,
203 prop_ref_or: &ReferenceOr<Schema>,
204 is_required: bool,
205 parent_struct_name: &str,
206 inline_types: &mut Vec<TokenStream>,
207) -> TokenStream {
208 let snake = prop_name.to_snake_case();
209 let field_ident = super::idents::keyword_safe_ident(&snake);
210 let rename_attr = {
211 let n = prop_name;
212 quote! { #[serde(rename = #n)] }
213 };
214
215 let field_doc = match prop_ref_or {
216 ReferenceOr::Item(s) => doc_attr(&s.schema_data.description),
217 ReferenceOr::Reference { .. } => quote! {},
218 };
219
220 let synth_name = format!("{parent_struct_name}{}", prop_name.to_pascal_case());
221 let field_type =
222 schema_to_rust_type_ctx(prop_ref_or, is_required, Some(&synth_name), inline_types);
223
224 #[cfg(feature = "validation")]
228 let validate_attrs = super::validation::validation_attrs(prop_ref_or);
229 #[cfg(not(feature = "validation"))]
230 let validate_attrs = quote! {};
231
232 quote! {
233 #field_doc
234 #rename_attr
235 #validate_attrs
236 pub #field_ident: #field_type,
237 }
238}
239
240#[must_use]
242pub fn doc_attr(description: &Option<String>) -> TokenStream {
243 description
244 .as_ref()
245 .map_or_else(|| quote! {}, |d| quote! { #[doc = #d] })
246}
247
248#[must_use]
257pub fn model_derives() -> TokenStream {
258 #[cfg(feature = "validation")]
259 let extra = quote! { , ::openapi_trait::serde_valid::Validate };
260 #[cfg(not(feature = "validation"))]
261 let extra = quote! {};
262
263 quote! {
264 #[derive(
265 ::core::fmt::Debug,
266 ::core::clone::Clone,
267 ::serde::Serialize,
268 ::serde::Deserialize
269 #extra
270 )]
271 }
272}