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 let status_suffix = match resp.status_code {
155 crate::openapi::ResponseStatus::Code(code) => code.to_string(),
156 crate::openapi::ResponseStatus::Default => "Default".to_string(),
157 };
158 for header in &resp.headers {
159 if let Some(ReferenceOr::Item(schema)) = &header.schema {
160 let name_hint = format!(
161 "{}Response{}{}",
162 op_name.to_upper_camel_case(),
163 status_suffix,
164 header.name.to_upper_camel_case()
165 );
166 register_inline_schema(
167 &mut type_space,
168 &mut inline_types,
169 schema,
170 name_hint,
171 );
172 }
173 }
174 }
175 }
176
177 Ok(Self {
178 type_space,
179 schema_to_type,
180 inline_types,
181 })
182 }
183
184 pub fn generate_all_types(&self) -> TokenStream {
186 self.type_space.to_stream()
187 }
188
189 pub fn get_type_name(&self, reference: &str) -> Option<String> {
192 let schema_name = reference.strip_prefix("#/components/schemas/")?;
194
195 self.schema_to_type.get(schema_name).cloned()
197 }
198
199 fn resolve_reference(&self, reference: &str) -> TokenStream {
202 if let Some(type_name) = self.get_type_name(reference) {
203 let ident = format_ident!("{}", type_name);
204 quote! { #ident }
205 } else {
206 quote! { serde_json::Value }
207 }
208 }
209
210 pub fn is_inline_schema(schema: &ReferenceOr<Schema>) -> bool {
212 matches!(schema, ReferenceOr::Item(_))
213 }
214
215 pub fn type_for_schema(&self, schema: &ReferenceOr<Schema>, name_hint: &str) -> TokenStream {
218 self.type_for_schema_with_definitions(schema, name_hint)
219 .type_ref
220 }
221
222 pub fn type_for_boxed_schema(
224 &self,
225 schema: &ReferenceOr<Box<Schema>>,
226 name_hint: &str,
227 ) -> TokenStream {
228 match schema {
229 ReferenceOr::Reference { reference } => self.resolve_reference(reference),
230 ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, name_hint).type_ref,
231 }
232 }
233
234 pub fn type_for_schema_with_definitions(
237 &self,
238 schema: &ReferenceOr<Schema>,
239 name_hint: &str,
240 ) -> GeneratedType {
241 match schema {
242 ReferenceOr::Reference { reference } => {
243 GeneratedType::simple(self.resolve_reference(reference))
244 }
245 ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, name_hint),
246 }
247 }
248
249 fn type_for_inline_schema(&self, schema: &Schema, name_hint: &str) -> GeneratedType {
254 if let Some(type_id) = self.inline_types.get(name_hint)
256 && let Ok(typ) = self.type_space.get_type(type_id)
257 {
258 return GeneratedType::simple(typ.ident());
259 }
260
261 match &schema.schema_kind {
264 SchemaKind::Type(Type::String(_)) => GeneratedType::simple(quote! { String }),
265 SchemaKind::Type(Type::Integer(_)) => GeneratedType::simple(quote! { i64 }),
266 SchemaKind::Type(Type::Number(_)) => GeneratedType::simple(quote! { f64 }),
267 SchemaKind::Type(Type::Boolean(_)) => GeneratedType::simple(quote! { bool }),
268 SchemaKind::Type(Type::Array(arr)) => {
269 if let Some(items) = &arr.items {
270 let inner = self.type_for_boxed_schema(items, &format!("{}Item", name_hint));
271 GeneratedType::simple(quote! { Vec<#inner> })
272 } else {
273 GeneratedType::simple(quote! { Vec<serde_json::Value> })
274 }
275 }
276 SchemaKind::Type(Type::Object(obj)) => self.generate_inline_struct(obj, name_hint),
277 _ => GeneratedType::simple(quote! { serde_json::Value }),
278 }
279 }
280
281 fn generate_inline_struct(
283 &self,
284 obj: &openapiv3::ObjectType,
285 name_hint: &str,
286 ) -> GeneratedType {
287 let struct_name = format_ident!("{}", name_hint.to_upper_camel_case());
288 let mut definitions = Vec::new();
289
290 let fields: Vec<_> = obj
292 .properties
293 .iter()
294 .map(|(prop_name, prop_schema)| {
295 let field_name = format_ident!("{}", AsSnakeCase(prop_name).to_string());
296 let is_required = obj.required.contains(prop_name);
297
298 let inner_hint = format!("{}{}", name_hint, prop_name.to_upper_camel_case());
300 let generated = match prop_schema {
301 ReferenceOr::Reference { reference } => {
302 GeneratedType::simple(self.resolve_reference(reference))
303 }
304 ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, &inner_hint),
305 };
306
307 definitions.extend(generated.definitions);
309
310 let field_type = if is_required {
311 generated.type_ref
312 } else {
313 let inner = generated.type_ref;
314 quote! { Option<#inner> }
315 };
316
317 let snake_name = AsSnakeCase(prop_name).to_string();
319 if snake_name != *prop_name {
320 quote! {
321 #[serde(rename = #prop_name)]
322 pub #field_name: #field_type
323 }
324 } else {
325 quote! { pub #field_name: #field_type }
326 }
327 })
328 .collect();
329
330 let struct_def = quote! {
332 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
333 pub struct #struct_name {
334 #(#fields,)*
335 }
336 };
337
338 definitions.push(struct_def);
339
340 GeneratedType {
341 type_ref: quote! { #struct_name },
342 definitions,
343 }
344 }
345
346 pub fn param_type(&self, param: &OperationParam) -> TokenStream {
348 if let Some(schema) = ¶m.schema {
349 self.type_for_schema(schema, ¶m.name.to_upper_camel_case())
350 } else {
351 quote! { String }
352 }
353 }
354
355 pub fn request_body_type(&self, body: &RequestBody, op_name: &str) -> TokenStream {
357 if let Some(schema) = &body.schema {
358 self.type_for_schema(schema, &format!("{}Body", op_name.to_upper_camel_case()))
359 } else {
360 quote! { serde_json::Value }
361 }
362 }
363
364 pub fn generate_query_struct(
369 &self,
370 op: &Operation,
371 overrides: &TypeOverrides,
372 unknown_field: Option<&syn::Ident>,
373 ) -> Option<(syn::Ident, TokenStream)> {
374 if overrides.is_replaced(op.method, &op.path, GeneratedTypeKind::Query) {
376 return None;
377 }
378
379 let query_params: Vec<_> = op
380 .parameters
381 .iter()
382 .filter(|p| p.location == ParamLocation::Query)
383 .collect();
384
385 if query_params.is_empty() && unknown_field.is_none() {
387 return None;
388 }
389
390 let default_name = format!(
391 "{}Query",
392 op.operation_id
393 .as_deref()
394 .unwrap_or(&op.path)
395 .to_upper_camel_case()
396 );
397
398 let struct_name = if let Some(TypeOverride::Rename { name, .. }) =
400 overrides.get(op.method, &op.path, GeneratedTypeKind::Query)
401 {
402 name.clone()
403 } else {
404 format_ident!("{}", default_name)
405 };
406
407 let fields = query_params.iter().map(|param| {
408 let name = format_ident!("{}", heck::AsSnakeCase(¶m.name).to_string());
409 let ty = self.param_type(param);
410
411 if param.required {
412 quote! { pub #name: #ty }
413 } else {
414 quote! { pub #name: Option<#ty> }
415 }
416 });
417
418 let unknown_field_def = unknown_field.map(|name| {
420 quote! {
421 #[serde(flatten)]
422 pub #name: ::std::collections::HashMap<String, String>
423 }
424 });
425
426 let definition = quote_spanned! { struct_name.span() =>
428 #[derive(Debug, Clone, serde::Deserialize)]
429 pub struct #struct_name {
430 #(#fields,)*
431 #unknown_field_def
432 }
433 };
434
435 Some((struct_name, definition))
436 }
437
438 pub fn generate_path_struct(
446 &self,
447 op: &Operation,
448 overrides: &TypeOverrides,
449 ) -> Option<(syn::Ident, TokenStream)> {
450 if overrides.is_replaced(op.method, &op.path, GeneratedTypeKind::Path) {
452 return None;
453 }
454
455 let mut path_params: Vec<_> = op
457 .parameters
458 .iter()
459 .filter(|p| p.location == ParamLocation::Path)
460 .collect();
461
462 if path_params.is_empty() {
463 return None;
464 }
465
466 path_params.sort_by_key(|p| {
468 let placeholder = format!("{{{}}}", p.name);
469 op.path.find(&placeholder).unwrap_or(usize::MAX)
470 });
471
472 let default_name = format!(
473 "{}Path",
474 op.operation_id
475 .as_deref()
476 .unwrap_or(&op.path)
477 .to_upper_camel_case()
478 );
479
480 let struct_name = if let Some(TypeOverride::Rename { name, .. }) =
482 overrides.get(op.method, &op.path, GeneratedTypeKind::Path)
483 {
484 name.clone()
485 } else {
486 format_ident!("{}", default_name)
487 };
488
489 let fields = path_params.iter().map(|param| {
490 let snake_name = heck::AsSnakeCase(¶m.name).to_string();
491 let field_name = format_ident!("{}", snake_name);
492 let ty = self.param_type(param);
493 let param_name = ¶m.name;
494
495 if snake_name != param.name {
497 quote! {
498 #[serde(rename = #param_name)]
499 pub #field_name: #ty
500 }
501 } else {
502 quote! { pub #field_name: #ty }
503 }
504 });
505
506 let definition = quote_spanned! { struct_name.span() =>
508 #[derive(Debug, Clone, serde::Deserialize)]
509 pub struct #struct_name {
510 #(#fields,)*
511 }
512 };
513
514 Some((struct_name, definition))
515 }
516}
517
518fn to_schemars<T: serde::Serialize>(schema: &T) -> Result<schemars::schema::Schema> {
520 serde_value::to_value(schema)
521 .map_err(|e| Error::TypeGenError(format!("failed to serialize schema: {}", e)))?
522 .deserialize_into::<schemars::schema::Schema>()
523 .map_err(|e| Error::TypeGenError(format!("failed to deserialize schema: {}", e)))
524}
525
526fn register_inline_schema(
528 type_space: &mut TypeSpace,
529 inline_types: &mut HashMap<String, TypeId>,
530 schema: &Schema,
531 name_hint: String,
532) {
533 if let Ok(schemars_schema) = to_schemars(schema)
534 && let Ok(type_id) =
535 type_space.add_type_with_name(&schemars_schema, Some(name_hint.clone()))
536 {
537 inline_types.insert(name_hint, type_id);
538 }
539}