openapi_trait_shared/codegen/
schemas.rs1use heck::{ToPascalCase, ToSnakeCase};
2use openapiv3::{Components, OpenAPI, ReferenceOr, Schema, SchemaKind, Type};
3use proc_macro2::TokenStream;
4use quote::{format_ident, quote};
5
6use super::types::{is_string_enum, ref_to_ident, schema_to_rust_type, string_enum_values};
7
8#[must_use]
10pub fn generate_schemas(openapi: &OpenAPI) -> TokenStream {
11 let Some(components) = &openapi.components else {
12 return quote! {};
13 };
14
15 let items: Vec<TokenStream> = components
16 .schemas
17 .iter()
18 .map(|(name, ref_or)| generate_schema_item(name, ref_or, components))
19 .collect();
20
21 quote! { #(#items)* }
22}
23
24fn generate_schema_item(
26 name: &str,
27 ref_or: &ReferenceOr<Schema>,
28 components: &Components,
29) -> TokenStream {
30 let schema = match ref_or {
31 ReferenceOr::Item(s) => s,
32 ReferenceOr::Reference { reference } => {
33 let ident = format_ident!("{}", name);
35 let target = ref_to_ident(reference);
36 return quote! { pub type #ident = #target; };
37 }
38 };
39
40 if is_string_enum(schema) {
41 generate_string_enum(name, schema)
42 } else if let SchemaKind::Type(Type::Object(obj)) = &schema.schema_kind {
43 generate_object_struct(name, schema, obj, components)
44 } else {
45 let ident = format_ident!("{}", name);
47 let inner = schema_to_rust_type(ref_or, true);
48 let doc = doc_attr(&schema.schema_data.description);
49 quote! {
50 #doc
51 pub type #ident = #inner;
52 }
53 }
54}
55
56fn generate_string_enum(name: &str, schema: &Schema) -> TokenStream {
58 let ident = format_ident!("{}", name);
59 let doc = doc_attr(&schema.schema_data.description);
60 let variants = string_enum_values(schema)
61 .into_iter()
62 .map(|v| {
63 let variant_ident = format_ident!("{}", v.to_pascal_case());
64 if variant_ident == v {
65 quote! { #variant_ident }
66 } else {
67 let rename = &v;
68 quote! {
69 #[serde(rename = #rename)]
70 #variant_ident
71 }
72 }
73 })
74 .collect::<Vec<_>>();
75
76 quote! {
77 #doc
78 #[derive(
79 ::core::fmt::Debug,
80 ::core::clone::Clone,
81 ::serde::Serialize,
82 ::serde::Deserialize,
83 )]
84
85 pub enum #ident {
86 #(#variants,)*
87 }
88 }
89}
90
91fn generate_object_struct(
93 name: &str,
94 schema: &Schema,
95 obj: &openapiv3::ObjectType,
96 _components: &Components,
97) -> TokenStream {
98 let ident = format_ident!("{}", name);
99 let doc = doc_attr(&schema.schema_data.description);
100
101 let fields: Vec<TokenStream> = obj
102 .properties
103 .iter()
104 .map(|(prop_name, prop_ref_or)| {
105 let is_required = obj.required.iter().any(|r| r == prop_name);
107
108 let snake = prop_name.to_snake_case();
109 let field_ident = keyword_safe_ident(&snake);
111 let rename_attr = {
113 let n = prop_name.as_str();
114 quote! { #[serde(rename = #n)] }
115 };
116
117 let field_doc = match prop_ref_or {
119 ReferenceOr::Item(s) => doc_attr(&s.schema_data.description),
120 ReferenceOr::Reference { .. } => quote! {},
121 };
122
123 let field_type = schema_to_rust_type(&prop_ref_or.clone().unbox(), is_required);
124
125 quote! {
126 #field_doc
127 #rename_attr
128 pub #field_ident: #field_type,
129 }
130 })
131 .collect();
132
133 quote! {
134 #doc
135 #[derive(
136 ::core::fmt::Debug,
137 ::core::clone::Clone,
138 ::serde::Serialize,
139 ::serde::Deserialize,
140 )]
141
142 pub struct #ident {
143 #(#fields)*
144 }
145 }
146}
147
148fn keyword_safe_ident(name: &str) -> proc_macro2::Ident {
151 const KEYWORDS: &[&str] = &[
153 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
154 "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move",
155 "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
156 "true", "type", "union", "unsafe", "use", "where", "while", "yield",
157 ];
158 if KEYWORDS.contains(&name) {
159 proc_macro2::Ident::new_raw(name, proc_macro2::Span::call_site())
160 } else {
161 format_ident!("{}", name)
162 }
163}
164
165#[must_use]
167pub fn doc_attr(description: &Option<String>) -> TokenStream {
168 description
169 .as_ref()
170 .map_or_else(|| quote! {}, |d| quote! { #[doc = #d] })
171}