1#![allow(clippy::pedantic)]
9
10use proc_macro::TokenStream;
11use quote::{ToTokens, quote};
12use syn::{
13 Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Lit, LitBool, LitStr, Meta, Path,
14 Type, parse_macro_input, spanned::Spanned,
15};
16
17#[proc_macro_derive(Generable, attributes(generable, serde))]
53pub fn derive_generable(input: TokenStream) -> TokenStream {
54 let input = parse_macro_input!(input as DeriveInput);
55 match derive_generable_impl(&input) {
56 Ok(tokens) => tokens.into(),
57 Err(err) => err.to_compile_error().into(),
58 }
59}
60
61struct ContainerAttrs {
62 crate_path: Path,
63 rename_all: Option<String>,
64 description: Option<String>,
65 example: Option<Lit>,
66}
67
68impl Default for ContainerAttrs {
69 fn default() -> Self {
70 let crate_path = match syn::parse_str::<Path>("::fm_rs") {
71 Ok(path) => path,
72 Err(_) => Path::from(syn::Ident::new("fm_rs", proc_macro2::Span::call_site())),
73 };
74 Self {
75 crate_path,
76 rename_all: None,
77 description: None,
78 example: None,
79 }
80 }
81}
82
83#[derive(Default)]
84struct FieldAttrs {
85 rename: Option<String>,
86 skip: bool,
87 default: bool,
88 description: Option<String>,
89 example: Option<Lit>,
90 minimum: Option<Lit>,
91 maximum: Option<Lit>,
92 min_length: Option<Lit>,
93 max_length: Option<Lit>,
94 pattern: Option<String>,
95 min_items: Option<Lit>,
96 max_items: Option<Lit>,
97 nullable: bool,
98}
99
100struct SchemaInfo {
101 expr: proc_macro2::TokenStream,
102 optional: bool,
103}
104
105fn derive_generable_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
106 let container_attrs = parse_container_attrs(&input.attrs)?;
107 let crate_path = &container_attrs.crate_path;
108 let ident = &input.ident;
109 let schema_body = match &input.data {
110 Data::Struct(data) => schema_for_struct(data, &container_attrs)?,
111 Data::Enum(data) => schema_for_enum(data, &container_attrs)?,
112 Data::Union(_) => {
113 return Err(syn::Error::new(
114 input.span(),
115 "Generable does not support unions",
116 ));
117 }
118 };
119
120 let tokens = quote! {
121 impl #crate_path::Generable for #ident {
122 fn schema() -> #crate_path::__serde_json::Value {
123 #schema_body
124 }
125 }
126 };
127
128 Ok(tokens)
129}
130
131fn parse_container_attrs(attrs: &[Attribute]) -> syn::Result<ContainerAttrs> {
132 let mut out = ContainerAttrs::default();
133
134 for attr in attrs {
135 if attr.path().is_ident("generable") {
136 attr.parse_nested_meta(|meta| {
137 if meta.path.is_ident("crate") {
138 let value = meta.value()?.parse::<LitStr>()?;
139 out.crate_path = syn::parse_str(&value.value())
140 .map_err(|_| syn::Error::new(value.span(), "invalid crate path"))?;
141 return Ok(());
142 }
143 if meta.path.is_ident("rename_all") {
144 let value = meta.value()?.parse::<LitStr>()?;
145 out.rename_all = Some(value.value());
146 return Ok(());
147 }
148 if meta.path.is_ident("description") {
149 let value = meta.value()?.parse::<LitStr>()?;
150 out.description = Some(value.value());
151 return Ok(());
152 }
153 if meta.path.is_ident("example") {
154 let value = meta.value()?.parse::<Lit>()?;
155 out.example = Some(value);
156 return Ok(());
157 }
158 Err(meta.error("unsupported generable attribute"))
159 })?;
160 }
161
162 if attr.path().is_ident("serde") {
163 attr.parse_nested_meta(|meta| {
164 if meta.path.is_ident("rename_all") {
165 let value = meta.value()?.parse::<LitStr>()?;
166 out.rename_all = Some(value.value());
167 return Ok(());
168 }
169 Ok(())
170 })?;
171 }
172 }
173
174 if out.description.is_none() {
175 out.description = doc_comment(attrs);
176 }
177
178 Ok(out)
179}
180
181fn parse_field_attrs(attrs: &[Attribute]) -> syn::Result<FieldAttrs> {
182 let mut out = FieldAttrs::default();
183
184 for attr in attrs {
185 if attr.path().is_ident("generable") {
186 attr.parse_nested_meta(|meta| {
187 if meta.path.is_ident("rename") {
188 let value = meta.value()?.parse::<LitStr>()?;
189 out.rename = Some(value.value());
190 return Ok(());
191 }
192 if meta.path.is_ident("skip") {
193 out.skip = true;
194 return Ok(());
195 }
196 if meta.path.is_ident("description") {
197 let value = meta.value()?.parse::<LitStr>()?;
198 out.description = Some(value.value());
199 return Ok(());
200 }
201 if meta.path.is_ident("example") {
202 let value = meta.value()?.parse::<Lit>()?;
203 out.example = Some(value);
204 return Ok(());
205 }
206 if meta.path.is_ident("minimum") {
207 let value = meta.value()?.parse::<Lit>()?;
208 out.minimum = Some(value);
209 return Ok(());
210 }
211 if meta.path.is_ident("maximum") {
212 let value = meta.value()?.parse::<Lit>()?;
213 out.maximum = Some(value);
214 return Ok(());
215 }
216 if meta.path.is_ident("min_length") {
217 let value = meta.value()?.parse::<Lit>()?;
218 out.min_length = Some(value);
219 return Ok(());
220 }
221 if meta.path.is_ident("max_length") {
222 let value = meta.value()?.parse::<Lit>()?;
223 out.max_length = Some(value);
224 return Ok(());
225 }
226 if meta.path.is_ident("pattern") {
227 let value = meta.value()?.parse::<LitStr>()?;
228 out.pattern = Some(value.value());
229 return Ok(());
230 }
231 if meta.path.is_ident("min_items") {
232 let value = meta.value()?.parse::<Lit>()?;
233 out.min_items = Some(value);
234 return Ok(());
235 }
236 if meta.path.is_ident("max_items") {
237 let value = meta.value()?.parse::<Lit>()?;
238 out.max_items = Some(value);
239 return Ok(());
240 }
241 if meta.path.is_ident("nullable") {
242 let value = meta.value()?.parse::<LitBool>()?;
243 out.nullable = value.value;
244 return Ok(());
245 }
246 Err(meta.error("unsupported generable attribute"))
247 })?;
248 }
249
250 if attr.path().is_ident("serde") {
251 attr.parse_nested_meta(|meta| {
252 if meta.path.is_ident("rename") {
253 let value = meta.value()?.parse::<LitStr>()?;
254 out.rename = Some(value.value());
255 return Ok(());
256 }
257 if meta.path.is_ident("skip") || meta.path.is_ident("skip_serializing") {
258 out.skip = true;
259 return Ok(());
260 }
261 if meta.path.is_ident("default") {
262 out.default = true;
263 return Ok(());
264 }
265 Ok(())
266 })?;
267 }
268 }
269
270 if out.description.is_none() {
271 out.description = doc_comment(attrs);
272 }
273
274 Ok(out)
275}
276
277fn schema_for_struct(
278 data: &DataStruct,
279 container: &ContainerAttrs,
280) -> syn::Result<proc_macro2::TokenStream> {
281 let crate_path = &container.crate_path;
282 let mut property_inserts = Vec::new();
283 let mut required_fields = Vec::new();
284 let mut container_inserts = Vec::new();
285
286 match &data.fields {
287 Fields::Named(fields) => {
288 for field in &fields.named {
289 let field_attrs = parse_field_attrs(&field.attrs)?;
290 if field_attrs.skip {
291 continue;
292 }
293 let ident = field
294 .ident
295 .as_ref()
296 .ok_or_else(|| syn::Error::new(field.span(), "expected named field"))?;
297 let name = field_name(ident.to_string(), &field_attrs, container);
298 let SchemaInfo {
299 expr: schema_expr,
300 optional,
301 } = schema_for_type(&field.ty, &field_attrs, crate_path)?;
302 property_inserts.push(quote! {
303 properties.insert(#name.to_string(), #schema_expr);
304 });
305 if !optional && !field_attrs.default {
306 required_fields.push(name);
307 }
308 }
309 }
310 Fields::Unnamed(fields) => {
311 if fields.unnamed.len() == 1 {
312 let field = fields
313 .unnamed
314 .first()
315 .ok_or_else(|| syn::Error::new(fields.span(), "expected field"))?;
316 let field_attrs = parse_field_attrs(&field.attrs)?;
317 let schema_info = schema_for_type(&field.ty, &field_attrs, crate_path)?;
318 return Ok(schema_info.expr);
319 }
320 return Err(syn::Error::new(
321 fields.span(),
322 "Generable only supports named fields or newtype structs",
323 ));
324 }
325 Fields::Unit => {
326 return Err(syn::Error::new(
327 data.fields.span(),
328 "Generable does not support unit structs",
329 ));
330 }
331 }
332
333 let required_values = required_fields
334 .iter()
335 .map(|name| quote!(#crate_path::__serde_json::Value::String(#name.to_string())));
336
337 if let Some(desc) = &container.description {
338 let desc_lit = LitStr::new(desc, proc_macro2::Span::call_site());
339 container_inserts.push(quote! {
340 schema.insert(
341 "description".to_string(),
342 #crate_path::__serde_json::Value::String(#desc_lit.to_string())
343 );
344 });
345 }
346 if let Some(example) = &container.example {
347 let example_tokens = example.to_token_stream();
348 container_inserts.push(quote! {
349 schema.insert(
350 "example".to_string(),
351 #crate_path::__serde_json::json!(#example_tokens)
352 );
353 });
354 }
355
356 Ok(quote! {
357 let mut schema = #crate_path::__serde_json::Map::new();
358 schema.insert(
359 "type".to_string(),
360 #crate_path::__serde_json::Value::String("object".to_string())
361 );
362 let mut properties = #crate_path::__serde_json::Map::new();
363 #(#property_inserts)*
364 schema.insert(
365 "properties".to_string(),
366 #crate_path::__serde_json::Value::Object(properties)
367 );
368 let mut required = Vec::new();
369 #(required.push(#required_values);)*
370 if !required.is_empty() {
371 schema.insert("required".to_string(), #crate_path::__serde_json::Value::Array(required));
372 }
373 #(#container_inserts)*
374 #crate_path::__serde_json::Value::Object(schema)
375 })
376}
377
378fn schema_for_enum(
379 data: &DataEnum,
380 container: &ContainerAttrs,
381) -> syn::Result<proc_macro2::TokenStream> {
382 let crate_path = &container.crate_path;
383 let mut variants = Vec::new();
384 let mut container_inserts = Vec::new();
385
386 for variant in &data.variants {
387 match &variant.fields {
388 Fields::Unit => {}
389 _ => {
390 return Err(syn::Error::new(
391 variant.span(),
392 "Generable only supports unit enums (string-only)",
393 ));
394 }
395 }
396
397 let variant_attrs = parse_field_attrs(&variant.attrs)?;
398 if variant_attrs.skip {
399 continue;
400 }
401 let variant_name = apply_rename(variant.ident.to_string(), &variant_attrs, container);
402 variants.push(variant_name);
403 }
404
405 let variant_values = variants
406 .iter()
407 .map(|name| quote!(#crate_path::__serde_json::Value::String(#name.to_string())));
408
409 if let Some(desc) = &container.description {
410 let desc_lit = LitStr::new(desc, proc_macro2::Span::call_site());
411 container_inserts.push(quote! {
412 schema.insert(
413 "description".to_string(),
414 #crate_path::__serde_json::Value::String(#desc_lit.to_string())
415 );
416 });
417 }
418 if let Some(example) = &container.example {
419 let example_tokens = example.to_token_stream();
420 container_inserts.push(quote! {
421 schema.insert(
422 "example".to_string(),
423 #crate_path::__serde_json::json!(#example_tokens)
424 );
425 });
426 }
427
428 Ok(quote! {
429 let mut schema = #crate_path::__serde_json::Map::new();
430 let variants = vec![#(#variant_values),*];
431 schema.insert(
432 "type".to_string(),
433 #crate_path::__serde_json::Value::String("string".to_string())
434 );
435 schema.insert("enum".to_string(), #crate_path::__serde_json::Value::Array(variants));
436 #(#container_inserts)*
437 #crate_path::__serde_json::Value::Object(schema)
438 })
439}
440
441fn schema_for_type(ty: &Type, attrs: &FieldAttrs, crate_path: &Path) -> syn::Result<SchemaInfo> {
442 if let Some(inner) = option_inner(ty) {
443 let mut schema = schema_for_type(inner, attrs, crate_path)?;
444 schema.optional = true;
445 return Ok(schema);
446 }
447
448 if let Some(inner) = vec_inner(ty) {
449 let inner_schema = schema_for_type(inner, &FieldAttrs::default(), crate_path)?.expr;
450 let mut entries = vec![schema_type_entry(crate_path, "array")];
451 entries.push(schema_entry("items", quote!(#inner_schema)));
452 add_attr_entries(crate_path, attrs, &mut entries);
453 return Ok(SchemaInfo {
454 expr: schema_object_expr(crate_path, entries),
455 optional: false,
456 });
457 }
458
459 if let Some((key_ty, value_ty)) = map_inner(ty) {
460 if !is_string_type(key_ty) {
461 return Err(syn::Error::new(
462 key_ty.span(),
463 "Generable only supports map keys of type String",
464 ));
465 }
466 let value_schema = schema_for_type(value_ty, &FieldAttrs::default(), crate_path)?.expr;
467 let mut entries = vec![schema_type_entry(crate_path, "object")];
468 entries.push(schema_entry("additionalProperties", quote!(#value_schema)));
469 add_attr_entries(crate_path, attrs, &mut entries);
470 return Ok(SchemaInfo {
471 expr: schema_object_expr(crate_path, entries),
472 optional: false,
473 });
474 }
475
476 if is_string_type(ty) {
477 let mut entries = vec![schema_type_entry(crate_path, "string")];
478 add_attr_entries(crate_path, attrs, &mut entries);
479 return Ok(SchemaInfo {
480 expr: schema_object_expr(crate_path, entries),
481 optional: false,
482 });
483 }
484
485 if is_bool_type(ty) {
486 let mut entries = vec![schema_type_entry(crate_path, "boolean")];
487 add_attr_entries(crate_path, attrs, &mut entries);
488 return Ok(SchemaInfo {
489 expr: schema_object_expr(crate_path, entries),
490 optional: false,
491 });
492 }
493
494 if is_integer_type(ty) {
495 let mut entries = vec![schema_type_entry(crate_path, "integer")];
496 add_attr_entries(crate_path, attrs, &mut entries);
497 return Ok(SchemaInfo {
498 expr: schema_object_expr(crate_path, entries),
499 optional: false,
500 });
501 }
502
503 if is_number_type(ty) {
504 let mut entries = vec![schema_type_entry(crate_path, "number")];
505 add_attr_entries(crate_path, attrs, &mut entries);
506 return Ok(SchemaInfo {
507 expr: schema_object_expr(crate_path, entries),
508 optional: false,
509 });
510 }
511
512 if is_serde_json_value(ty) {
513 let mut entries = Vec::new();
514 add_attr_entries(crate_path, attrs, &mut entries);
515 return Ok(SchemaInfo {
516 expr: schema_object_expr(crate_path, entries),
517 optional: false,
518 });
519 }
520
521 let mut entries = Vec::new();
522 add_attr_entries(crate_path, attrs, &mut entries);
523 let schema = if entries.is_empty() {
524 quote!(<#ty as #crate_path::Generable>::schema())
525 } else {
526 let base = quote!(<#ty as #crate_path::Generable>::schema());
527 quote!({
528 let mut schema = #base;
529 if let #crate_path::__serde_json::Value::Object(ref mut map) = schema {
530 #(#entries)*
531 }
532 schema
533 })
534 };
535
536 Ok(SchemaInfo {
537 expr: schema,
538 optional: false,
539 })
540}
541
542fn schema_object_expr(
543 crate_path: &Path,
544 entries: Vec<proc_macro2::TokenStream>,
545) -> proc_macro2::TokenStream {
546 quote!({
547 let mut map = #crate_path::__serde_json::Map::new();
548 #(#entries)*
549 #crate_path::__serde_json::Value::Object(map)
550 })
551}
552
553fn schema_type_entry(crate_path: &Path, ty: &str) -> proc_macro2::TokenStream {
554 let lit = LitStr::new(ty, proc_macro2::Span::call_site());
555 schema_entry(
556 "type",
557 quote!(#crate_path::__serde_json::Value::String(#lit.to_string())),
558 )
559}
560
561fn schema_entry(key: &str, value_expr: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
562 let lit = LitStr::new(key, proc_macro2::Span::call_site());
563 quote!(map.insert(#lit.to_string(), #value_expr);)
564}
565
566fn schema_string_insert(crate_path: &Path, key: &str, value: &str) -> proc_macro2::TokenStream {
567 let key_lit = LitStr::new(key, proc_macro2::Span::call_site());
568 let val_lit = LitStr::new(value, proc_macro2::Span::call_site());
569 quote!(map.insert(#key_lit.to_string(), #crate_path::__serde_json::Value::String(#val_lit.to_string()));)
570}
571
572fn schema_value_insert(crate_path: &Path, key: &str, value: &Lit) -> proc_macro2::TokenStream {
573 let key_lit = LitStr::new(key, proc_macro2::Span::call_site());
574 let value_tokens = value.to_token_stream();
575 quote!(map.insert(#key_lit.to_string(), #crate_path::__serde_json::json!(#value_tokens));)
576}
577
578fn add_attr_entries(
579 crate_path: &Path,
580 attrs: &FieldAttrs,
581 entries: &mut Vec<proc_macro2::TokenStream>,
582) {
583 if let Some(desc) = &attrs.description {
584 entries.push(schema_string_insert(crate_path, "description", desc));
585 }
586 if let Some(example) = &attrs.example {
587 entries.push(schema_value_insert(crate_path, "example", example));
588 }
589 if let Some(minimum) = &attrs.minimum {
590 entries.push(schema_value_insert(crate_path, "minimum", minimum));
591 }
592 if let Some(maximum) = &attrs.maximum {
593 entries.push(schema_value_insert(crate_path, "maximum", maximum));
594 }
595 if let Some(min_length) = &attrs.min_length {
596 entries.push(schema_value_insert(crate_path, "minLength", min_length));
597 }
598 if let Some(max_length) = &attrs.max_length {
599 entries.push(schema_value_insert(crate_path, "maxLength", max_length));
600 }
601 if let Some(pattern) = &attrs.pattern {
602 entries.push(schema_string_insert(crate_path, "pattern", pattern));
603 }
604 if let Some(min_items) = &attrs.min_items {
605 entries.push(schema_value_insert(crate_path, "minItems", min_items));
606 }
607 if let Some(max_items) = &attrs.max_items {
608 entries.push(schema_value_insert(crate_path, "maxItems", max_items));
609 }
610 if attrs.nullable {
611 entries.push(schema_entry(
612 "nullable",
613 quote!(#crate_path::__serde_json::Value::Bool(true)),
614 ));
615 }
616}
617
618fn field_name(raw: String, attrs: &FieldAttrs, container: &ContainerAttrs) -> String {
619 if let Some(rename) = &attrs.rename {
620 return rename.clone();
621 }
622 if let Some(rule) = &container.rename_all {
623 apply_rename_all(&raw, rule)
624 } else {
625 raw
626 }
627}
628
629fn apply_rename(raw: String, attrs: &FieldAttrs, container: &ContainerAttrs) -> String {
630 if let Some(rename) = &attrs.rename {
631 return rename.clone();
632 }
633 if let Some(rule) = &container.rename_all {
634 apply_rename_all(&raw, rule)
635 } else {
636 raw
637 }
638}
639
640fn apply_rename_all(name: &str, rule: &str) -> String {
641 let words = split_words(name);
642 match rule {
643 "lowercase" => join_words(&words, "", |w| w.to_ascii_lowercase()),
644 "UPPERCASE" => join_words(&words, "", |w| w.to_ascii_uppercase()),
645 "snake_case" => join_words(&words, "_", |w| w.to_ascii_lowercase()),
646 "SCREAMING_SNAKE_CASE" => join_words(&words, "_", |w| w.to_ascii_uppercase()),
647 "kebab-case" => join_words(&words, "-", |w| w.to_ascii_lowercase()),
648 "PascalCase" => join_pascal(&words),
649 "camelCase" => join_camel(&words),
650 _ => name.to_string(),
651 }
652}
653
654fn split_words(name: &str) -> Vec<String> {
655 let mut words = Vec::new();
656 let mut current = String::new();
657 let mut chars = name.chars().peekable();
658 let mut prev_is_upper = false;
659 let mut prev_is_lower = false;
660
661 while let Some(ch) = chars.next() {
662 if ch == '_' || ch == '-' {
663 if !current.is_empty() {
664 words.push(current);
665 current = String::new();
666 }
667 prev_is_upper = false;
668 prev_is_lower = false;
669 continue;
670 }
671
672 let is_upper = ch.is_ascii_uppercase();
673 let is_lower = ch.is_ascii_lowercase();
674 let next_is_lower = chars
675 .peek()
676 .map(|next| next.is_ascii_lowercase())
677 .unwrap_or(false);
678
679 if !current.is_empty()
680 && ((prev_is_lower && is_upper) || (prev_is_upper && is_upper && next_is_lower))
681 {
682 words.push(current);
683 current = String::new();
684 }
685
686 current.push(ch);
687 prev_is_upper = is_upper;
688 prev_is_lower = is_lower;
689 }
690
691 if !current.is_empty() {
692 words.push(current);
693 }
694
695 words
696}
697
698fn join_words<F>(words: &[String], sep: &str, mut transform: F) -> String
699where
700 F: FnMut(&str) -> String,
701{
702 let mut out = String::new();
703 for (idx, word) in words.iter().enumerate() {
704 if idx > 0 {
705 out.push_str(sep);
706 }
707 out.push_str(&transform(word));
708 }
709 out
710}
711
712fn join_pascal(words: &[String]) -> String {
713 let mut out = String::new();
714 for word in words {
715 if word.is_empty() {
716 continue;
717 }
718 let mut chars = word.chars();
719 if let Some(first) = chars.next() {
720 out.push(first.to_ascii_uppercase());
721 out.push_str(&chars.as_str().to_ascii_lowercase());
722 }
723 }
724 out
725}
726
727fn join_camel(words: &[String]) -> String {
728 let mut out = String::new();
729 for (idx, word) in words.iter().enumerate() {
730 if word.is_empty() {
731 continue;
732 }
733 if idx == 0 {
734 out.push_str(&word.to_ascii_lowercase());
735 } else {
736 out.push_str(&join_pascal(std::slice::from_ref(word)));
737 }
738 }
739 out
740}
741
742fn option_inner(ty: &Type) -> Option<&Type> {
743 if let Type::Path(type_path) = ty {
744 let segment = type_path.path.segments.last()?;
745 if segment.ident == "Option"
746 && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
747 && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
748 {
749 return Some(inner);
750 }
751 }
752 None
753}
754
755fn vec_inner(ty: &Type) -> Option<&Type> {
756 if let Type::Path(type_path) = ty {
757 let segment = type_path.path.segments.last()?;
758 if (segment.ident == "Vec" || segment.ident == "VecDeque")
759 && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
760 && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
761 {
762 return Some(inner);
763 }
764 }
765 None
766}
767
768fn map_inner(ty: &Type) -> Option<(&Type, &Type)> {
769 if let Type::Path(type_path) = ty {
770 let segment = type_path.path.segments.last()?;
771 if (segment.ident == "HashMap" || segment.ident == "BTreeMap")
772 && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
773 {
774 let mut iter = args.args.iter();
775 let key = match iter.next()? {
776 syn::GenericArgument::Type(ty) => ty,
777 _ => return None,
778 };
779 let value = match iter.next()? {
780 syn::GenericArgument::Type(ty) => ty,
781 _ => return None,
782 };
783 return Some((key, value));
784 }
785 }
786 None
787}
788
789fn is_string_type(ty: &Type) -> bool {
790 match ty {
791 Type::Path(type_path) => {
792 let ident = &type_path.path.segments.last().map(|s| &s.ident);
793 matches!(ident, Some(id) if *id == "String")
794 }
795 Type::Reference(reference) => match &*reference.elem {
796 Type::Path(type_path) => {
797 let ident = &type_path.path.segments.last().map(|s| &s.ident);
798 matches!(ident, Some(id) if *id == "str")
799 }
800 _ => false,
801 },
802 _ => false,
803 }
804}
805
806fn is_bool_type(ty: &Type) -> bool {
807 matches!(ty, Type::Path(type_path) if type_path.path.is_ident("bool"))
808}
809
810fn is_integer_type(ty: &Type) -> bool {
811 if let Type::Path(type_path) = ty {
812 let ident = type_path.path.segments.last().map(|s| s.ident.to_string());
813 if let Some(name) = ident {
814 return matches!(
815 name.as_str(),
816 "u8" | "u16"
817 | "u32"
818 | "u64"
819 | "u128"
820 | "usize"
821 | "i8"
822 | "i16"
823 | "i32"
824 | "i64"
825 | "i128"
826 | "isize"
827 );
828 }
829 }
830 false
831}
832
833fn is_number_type(ty: &Type) -> bool {
834 matches!(ty, Type::Path(type_path) if type_path.path.is_ident("f32") || type_path.path.is_ident("f64"))
835}
836
837fn is_serde_json_value(ty: &Type) -> bool {
838 if let Type::Path(type_path) = ty
839 && let Some(last) = type_path.path.segments.last()
840 {
841 return last.ident == "Value";
842 }
843 false
844}
845
846fn doc_comment(attrs: &[Attribute]) -> Option<String> {
847 let mut parts = Vec::new();
848 for attr in attrs {
849 if attr.path().is_ident("doc")
850 && let Meta::NameValue(meta) = &attr.meta
851 && let syn::Expr::Lit(expr_lit) = &meta.value
852 && let Lit::Str(lit) = &expr_lit.lit
853 {
854 let value = lit.value();
855 let trimmed = value.trim();
856 if !trimmed.is_empty() {
857 parts.push(trimmed.to_string());
858 }
859 }
860 }
861 if parts.is_empty() {
862 None
863 } else {
864 Some(parts.join(" "))
865 }
866}