reinhardt_openapi_macros/
lib.rs1#![warn(missing_docs)]
8
9use proc_macro::TokenStream;
10use quote::quote;
11use syn::{Data, DeriveInput, Fields, parse_macro_input};
12
13mod crate_paths;
14mod schema;
15mod serde_attrs;
16
17use crate::crate_paths::get_reinhardt_openapi_crate;
18use schema::{FieldAttributes, extract_container_attributes, extract_field_attributes};
19use serde_attrs::{
20 TaggingStrategy, extract_serde_enum_attrs, extract_serde_rename_all,
21 extract_serde_variant_attrs,
22};
23
24#[proc_macro_derive(Schema, attributes(schema))]
80pub fn derive_schema(input: TokenStream) -> TokenStream {
81 let input = parse_macro_input!(input as DeriveInput);
82
83 match &input.data {
84 Data::Struct(data) => derive_struct_schema(&input, data),
85 Data::Enum(data) => derive_enum_schema(&input, data),
86 Data::Union(_) => syn::Error::new_spanned(&input, "Schema cannot be derived for unions")
87 .to_compile_error()
88 .into(),
89 }
90}
91
92fn derive_struct_schema(input: &DeriveInput, data: &syn::DataStruct) -> TokenStream {
94 let name = &input.ident;
95 let generics = &input.generics;
96 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
97
98 let fields = match &data.fields {
99 Fields::Named(fields) => &fields.named,
100 _ => {
101 return syn::Error::new_spanned(
102 input,
103 "Schema can only be derived for structs with named fields",
104 )
105 .to_compile_error()
106 .into();
107 }
108 };
109
110 let struct_name = name.to_string();
112 let container_attrs = match extract_container_attributes(&input.attrs) {
113 Ok(attrs) => attrs,
114 Err(err) => return err.to_compile_error().into(),
115 };
116
117 let rename_all = extract_serde_rename_all(&input.attrs);
120
121 let mut field_schemas = Vec::new();
123 let mut required_fields = Vec::new();
124 let mut flatten_schemas = Vec::new();
126
127 for field in fields {
128 let field_name = field.ident.as_ref().unwrap();
129 let field_name_str = field_name.to_string();
130 let field_type = &field.ty;
131
132 let attrs = match extract_field_attributes(&field.attrs) {
134 Ok(attrs) => attrs,
135 Err(err) => return err.to_compile_error().into(),
136 };
137
138 if attrs.read_only && attrs.write_only {
140 return syn::Error::new_spanned(
141 field,
142 "A field cannot be both read_only and write_only",
143 )
144 .to_compile_error()
145 .into();
146 }
147
148 if attrs.skip || attrs.skip_serializing || attrs.skip_deserializing {
150 continue;
151 }
152
153 if attrs.flatten {
155 let schema_builder = build_field_schema(field_type, &attrs);
156 flatten_schemas.push(schema_builder);
157 continue;
158 }
159
160 let property_name = attrs
163 .rename
164 .clone()
165 .unwrap_or_else(|| apply_rename_all(&field_name_str, rename_all.as_deref()));
166
167 let is_option = is_option_type(field_type);
170 if !is_option && !attrs.default {
171 required_fields.push(property_name.clone());
172 }
173
174 let schema_builder = build_field_schema(field_type, &attrs);
176
177 field_schemas.push(quote! {
178 builder = builder.property(#property_name, #schema_builder);
179 });
180 }
181
182 let required_builder = if !required_fields.is_empty() {
184 quote! {
185 #(builder = builder.required(#required_fields);)*
186 }
187 } else {
188 quote! {}
189 };
190
191 let openapi_crate = get_reinhardt_openapi_crate();
193
194 let container_mods = generate_container_modifications(&container_attrs, &openapi_crate);
196
197 let schema_body = if !flatten_schemas.is_empty() {
199 quote! {
200 use #openapi_crate::Schema;
201 use #openapi_crate::utoipa::openapi::schema::{AllOfBuilder, ObjectBuilder, SchemaType, Type};
202
203 let mut builder = ObjectBuilder::new()
205 .schema_type(SchemaType::Type(Type::Object));
206
207 #(#field_schemas)*
208 #required_builder
209
210 let main_schema = Schema::Object(builder.build());
211
212 let mut all_of_builder = AllOfBuilder::new();
214 all_of_builder = all_of_builder.item(#openapi_crate::RefOr::T(main_schema));
215 #(all_of_builder = all_of_builder.item(#openapi_crate::RefOr::T(#flatten_schemas));)*
216
217 let mut schema = Schema::AllOf(all_of_builder.build());
218 #container_mods
219 schema
220 }
221 } else {
222 quote! {
223 use #openapi_crate::Schema;
224 use #openapi_crate::utoipa::openapi::schema::{ObjectBuilder, SchemaType, Type};
225
226 let mut builder = ObjectBuilder::new()
227 .schema_type(SchemaType::Type(Type::Object));
228
229 #(#field_schemas)*
230 #required_builder
231
232 let mut schema = Schema::Object(builder.build());
233 #container_mods
234 schema
235 }
236 };
237
238 let inventory_registration = if generics.params.is_empty() {
241 quote! {
242 ::inventory::submit! {
245 #openapi_crate::SchemaRegistration::new(
246 #struct_name,
247 #name::schema
248 )
249 }
250 }
251 } else {
252 quote! {}
253 };
254
255 let expanded = quote! {
256 impl #impl_generics #openapi_crate::ToSchema for #name #ty_generics #where_clause {
257 fn schema() -> #openapi_crate::Schema {
258 #schema_body
259 }
260
261 fn schema_name() -> Option<String> {
262 Some(#struct_name.to_string())
263 }
264 }
265
266 #inventory_registration
267 };
268
269 TokenStream::from(expanded)
270}
271
272fn derive_enum_schema(input: &DeriveInput, data: &syn::DataEnum) -> TokenStream {
274 let name = &input.ident;
275 let generics = &input.generics;
276 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
277 let enum_name = name.to_string();
278
279 let container_attrs = match extract_container_attributes(&input.attrs) {
281 Ok(attrs) => attrs,
282 Err(err) => return err.to_compile_error().into(),
283 };
284
285 let serde_attrs = extract_serde_enum_attrs(&input.attrs);
287 let tagging = serde_attrs.tagging_strategy();
288
289 let openapi_crate = get_reinhardt_openapi_crate();
291
292 let all_unit_variants = data
294 .variants
295 .iter()
296 .all(|v| matches!(v.fields, Fields::Unit));
297
298 let container_mods = generate_container_modifications(&container_attrs, &openapi_crate);
300
301 let schema_body = if all_unit_variants && matches!(tagging, TaggingStrategy::External) {
302 let base = generate_simple_enum_schema(data, &openapi_crate, &serde_attrs);
304 quote! {
305 let mut schema = { #base };
306 #container_mods
307 schema
308 }
309 } else {
310 let base = generate_complex_enum_schema(data, &openapi_crate, &enum_name, &tagging);
312 quote! {
313 let mut schema = { #base };
314 #container_mods
315 schema
316 }
317 };
318
319 let inventory_registration = if generics.params.is_empty() {
321 quote! {
322 ::inventory::submit! {
323 #openapi_crate::SchemaRegistration::new(
324 #enum_name,
325 #name::schema
326 )
327 }
328 }
329 } else {
330 quote! {}
331 };
332
333 let expanded = quote! {
334 impl #impl_generics #openapi_crate::ToSchema for #name #ty_generics #where_clause {
335 fn schema() -> #openapi_crate::Schema {
336 #schema_body
337 }
338
339 fn schema_name() -> Option<String> {
340 Some(#enum_name.to_string())
341 }
342 }
343
344 #inventory_registration
345 };
346
347 TokenStream::from(expanded)
348}
349
350fn generate_container_modifications(
355 attrs: &schema::ContainerAttributes,
356 openapi_crate: &proc_macro2::TokenStream,
357) -> proc_macro2::TokenStream {
358 let mut mods = Vec::new();
359
360 if let Some(ref title) = attrs.title {
361 mods.push(quote! {
362 match schema {
363 Schema::Object(ref mut obj) => {
364 obj.title = Some(#title.to_string());
365 }
366 Schema::AllOf(ref mut all_of) => {
367 all_of.title = Some(#title.to_string());
368 }
369 _ => {}
370 }
371 });
372 }
373
374 if let Some(ref description) = attrs.description {
375 mods.push(quote! {
376 match schema {
377 Schema::Object(ref mut obj) => {
378 obj.description = Some(#description.to_string());
379 }
380 Schema::AllOf(ref mut all_of) => {
381 all_of.description = Some(#description.to_string());
382 }
383 _ => {}
384 }
385 });
386 }
387
388 if let Some(ref example) = attrs.example {
389 mods.push(quote! {
391 let example_value: serde_json::Value = serde_json::from_str(#example)
392 .unwrap_or_else(|_| serde_json::json!(#example));
393 match schema {
394 Schema::Object(ref mut obj) => {
395 obj.example = Some(example_value);
396 }
397 Schema::AllOf(ref mut all_of) => {
398 all_of.example = Some(example_value);
399 }
400 _ => {}
401 }
402 });
403 }
404
405 if attrs.deprecated {
406 mods.push(quote! {
407 match schema {
408 Schema::Object(ref mut obj) => {
409 obj.deprecated = Some(#openapi_crate::utoipa::openapi::Deprecated::True);
410 }
411 _ => {}
414 }
415 });
416 }
417
418 if attrs.nullable {
419 mods.push(quote! {
420 {
421 use #openapi_crate::utoipa::openapi::schema::{SchemaType, Type};
422 match schema {
423 Schema::Object(ref mut obj) => {
424 let existing_type = std::mem::replace(
426 &mut obj.schema_type,
427 SchemaType::AnyValue,
428 );
429 match existing_type {
430 SchemaType::Type(t) => {
431 obj.schema_type = SchemaType::from_iter([t, Type::Null]);
432 }
433 other => {
434 obj.schema_type = other;
435 }
436 }
437 }
438 Schema::AllOf(ref mut all_of) => {
439 let existing_type = std::mem::replace(
441 &mut all_of.schema_type,
442 SchemaType::AnyValue,
443 );
444 match existing_type {
445 SchemaType::AnyValue => {
446 all_of.schema_type = SchemaType::from_iter([Type::Object, Type::Null]);
447 }
448 SchemaType::Type(t) => {
449 all_of.schema_type = SchemaType::from_iter([t, Type::Null]);
450 }
451 other => {
452 all_of.schema_type = other;
453 }
454 }
455 }
456 _ => {}
457 }
458 }
459 });
460 }
461
462 if mods.is_empty() {
463 quote! {}
464 } else {
465 quote! {
466 #(#mods)*
467 }
468 }
469}
470
471fn generate_simple_enum_schema(
473 data: &syn::DataEnum,
474 openapi_crate: &proc_macro2::TokenStream,
475 serde_attrs: &serde_attrs::SerdeEnumAttrs,
476) -> proc_macro2::TokenStream {
477 let variant_names: Vec<String> = data
478 .variants
479 .iter()
480 .filter_map(|v| {
481 let variant_attrs = extract_serde_variant_attrs(&v.attrs);
482 if variant_attrs.skip {
483 return None;
484 }
485 let name = variant_attrs.rename.unwrap_or_else(|| {
487 apply_rename_all(&v.ident.to_string(), serde_attrs.rename_all.as_deref())
488 });
489 Some(name)
490 })
491 .collect();
492
493 quote! {
494 use #openapi_crate::Schema;
495 use #openapi_crate::utoipa::openapi::schema::{ObjectBuilder, SchemaType, Type};
496
497 Schema::Object(
498 ObjectBuilder::new()
499 .schema_type(SchemaType::Type(Type::String))
500 .enum_values(Some(vec![#(serde_json::Value::String(#variant_names.to_string())),*]))
501 .build()
502 )
503 }
504}
505
506fn generate_complex_enum_schema(
508 data: &syn::DataEnum,
509 openapi_crate: &proc_macro2::TokenStream,
510 enum_name: &str,
511 tagging: &TaggingStrategy,
512) -> proc_macro2::TokenStream {
513 let tagging_expr = match tagging {
515 TaggingStrategy::External => quote! {
516 #openapi_crate::EnumTagging::External
517 },
518 TaggingStrategy::Internal { tag } => quote! {
519 #openapi_crate::EnumTagging::Internal { tag: #tag.to_string() }
520 },
521 TaggingStrategy::Adjacent { tag, content } => quote! {
522 #openapi_crate::EnumTagging::Adjacent {
523 tag: #tag.to_string(),
524 content: #content.to_string(),
525 }
526 },
527 TaggingStrategy::Untagged => quote! {
528 #openapi_crate::EnumTagging::Untagged
529 },
530 };
531
532 let variant_additions: Vec<proc_macro2::TokenStream> = data
534 .variants
535 .iter()
536 .filter_map(|variant| {
537 let variant_attrs = extract_serde_variant_attrs(&variant.attrs);
538 if variant_attrs.skip {
539 return None;
540 }
541
542 let variant_name = variant_attrs
543 .rename
544 .clone()
545 .unwrap_or_else(|| variant.ident.to_string());
546
547 let variant_schema = generate_variant_schema(&variant.fields, openapi_crate);
548
549 Some(quote! {
550 builder = builder.variant(#variant_name, #variant_schema);
551 })
552 })
553 .collect();
554
555 quote! {
556 use #openapi_crate::{EnumSchemaBuilder, Schema, SchemaExt};
557
558 let mut builder = EnumSchemaBuilder::new(#enum_name)
559 .tagging(#tagging_expr);
560
561 #(#variant_additions)*
562
563 builder.build()
564 }
565}
566
567fn generate_variant_schema(
569 fields: &Fields,
570 openapi_crate: &proc_macro2::TokenStream,
571) -> proc_macro2::TokenStream {
572 match fields {
573 Fields::Unit => {
574 quote! {
576 #openapi_crate::Schema::object()
577 }
578 }
579 Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
580 let inner_type = &fields.unnamed.first().unwrap().ty;
582 quote! {
583 <#inner_type as #openapi_crate::ToSchema>::schema()
584 }
585 }
586 Fields::Unnamed(fields) => {
587 let type_schemas: Vec<proc_macro2::TokenStream> = fields
589 .unnamed
590 .iter()
591 .map(|f| {
592 let ty = &f.ty;
593 quote! {
594 #openapi_crate::RefOr::T(<#ty as #openapi_crate::ToSchema>::schema())
595 }
596 })
597 .collect();
598
599 quote! {
600 {
601 use #openapi_crate::utoipa::openapi::schema::{ArrayBuilder, SchemaType, Type};
602 #openapi_crate::Schema::Array(
603 ArrayBuilder::new()
604 .schema_type(SchemaType::Type(Type::Array))
605 .prefix_items(vec![#(#type_schemas),*])
606 .build()
607 )
608 }
609 }
610 }
611 Fields::Named(fields) => {
612 let mut property_additions = Vec::new();
614 let mut required_additions = Vec::new();
615
616 for field in &fields.named {
617 let field_name = field.ident.as_ref().unwrap();
618 let field_name_str = field_name.to_string();
619 let field_type = &field.ty;
620
621 let field_attrs = extract_field_attributes(&field.attrs).unwrap_or_default();
623 let property_name = field_attrs.rename.unwrap_or(field_name_str);
624
625 let is_option = is_option_type(field_type);
627 if !is_option {
628 required_additions.push(quote! {
629 builder = builder.required(#property_name);
630 });
631 }
632
633 property_additions.push(quote! {
634 builder = builder.property(
635 #property_name,
636 <#field_type as #openapi_crate::ToSchema>::schema()
637 );
638 });
639 }
640
641 quote! {
642 {
643 use #openapi_crate::utoipa::openapi::schema::{ObjectBuilder, SchemaType, Type};
644 let mut builder = ObjectBuilder::new()
645 .schema_type(SchemaType::Type(Type::Object));
646 #(#property_additions)*
647 #(#required_additions)*
648 #openapi_crate::Schema::Object(builder.build())
649 }
650 }
651 }
652 }
653}
654
655fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
657 match rename_all {
658 Some("lowercase") => name.to_lowercase(),
659 Some("UPPERCASE") => name.to_uppercase(),
660 Some("camelCase") => to_camel_case(name),
661 Some("snake_case") => to_snake_case(name),
662 Some("SCREAMING_SNAKE_CASE") => to_snake_case(name).to_uppercase(),
663 Some("kebab-case") => to_snake_case(name).replace('_', "-"),
664 Some("SCREAMING-KEBAB-CASE") => to_snake_case(name).to_uppercase().replace('_', "-"),
665 Some("PascalCase") | None => name.to_string(),
666 Some(_) => name.to_string(),
667 }
668}
669
670fn to_camel_case(s: &str) -> String {
672 let mut result = String::new();
673 for (i, c) in s.chars().enumerate() {
674 if i == 0 {
675 result.extend(c.to_lowercase());
676 } else {
677 result.push(c);
678 }
679 }
680 result
681}
682
683fn to_snake_case(s: &str) -> String {
689 let mut result = String::new();
690 let chars: Vec<char> = s.chars().collect();
691
692 for (i, c) in chars.iter().enumerate() {
693 if c.is_uppercase() {
694 if i > 0 {
701 let prev_is_lowercase = chars[i - 1].is_lowercase();
702 let next_is_lowercase = i + 1 < chars.len() && chars[i + 1].is_lowercase();
703
704 if prev_is_lowercase || next_is_lowercase {
705 result.push('_');
706 }
707 }
708 result.extend(c.to_lowercase());
709 } else {
710 result.push(*c);
711 }
712 }
713 result
714}
715
716fn is_option_type(ty: &syn::Type) -> bool {
718 if let syn::Type::Path(type_path) = ty
719 && let Some(segment) = type_path.path.segments.last()
720 {
721 return segment.ident == "Option";
722 }
723 false
724}
725
726fn build_field_schema(field_type: &syn::Type, attrs: &FieldAttributes) -> proc_macro2::TokenStream {
728 let openapi_crate = get_reinhardt_openapi_crate();
729 let base_schema = quote! {
730 <#field_type as #openapi_crate::ToSchema>::schema()
731 };
732
733 if attrs.is_empty() {
735 return base_schema;
736 }
737
738 let mut modifications = Vec::new();
740
741 if let Some(ref title) = attrs.title {
742 modifications.push(quote! {
743 if let Schema::Object(ref mut obj) = schema {
744 obj.title = Some(#title.to_string());
745 }
746 });
747 }
748
749 if let Some(ref description) = attrs.description {
750 modifications.push(quote! {
751 if let Schema::Object(ref mut obj) = schema {
752 obj.description = Some(#description.to_string());
753 }
754 });
755 }
756
757 if let Some(ref example) = attrs.example {
758 modifications.push(quote! {
760 if let Schema::Object(ref mut obj) = schema {
761 obj.example = Some(
762 serde_json::from_str(#example)
763 .unwrap_or_else(|_| serde_json::json!(#example))
764 );
765 }
766 });
767 }
768
769 if let Some(ref format) = attrs.format {
770 modifications.push(quote! {
771 if let Schema::Object(ref mut obj) = schema {
772 obj.format = Some(#openapi_crate::utoipa::openapi::schema::SchemaFormat::Custom(#format.to_string()));
773 }
774 });
775 }
776
777 if attrs.read_only {
778 modifications.push(quote! {
779 if let Schema::Object(ref mut obj) = schema {
780 obj.read_only = Some(true);
781 }
782 });
783 }
784
785 if attrs.write_only {
786 modifications.push(quote! {
787 if let Schema::Object(ref mut obj) = schema {
788 obj.write_only = Some(true);
789 }
790 });
791 }
792
793 if attrs.deprecated {
794 modifications.push(quote! {
795 if let Schema::Object(ref mut obj) = schema {
796 obj.deprecated = Some(#openapi_crate::utoipa::openapi::Deprecated::True);
797 }
798 });
799 }
800
801 if let Some(min) = attrs.minimum {
802 modifications.push(quote! {
803 if let Schema::Object(ref mut obj) = schema {
804 obj.minimum = Some(#openapi_crate::utoipa::Number::from(#min as f64));
805 }
806 });
807 }
808
809 if let Some(max) = attrs.maximum {
810 modifications.push(quote! {
811 if let Schema::Object(ref mut obj) = schema {
812 obj.maximum = Some(#openapi_crate::utoipa::Number::from(#max as f64));
813 }
814 });
815 }
816
817 if let Some(ex_min) = attrs.exclusive_minimum {
818 modifications.push(quote! {
819 if let Schema::Object(ref mut obj) = schema {
820 obj.exclusive_minimum = Some(#openapi_crate::utoipa::Number::from(#ex_min as f64));
821 }
822 });
823 }
824
825 if let Some(ex_max) = attrs.exclusive_maximum {
826 modifications.push(quote! {
827 if let Schema::Object(ref mut obj) = schema {
828 obj.exclusive_maximum = Some(#openapi_crate::utoipa::Number::from(#ex_max as f64));
829 }
830 });
831 }
832
833 if let Some(mul) = attrs.multiple_of {
834 modifications.push(quote! {
835 if let Schema::Object(ref mut obj) = schema {
836 obj.multiple_of = Some(#openapi_crate::utoipa::Number::from(#mul));
837 }
838 });
839 }
840
841 if let Some(min_len) = attrs.min_length {
842 modifications.push(quote! {
843 if let Schema::Object(ref mut obj) = schema {
844 obj.min_length = Some(#min_len);
845 }
846 });
847 }
848
849 if let Some(max_len) = attrs.max_length {
850 modifications.push(quote! {
851 if let Schema::Object(ref mut obj) = schema {
852 obj.max_length = Some(#max_len);
853 }
854 });
855 }
856
857 if let Some(ref pattern) = attrs.pattern {
858 modifications.push(quote! {
859 if let Schema::Object(ref mut obj) = schema {
860 obj.pattern = Some(#pattern.to_string());
861 }
862 });
863 }
864
865 if let Some(min_items) = attrs.min_items {
866 modifications.push(quote! {
867 if let Schema::Array(ref mut arr) = schema {
868 arr.min_items = Some(#min_items);
869 }
870 });
871 }
872
873 if let Some(max_items) = attrs.max_items {
874 modifications.push(quote! {
875 if let Schema::Array(ref mut arr) = schema {
876 arr.max_items = Some(#max_items);
877 }
878 });
879 }
880
881 if attrs.unique_items {
882 modifications.push(quote! {
883 if let Schema::Array(ref mut arr) = schema {
884 arr.unique_items = true;
885 }
886 });
887 }
888
889 if attrs.nullable {
890 modifications.push(quote! {
891 use #openapi_crate::utoipa::openapi::schema::{SchemaType, Type};
892 match schema {
893 Schema::Object(ref mut obj) => {
894 let existing_type = std::mem::replace(
896 &mut obj.schema_type,
897 SchemaType::AnyValue,
898 );
899 match existing_type {
900 SchemaType::Type(t) => {
901 obj.schema_type = SchemaType::from_iter([t, Type::Null]);
902 }
903 other => {
904 obj.schema_type = other;
905 }
906 }
907 }
908 Schema::Array(ref mut arr) => {
909 let existing_type = std::mem::replace(
911 &mut arr.schema_type,
912 SchemaType::AnyValue,
913 );
914 match existing_type {
915 SchemaType::Type(t) => {
916 arr.schema_type = SchemaType::from_iter([t, Type::Null]);
917 }
918 other => {
919 arr.schema_type = other;
920 }
921 }
922 }
923 Schema::AllOf(ref mut all_of) => {
924 let existing_type = std::mem::replace(
926 &mut all_of.schema_type,
927 SchemaType::AnyValue,
928 );
929 match existing_type {
930 SchemaType::AnyValue => {
931 all_of.schema_type = SchemaType::from_iter([Type::Object, Type::Null]);
932 }
933 SchemaType::Type(t) => {
934 all_of.schema_type = SchemaType::from_iter([t, Type::Null]);
935 }
936 other => {
937 all_of.schema_type = other;
938 }
939 }
940 }
941 _ => {}
942 }
943 });
944 }
945
946 if let Some(ref default_value) = attrs.default_value {
947 modifications.push(quote! {
948 if let Schema::Object(ref mut obj) = schema {
949 obj.default = Some(serde_json::json!(#default_value));
950 }
951 });
952 }
953
954 if modifications.is_empty() {
955 base_schema
956 } else {
957 quote! {
958 {
959 use #openapi_crate::Schema;
960 let mut schema = #base_schema;
961 #(#modifications)*
962 schema
963 }
964 }
965 }
966}