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 = 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
239fn keyword_safe_ident(name: &str) -> proc_macro2::Ident {
242 const KEYWORDS: &[&str] = &[
244 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
245 "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move",
246 "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
247 "true", "type", "union", "unsafe", "use", "where", "while", "yield",
248 ];
249 if KEYWORDS.contains(&name) {
250 proc_macro2::Ident::new_raw(name, proc_macro2::Span::call_site())
251 } else {
252 format_ident!("{}", name)
253 }
254}
255
256#[must_use]
258pub fn doc_attr(description: &Option<String>) -> TokenStream {
259 description
260 .as_ref()
261 .map_or_else(|| quote! {}, |d| quote! { #[doc = #d] })
262}