1use std::collections::HashMap;
4
5use heck::{AsSnakeCase, ToUpperCamelCase};
6use openapiv3::{ReferenceOr, Schema, SchemaKind, Type};
7use proc_macro2::TokenStream;
8use quote::{format_ident, quote, quote_spanned};
9use typify::{TypeId, TypeSpace, TypeSpaceSettings};
10
11pub struct GeneratedType {
13 pub type_ref: TokenStream,
15 pub definitions: Vec<TokenStream>,
17}
18
19impl GeneratedType {
20 pub fn simple(type_ref: TokenStream) -> Self {
22 Self {
23 type_ref,
24 definitions: vec![],
25 }
26 }
27}
28
29use crate::openapi::{Operation, OperationParam, ParamLocation, ParsedSpec, RequestBody};
30use crate::{Error, GeneratedTypeKind, Result, TypeOverride, TypeOverrides};
31
32pub struct TypeGenerator {
38 type_space: TypeSpace,
39 schema_to_type: HashMap<String, String>,
41 inline_types: HashMap<String, TypeId>,
43}
44
45impl TypeGenerator {
46 pub fn new(spec: &ParsedSpec) -> Result<Self> {
48 Self::with_settings(spec, TypeSpaceSettings::default(), HashMap::new())
49 }
50
51 pub fn with_settings(
56 spec: &ParsedSpec,
57 settings: TypeSpaceSettings,
58 renames: HashMap<String, String>,
59 ) -> Result<Self> {
60 let mut type_space = TypeSpace::new(&settings);
61
62 let schema_names: Vec<String> = spec
64 .components
65 .as_ref()
66 .map(|c| c.schemas.keys().cloned().collect())
67 .unwrap_or_default();
68
69 if let Some(components) = &spec.components {
71 let schemas = components
72 .schemas
73 .iter()
74 .map(|(name, schema)| {
75 let schema = to_schemars(schema)?;
76 Ok((name.clone(), schema))
77 })
78 .collect::<Result<Vec<_>>>()?;
79
80 type_space
81 .add_ref_types(schemas.into_iter())
82 .map_err(|e| Error::TypeGenError(e.to_string()))?;
83 }
84
85 for original_name in renames.keys() {
87 if !schema_names.contains(original_name) {
88 return Err(Error::UnknownSchema {
89 name: original_name.clone(),
90 available: schema_names.join(", "),
91 });
92 }
93 }
94
95 let generated_names: std::collections::HashSet<String> =
97 type_space.iter_types().map(|t| t.name()).collect();
98
99 let mut schema_to_type = HashMap::new();
100 for schema_name in &schema_names {
101 let expected_name = if let Some(renamed) = renames.get(schema_name) {
103 renamed.to_upper_camel_case()
104 } else {
105 schema_name.to_upper_camel_case()
106 };
107
108 if generated_names.contains(&expected_name) {
110 schema_to_type.insert(schema_name.clone(), expected_name);
111 } else {
112 return Err(Error::TypeGenError(format!(
114 "typify did not generate expected type '{}' for schema '{}'",
115 expected_name, schema_name
116 )));
117 }
118 }
119
120 let mut inline_types = HashMap::new();
122 for op in spec.operations() {
123 let op_name = op.raw_name();
124
125 for param in &op.parameters {
127 if let Some(ReferenceOr::Item(schema)) = ¶m.schema {
128 let name_hint = param.name.to_upper_camel_case();
129 register_inline_schema(&mut type_space, &mut inline_types, schema, name_hint);
130 }
131 }
132
133 if let Some(body) = &op.request_body
135 && let Some(ReferenceOr::Item(schema)) = &body.schema
136 {
137 let name_hint = format!("{}Body", op_name.to_upper_camel_case());
138 register_inline_schema(&mut type_space, &mut inline_types, schema, name_hint);
139 }
140
141 for resp in &op.responses {
143 if let Some(ReferenceOr::Item(schema)) = &resp.schema {
144 let status_suffix = match resp.status_code {
145 crate::openapi::ResponseStatus::Code(code) => code.to_string(),
146 crate::openapi::ResponseStatus::Default => "Default".to_string(),
147 };
148 let name_hint =
149 format!("{}Response{}", op_name.to_upper_camel_case(), status_suffix);
150 register_inline_schema(&mut type_space, &mut inline_types, schema, name_hint);
151 }
152 }
153 }
154
155 Ok(Self {
156 type_space,
157 schema_to_type,
158 inline_types,
159 })
160 }
161
162 pub fn generate_all_types(&self) -> TokenStream {
164 self.type_space.to_stream()
165 }
166
167 pub fn get_type_name(&self, reference: &str) -> Option<String> {
170 let schema_name = reference.strip_prefix("#/components/schemas/")?;
172
173 self.schema_to_type.get(schema_name).cloned()
175 }
176
177 fn resolve_reference(&self, reference: &str) -> TokenStream {
180 if let Some(type_name) = self.get_type_name(reference) {
181 let ident = format_ident!("{}", type_name);
182 quote! { #ident }
183 } else {
184 quote! { serde_json::Value }
185 }
186 }
187
188 pub fn is_inline_schema(schema: &ReferenceOr<Schema>) -> bool {
190 matches!(schema, ReferenceOr::Item(_))
191 }
192
193 pub fn type_for_schema(&self, schema: &ReferenceOr<Schema>, name_hint: &str) -> TokenStream {
196 self.type_for_schema_with_definitions(schema, name_hint)
197 .type_ref
198 }
199
200 pub fn type_for_boxed_schema(
202 &self,
203 schema: &ReferenceOr<Box<Schema>>,
204 name_hint: &str,
205 ) -> TokenStream {
206 match schema {
207 ReferenceOr::Reference { reference } => self.resolve_reference(reference),
208 ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, name_hint).type_ref,
209 }
210 }
211
212 pub fn type_for_schema_with_definitions(
215 &self,
216 schema: &ReferenceOr<Schema>,
217 name_hint: &str,
218 ) -> GeneratedType {
219 match schema {
220 ReferenceOr::Reference { reference } => {
221 GeneratedType::simple(self.resolve_reference(reference))
222 }
223 ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, name_hint),
224 }
225 }
226
227 fn type_for_inline_schema(&self, schema: &Schema, name_hint: &str) -> GeneratedType {
232 if let Some(type_id) = self.inline_types.get(name_hint)
234 && let Ok(typ) = self.type_space.get_type(type_id)
235 {
236 return GeneratedType::simple(typ.ident());
237 }
238
239 match &schema.schema_kind {
242 SchemaKind::Type(Type::String(_)) => GeneratedType::simple(quote! { String }),
243 SchemaKind::Type(Type::Integer(_)) => GeneratedType::simple(quote! { i64 }),
244 SchemaKind::Type(Type::Number(_)) => GeneratedType::simple(quote! { f64 }),
245 SchemaKind::Type(Type::Boolean(_)) => GeneratedType::simple(quote! { bool }),
246 SchemaKind::Type(Type::Array(arr)) => {
247 if let Some(items) = &arr.items {
248 let inner = self.type_for_boxed_schema(items, &format!("{}Item", name_hint));
249 GeneratedType::simple(quote! { Vec<#inner> })
250 } else {
251 GeneratedType::simple(quote! { Vec<serde_json::Value> })
252 }
253 }
254 SchemaKind::Type(Type::Object(obj)) => self.generate_inline_struct(obj, name_hint),
255 _ => GeneratedType::simple(quote! { serde_json::Value }),
256 }
257 }
258
259 fn generate_inline_struct(
261 &self,
262 obj: &openapiv3::ObjectType,
263 name_hint: &str,
264 ) -> GeneratedType {
265 let struct_name = format_ident!("{}", name_hint.to_upper_camel_case());
266 let mut definitions = Vec::new();
267
268 let fields: Vec<_> = obj
270 .properties
271 .iter()
272 .map(|(prop_name, prop_schema)| {
273 let field_name = format_ident!("{}", AsSnakeCase(prop_name).to_string());
274 let is_required = obj.required.contains(prop_name);
275
276 let inner_hint = format!("{}{}", name_hint, prop_name.to_upper_camel_case());
278 let generated = match prop_schema {
279 ReferenceOr::Reference { reference } => {
280 GeneratedType::simple(self.resolve_reference(reference))
281 }
282 ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, &inner_hint),
283 };
284
285 definitions.extend(generated.definitions);
287
288 let field_type = if is_required {
289 generated.type_ref
290 } else {
291 let inner = generated.type_ref;
292 quote! { Option<#inner> }
293 };
294
295 let snake_name = AsSnakeCase(prop_name).to_string();
297 if snake_name != *prop_name {
298 quote! {
299 #[serde(rename = #prop_name)]
300 pub #field_name: #field_type
301 }
302 } else {
303 quote! { pub #field_name: #field_type }
304 }
305 })
306 .collect();
307
308 let struct_def = quote! {
310 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
311 pub struct #struct_name {
312 #(#fields,)*
313 }
314 };
315
316 definitions.push(struct_def);
317
318 GeneratedType {
319 type_ref: quote! { #struct_name },
320 definitions,
321 }
322 }
323
324 pub fn param_type(&self, param: &OperationParam) -> TokenStream {
326 if let Some(schema) = ¶m.schema {
327 self.type_for_schema(schema, ¶m.name.to_upper_camel_case())
328 } else {
329 quote! { String }
330 }
331 }
332
333 pub fn request_body_type(&self, body: &RequestBody, op_name: &str) -> TokenStream {
335 if let Some(schema) = &body.schema {
336 self.type_for_schema(schema, &format!("{}Body", op_name.to_upper_camel_case()))
337 } else {
338 quote! { serde_json::Value }
339 }
340 }
341
342 pub fn generate_query_struct(
347 &self,
348 op: &Operation,
349 overrides: &TypeOverrides,
350 unknown_field: Option<&syn::Ident>,
351 ) -> Option<(syn::Ident, TokenStream)> {
352 if overrides.is_replaced(op.method, &op.path, GeneratedTypeKind::Query) {
354 return None;
355 }
356
357 let query_params: Vec<_> = op
358 .parameters
359 .iter()
360 .filter(|p| p.location == ParamLocation::Query)
361 .collect();
362
363 if query_params.is_empty() && unknown_field.is_none() {
365 return None;
366 }
367
368 let default_name = format!(
369 "{}Query",
370 op.operation_id
371 .as_deref()
372 .unwrap_or(&op.path)
373 .to_upper_camel_case()
374 );
375
376 let struct_name = if let Some(TypeOverride::Rename { name, .. }) =
378 overrides.get(op.method, &op.path, GeneratedTypeKind::Query)
379 {
380 name.clone()
381 } else {
382 format_ident!("{}", default_name)
383 };
384
385 let fields = query_params.iter().map(|param| {
386 let name = format_ident!("{}", heck::AsSnakeCase(¶m.name).to_string());
387 let ty = self.param_type(param);
388
389 if param.required {
390 quote! { pub #name: #ty }
391 } else {
392 quote! { pub #name: Option<#ty> }
393 }
394 });
395
396 let unknown_field_def = unknown_field.map(|name| {
398 quote! {
399 #[serde(flatten)]
400 pub #name: ::std::collections::HashMap<String, String>
401 }
402 });
403
404 let definition = quote_spanned! { struct_name.span() =>
406 #[derive(Debug, Clone, serde::Deserialize)]
407 pub struct #struct_name {
408 #(#fields,)*
409 #unknown_field_def
410 }
411 };
412
413 Some((struct_name, definition))
414 }
415
416 pub fn generate_path_struct(
424 &self,
425 op: &Operation,
426 overrides: &TypeOverrides,
427 ) -> Option<(syn::Ident, TokenStream)> {
428 if overrides.is_replaced(op.method, &op.path, GeneratedTypeKind::Path) {
430 return None;
431 }
432
433 let mut path_params: Vec<_> = op
435 .parameters
436 .iter()
437 .filter(|p| p.location == ParamLocation::Path)
438 .collect();
439
440 if path_params.is_empty() {
441 return None;
442 }
443
444 path_params.sort_by_key(|p| {
446 let placeholder = format!("{{{}}}", p.name);
447 op.path.find(&placeholder).unwrap_or(usize::MAX)
448 });
449
450 let default_name = format!(
451 "{}Path",
452 op.operation_id
453 .as_deref()
454 .unwrap_or(&op.path)
455 .to_upper_camel_case()
456 );
457
458 let struct_name = if let Some(TypeOverride::Rename { name, .. }) =
460 overrides.get(op.method, &op.path, GeneratedTypeKind::Path)
461 {
462 name.clone()
463 } else {
464 format_ident!("{}", default_name)
465 };
466
467 let fields = path_params.iter().map(|param| {
468 let snake_name = heck::AsSnakeCase(¶m.name).to_string();
469 let field_name = format_ident!("{}", snake_name);
470 let ty = self.param_type(param);
471 let param_name = ¶m.name;
472
473 if snake_name != param.name {
475 quote! {
476 #[serde(rename = #param_name)]
477 pub #field_name: #ty
478 }
479 } else {
480 quote! { pub #field_name: #ty }
481 }
482 });
483
484 let definition = quote_spanned! { struct_name.span() =>
486 #[derive(Debug, Clone, serde::Deserialize)]
487 pub struct #struct_name {
488 #(#fields,)*
489 }
490 };
491
492 Some((struct_name, definition))
493 }
494}
495
496fn to_schemars<T: serde::Serialize>(schema: &T) -> Result<schemars::schema::Schema> {
498 serde_value::to_value(schema)
499 .map_err(|e| Error::TypeGenError(format!("failed to serialize schema: {}", e)))?
500 .deserialize_into::<schemars::schema::Schema>()
501 .map_err(|e| Error::TypeGenError(format!("failed to deserialize schema: {}", e)))
502}
503
504fn register_inline_schema(
506 type_space: &mut TypeSpace,
507 inline_types: &mut HashMap<String, TypeId>,
508 schema: &Schema,
509 name_hint: String,
510) {
511 if let Ok(schemars_schema) = to_schemars(schema)
512 && let Ok(type_id) =
513 type_space.add_type_with_name(&schemars_schema, Some(name_hint.clone()))
514 {
515 inline_types.insert(name_hint, type_id);
516 }
517}