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
92fn 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 quote! {
113 #doc
114 #[derive(
115 ::core::fmt::Debug,
116 ::core::clone::Clone,
117 ::serde::Serialize,
118 ::serde::Deserialize,
119 )]
120
121 pub enum #ident {
122 #(#variants,)*
123 }
124 }
125}
126
127#[must_use]
134pub fn generate_object_struct(
135 name: &str,
136 schema: &Schema,
137 obj: &openapiv3::ObjectType,
138 inline_types: &mut Vec<TokenStream>,
139) -> TokenStream {
140 let ident = format_ident!("{}", name.to_pascal_case());
141 let doc = doc_attr(&schema.schema_data.description);
142
143 if obj.properties.is_empty() {
146 if let Some(ap) = &obj.additional_properties {
147 if let Some(value_ty) = additional_properties_value_type(ap, Some(name), inline_types) {
148 return quote! {
149 #doc
150 pub type #ident =
151 ::std::collections::HashMap<::std::string::String, #value_ty>;
152 };
153 }
154 }
155 }
156
157 let fields: Vec<TokenStream> = obj
158 .properties
159 .iter()
160 .map(|(prop_name, prop_ref_or)| {
161 let is_required = obj.required.iter().any(|r| r == prop_name);
162 object_field_tokens(
163 prop_name,
164 &prop_ref_or.clone().unbox(),
165 is_required,
166 name,
167 inline_types,
168 )
169 })
170 .collect();
171
172 let additional_field = obj.additional_properties.as_ref().and_then(|ap| {
175 let synth_name = format!("{name}AdditionalProperties");
176 additional_properties_value_type(ap, Some(&synth_name), inline_types).map(|value_ty| {
177 quote! {
178 #[serde(flatten)]
179 pub additional_properties:
180 ::std::collections::HashMap<::std::string::String, #value_ty>,
181 }
182 })
183 });
184
185 quote! {
186 #doc
187 #[derive(
188 ::core::fmt::Debug,
189 ::core::clone::Clone,
190 ::serde::Serialize,
191 ::serde::Deserialize,
192 )]
193
194 pub struct #ident {
195 #(#fields)*
196 #additional_field
197 }
198 }
199}
200
201#[must_use]
209pub fn object_field_tokens(
210 prop_name: &str,
211 prop_ref_or: &ReferenceOr<Schema>,
212 is_required: bool,
213 parent_struct_name: &str,
214 inline_types: &mut Vec<TokenStream>,
215) -> TokenStream {
216 let snake = prop_name.to_snake_case();
217 let field_ident = super::idents::keyword_safe_ident(&snake);
218 let rename_attr = {
219 let n = prop_name;
220 quote! { #[serde(rename = #n)] }
221 };
222
223 let field_doc = match prop_ref_or {
224 ReferenceOr::Item(s) => doc_attr(&s.schema_data.description),
225 ReferenceOr::Reference { .. } => quote! {},
226 };
227
228 let synth_name = format!("{parent_struct_name}{}", prop_name.to_pascal_case());
229 let field_type =
230 schema_to_rust_type_ctx(prop_ref_or, is_required, Some(&synth_name), inline_types);
231
232 quote! {
233 #field_doc
234 #rename_attr
235 pub #field_ident: #field_type,
236 }
237}
238
239#[must_use]
241pub fn doc_attr(description: &Option<String>) -> TokenStream {
242 description
243 .as_ref()
244 .map_or_else(|| quote! {}, |d| quote! { #[doc = #d] })
245}