1use std::collections::HashMap;
4
5use heck::{AsSnakeCase, ToUpperCamelCase};
6use openapiv3::{
7 IntegerFormat, NumberFormat, ReferenceOr, Schema, SchemaKind, Type, VariantOrUnknownOrEmpty,
8};
9use proc_macro2::TokenStream;
10use quote::{format_ident, quote, quote_spanned};
11use typify::{TypeSpace, TypeSpaceSettings};
12
13pub struct GeneratedType {
15 pub type_ref: TokenStream,
17 pub definitions: Vec<TokenStream>,
19}
20
21use crate::openapi::{Operation, OperationParam, ParamLocation, ParsedSpec, RequestBody};
22use crate::{Error, GeneratedTypeKind, Result, TypeOverride, TypeOverrides};
23
24pub struct TypeGenerator {
30 type_space: TypeSpace,
31 schema_to_type: HashMap<String, String>,
33}
34
35impl TypeGenerator {
36 pub fn new(spec: &ParsedSpec) -> Result<Self> {
38 Self::with_settings(spec, TypeSpaceSettings::default(), HashMap::new())
39 }
40
41 pub fn with_settings(
46 spec: &ParsedSpec,
47 settings: TypeSpaceSettings,
48 renames: HashMap<String, String>,
49 ) -> Result<Self> {
50 let mut type_space = TypeSpace::new(&settings);
51
52 let schema_names: Vec<String> = spec
54 .components
55 .as_ref()
56 .map(|c| c.schemas.keys().cloned().collect())
57 .unwrap_or_default();
58
59 if let Some(components) = &spec.components {
61 let schemas = components
62 .schemas
63 .iter()
64 .map(|(name, schema)| {
65 let schema = convert_to_schemars(schema)?;
66 Ok((name.clone(), schema))
67 })
68 .collect::<Result<Vec<_>>>()?;
69
70 type_space
71 .add_ref_types(schemas.into_iter())
72 .map_err(|e| Error::TypeGenError(e.to_string()))?;
73 }
74
75 for original_name in renames.keys() {
77 if !schema_names.contains(original_name) {
78 return Err(Error::UnknownSchema {
79 name: original_name.clone(),
80 available: schema_names.join(", "),
81 });
82 }
83 }
84
85 let generated_names: std::collections::HashSet<String> =
87 type_space.iter_types().map(|t| t.name()).collect();
88
89 let mut schema_to_type = HashMap::new();
90 for schema_name in &schema_names {
91 let expected_name = if let Some(renamed) = renames.get(schema_name) {
93 renamed.to_upper_camel_case()
94 } else {
95 schema_name.to_upper_camel_case()
96 };
97
98 if generated_names.contains(&expected_name) {
100 schema_to_type.insert(schema_name.clone(), expected_name);
101 } else {
102 return Err(Error::TypeGenError(format!(
104 "typify did not generate expected type '{}' for schema '{}'",
105 expected_name, schema_name
106 )));
107 }
108 }
109
110 Ok(Self {
111 type_space,
112 schema_to_type,
113 })
114 }
115
116 pub fn generate_all_types(&self) -> TokenStream {
118 self.type_space.to_stream()
119 }
120
121 pub fn get_type_name(&self, reference: &str) -> Option<String> {
124 let schema_name = reference.strip_prefix("#/components/schemas/")?;
126
127 self.schema_to_type.get(schema_name).cloned()
129 }
130
131 pub fn is_inline_schema(schema: &ReferenceOr<Schema>) -> bool {
133 matches!(schema, ReferenceOr::Item(_))
134 }
135
136 pub fn type_for_schema(&self, schema: &ReferenceOr<Schema>, name_hint: &str) -> TokenStream {
139 self.type_for_schema_with_definitions(schema, name_hint)
140 .type_ref
141 }
142
143 pub fn type_for_boxed_schema(
145 &self,
146 schema: &ReferenceOr<Box<Schema>>,
147 name_hint: &str,
148 ) -> TokenStream {
149 match schema {
150 ReferenceOr::Reference { reference } => {
151 if let Some(type_name) = self.get_type_name(reference) {
152 let ident = format_ident!("{}", type_name);
153 quote! { #ident }
154 } else {
155 quote! { serde_json::Value }
156 }
157 }
158 ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, name_hint).type_ref,
159 }
160 }
161
162 pub fn type_for_schema_with_definitions(
165 &self,
166 schema: &ReferenceOr<Schema>,
167 name_hint: &str,
168 ) -> GeneratedType {
169 match schema {
170 ReferenceOr::Reference { reference } => {
171 let type_ref = if let Some(type_name) = self.get_type_name(reference) {
172 let ident = format_ident!("{}", type_name);
173 quote! { #ident }
174 } else {
175 quote! { serde_json::Value }
176 };
177 GeneratedType {
178 type_ref,
179 definitions: vec![],
180 }
181 }
182 ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, name_hint),
183 }
184 }
185
186 fn type_for_inline_schema(&self, schema: &Schema, name_hint: &str) -> GeneratedType {
188 match &schema.schema_kind {
189 SchemaKind::Type(Type::String(_)) => GeneratedType {
190 type_ref: quote! { String },
191 definitions: vec![],
192 },
193 SchemaKind::Type(Type::Integer(int_type)) => {
194 let type_ref = match &int_type.format {
195 VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32) => quote! { i32 },
196 VariantOrUnknownOrEmpty::Item(IntegerFormat::Int64) => quote! { i64 },
197 _ => quote! { i64 },
198 };
199 GeneratedType {
200 type_ref,
201 definitions: vec![],
202 }
203 }
204 SchemaKind::Type(Type::Number(num_type)) => {
205 let type_ref = match &num_type.format {
206 VariantOrUnknownOrEmpty::Item(NumberFormat::Float) => quote! { f32 },
207 VariantOrUnknownOrEmpty::Item(NumberFormat::Double) => quote! { f64 },
208 _ => quote! { f64 },
209 };
210 GeneratedType {
211 type_ref,
212 definitions: vec![],
213 }
214 }
215 SchemaKind::Type(Type::Boolean(_)) => GeneratedType {
216 type_ref: quote! { bool },
217 definitions: vec![],
218 },
219 SchemaKind::Type(Type::Array(arr)) => {
220 if let Some(items) = &arr.items {
221 let inner = self.type_for_boxed_schema(items, &format!("{}Item", name_hint));
222 GeneratedType {
223 type_ref: quote! { Vec<#inner> },
224 definitions: vec![],
225 }
226 } else {
227 GeneratedType {
228 type_ref: quote! { Vec<serde_json::Value> },
229 definitions: vec![],
230 }
231 }
232 }
233 SchemaKind::Type(Type::Object(obj)) => {
234 self.generate_inline_struct(obj, &schema.schema_data, name_hint)
235 }
236 _ => GeneratedType {
237 type_ref: quote! { serde_json::Value },
238 definitions: vec![],
239 },
240 }
241 }
242
243 fn generate_inline_struct(
245 &self,
246 obj: &openapiv3::ObjectType,
247 _schema_data: &openapiv3::SchemaData,
248 name_hint: &str,
249 ) -> GeneratedType {
250 let struct_name = format_ident!("{}", name_hint.to_upper_camel_case());
251 let mut definitions = Vec::new();
252
253 let fields: Vec<_> = obj
255 .properties
256 .iter()
257 .map(|(prop_name, prop_schema)| {
258 let field_name = format_ident!("{}", AsSnakeCase(prop_name).to_string());
259 let is_required = obj.required.contains(prop_name);
260
261 let inner_hint = format!("{}{}", name_hint, prop_name.to_upper_camel_case());
263 let generated = match prop_schema {
264 ReferenceOr::Reference { reference } => {
265 let type_ref = if let Some(type_name) = self.get_type_name(reference) {
266 let ident = format_ident!("{}", type_name);
267 quote! { #ident }
268 } else {
269 quote! { serde_json::Value }
270 };
271 GeneratedType {
272 type_ref,
273 definitions: vec![],
274 }
275 }
276 ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, &inner_hint),
277 };
278
279 definitions.extend(generated.definitions);
281
282 let field_type = if is_required {
283 generated.type_ref
284 } else {
285 let inner = generated.type_ref;
286 quote! { Option<#inner> }
287 };
288
289 let snake_name = AsSnakeCase(prop_name).to_string();
291 if snake_name != *prop_name {
292 quote! {
293 #[serde(rename = #prop_name)]
294 pub #field_name: #field_type
295 }
296 } else {
297 quote! { pub #field_name: #field_type }
298 }
299 })
300 .collect();
301
302 let struct_def = quote! {
304 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
305 pub struct #struct_name {
306 #(#fields,)*
307 }
308 };
309
310 definitions.push(struct_def);
311
312 GeneratedType {
313 type_ref: quote! { #struct_name },
314 definitions,
315 }
316 }
317
318 pub fn path_param_type(&self, param: &OperationParam) -> TokenStream {
320 if let Some(schema) = ¶m.schema {
321 self.type_for_schema(schema, ¶m.name.to_upper_camel_case())
322 } else {
323 quote! { String }
324 }
325 }
326
327 pub fn query_param_type(&self, param: &OperationParam) -> TokenStream {
329 if let Some(schema) = ¶m.schema {
330 self.type_for_schema(schema, ¶m.name.to_upper_camel_case())
331 } else {
332 quote! { String }
333 }
334 }
335
336 pub fn request_body_type(&self, body: &RequestBody, op_name: &str) -> TokenStream {
338 if let Some(schema) = &body.schema {
339 self.type_for_schema(schema, &format!("{}Body", op_name.to_upper_camel_case()))
340 } else {
341 quote! { serde_json::Value }
342 }
343 }
344
345 pub fn response_type(
347 &self,
348 schema: &Option<ReferenceOr<Schema>>,
349 op_name: &str,
350 status: u16,
351 ) -> TokenStream {
352 if let Some(schema) = schema {
353 self.type_for_schema(
354 schema,
355 &format!("{}Response{}", op_name.to_upper_camel_case(), status),
356 )
357 } else {
358 quote! { () }
359 }
360 }
361
362 pub fn generate_query_struct(
364 &self,
365 op: &Operation,
366 overrides: &TypeOverrides,
367 ) -> Option<(syn::Ident, TokenStream)> {
368 if overrides.is_replaced(op.method, &op.path, GeneratedTypeKind::Query) {
370 return None;
371 }
372
373 let query_params: Vec<_> = op
374 .parameters
375 .iter()
376 .filter(|p| p.location == ParamLocation::Query)
377 .collect();
378
379 if query_params.is_empty() {
380 return None;
381 }
382
383 let default_name = format!(
384 "{}Query",
385 op.operation_id
386 .as_deref()
387 .unwrap_or(&op.path)
388 .to_upper_camel_case()
389 );
390
391 let struct_name = if let Some(TypeOverride::Rename { name, .. }) =
393 overrides.get(op.method, &op.path, GeneratedTypeKind::Query)
394 {
395 name.clone()
396 } else {
397 format_ident!("{}", default_name)
398 };
399
400 let fields = query_params.iter().map(|param| {
401 let name = format_ident!("{}", heck::AsSnakeCase(¶m.name).to_string());
402 let ty = self.query_param_type(param);
403
404 if param.required {
405 quote! { pub #name: #ty }
406 } else {
407 quote! { pub #name: Option<#ty> }
408 }
409 });
410
411 let definition = quote_spanned! { struct_name.span() =>
413 #[derive(Debug, Clone, serde::Deserialize)]
414 pub struct #struct_name {
415 #(#fields,)*
416 }
417 };
418
419 Some((struct_name, definition))
420 }
421
422 pub fn generate_path_type(&self, op: &Operation) -> TokenStream {
424 let path_params: Vec<_> = op
425 .parameters
426 .iter()
427 .filter(|p| p.location == ParamLocation::Path)
428 .collect();
429
430 if path_params.is_empty() {
431 return quote! { () };
432 }
433
434 if path_params.len() == 1 {
435 return self.path_param_type(path_params[0]);
436 }
437
438 let types = path_params.iter().map(|p| self.path_param_type(p));
439 quote! { (#(#types),*) }
440 }
441}
442
443fn convert_to_schemars(schema: &ReferenceOr<Schema>) -> Result<schemars::schema::Schema> {
445 serde_value::to_value(schema)
446 .map_err(|e| Error::TypeGenError(format!("failed to serialize schema: {}", e)))?
447 .deserialize_into::<schemars::schema::Schema>()
448 .map_err(|e| Error::TypeGenError(format!("failed to deserialize schema: {}", e)))
449}