1use proc_macro::TokenStream;
58use quote::quote;
59use syn::{
60 parse::{Parse, ParseStream},
61 parse_macro_input, Data, DeriveInput, Fields, Ident, ItemFn, Lit, LitInt, LitStr, Token,
62};
63
64mod handler;
65
66fn annotate(method: &str, path: LitStr, func: ItemFn) -> TokenStream {
69 handler::expand(method, path, func).into()
70}
71
72struct RouteArgs {
75 method: Ident,
76 path: LitStr,
77}
78
79impl Parse for RouteArgs {
80 fn parse(input: ParseStream) -> syn::Result<Self> {
81 let method: Ident = input.parse()?;
82 input.parse::<Token![,]>()?;
83 let path: LitStr = input.parse()?;
84 Ok(RouteArgs { method, path })
85 }
86}
87
88#[proc_macro_attribute]
120pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
121 let RouteArgs { method, path } = parse_macro_input!(args as RouteArgs);
122 let func = parse_macro_input!(input as ItemFn);
123 annotate(&method.to_string(), path, func)
124}
125
126#[proc_macro_attribute]
145pub fn get(args: TokenStream, input: TokenStream) -> TokenStream {
146 let path = parse_macro_input!(args as LitStr);
147 let func = parse_macro_input!(input as ItemFn);
148 annotate("GET", path, func)
149}
150
151#[proc_macro_attribute]
153pub fn post(args: TokenStream, input: TokenStream) -> TokenStream {
154 let path = parse_macro_input!(args as LitStr);
155 let func = parse_macro_input!(input as ItemFn);
156 annotate("POST", path, func)
157}
158
159#[proc_macro_attribute]
161pub fn put(args: TokenStream, input: TokenStream) -> TokenStream {
162 let path = parse_macro_input!(args as LitStr);
163 let func = parse_macro_input!(input as ItemFn);
164 annotate("PUT", path, func)
165}
166
167#[proc_macro_attribute]
169pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream {
170 let path = parse_macro_input!(args as LitStr);
171 let func = parse_macro_input!(input as ItemFn);
172 annotate("PATCH", path, func)
173}
174
175#[proc_macro_attribute]
177pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream {
178 let path = parse_macro_input!(args as LitStr);
179 let func = parse_macro_input!(input as ItemFn);
180 annotate("DELETE", path, func)
181}
182
183#[proc_macro_derive(FromPath)]
198pub fn derive_from_path(input: TokenStream) -> TokenStream {
199 let ast = parse_macro_input!(input as DeriveInput);
200 handler::derive_from_path(ast)
201 .unwrap_or_else(|e| e.to_compile_error())
202 .into()
203}
204
205#[proc_macro_derive(FromRequest)]
224pub fn derive_from_request(input: TokenStream) -> TokenStream {
225 let ast = parse_macro_input!(input as DeriveInput);
226 impl_from_request(ast)
227 .unwrap_or_else(|e| e.to_compile_error())
228 .into()
229}
230
231#[proc_macro_derive(Validate, attributes(validate))]
266pub fn derive_validate(input: TokenStream) -> TokenStream {
267 let ast = parse_macro_input!(input as DeriveInput);
268 impl_validate(ast)
269 .unwrap_or_else(|e| e.to_compile_error())
270 .into()
271}
272
273fn impl_validate(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
274 let span = input.ident.span();
275 let name = &input.ident;
276
277 let fields = match &input.data {
278 Data::Struct(s) => match &s.fields {
279 Fields::Named(f) => &f.named,
280 _ => {
281 return Err(syn::Error::new(
282 span,
283 "#[derive(Validate)] only supports structs with named fields",
284 ))
285 }
286 },
287 _ => {
288 return Err(syn::Error::new(
289 span,
290 "#[derive(Validate)] can only be derived on structs",
291 ))
292 }
293 };
294
295 let mut all_checks: Vec<proc_macro2::TokenStream> = Vec::new();
296
297 for field in fields {
298 let field_ident = field.ident.as_ref().unwrap();
299 let field_name = field_ident.to_string();
300 for attr in &field.attrs {
301 if attr.path().is_ident("validate") {
302 let checks = generate_field_checks(attr, field_ident, &field_name)?;
303 all_checks.extend(checks);
304 }
305 }
306 }
307
308 Ok(quote! {
309 const _: () = {
310 use ::rust_web_server as _rws;
311 impl _rws::validate::Validate for #name {
312 fn validate(&self) -> ::core::result::Result<(), _rws::validate::ValidationErrors> {
313 let mut __errors = _rws::validate::ValidationErrors::new();
314 #(#all_checks)*
315 if __errors.is_empty() {
316 ::core::result::Result::Ok(())
317 } else {
318 ::core::result::Result::Err(__errors)
319 }
320 }
321 }
322 };
323 })
324}
325
326fn generate_field_checks(
327 attr: &syn::Attribute,
328 field_ident: &syn::Ident,
329 field_name: &str,
330) -> syn::Result<Vec<proc_macro2::TokenStream>> {
331 let mut checks: Vec<proc_macro2::TokenStream> = Vec::new();
332
333 attr.parse_nested_meta(|meta| {
334 if meta.path.is_ident("email") {
335 let msg = format!("{field_name} must be a valid email address");
336 checks.push(quote! {
337 if !_rws::validate::is_email(&self.#field_ident) {
338 __errors.add(#field_name, #msg);
339 }
340 });
341 } else if meta.path.is_ident("required") {
342 let msg = format!("{field_name} must not be empty");
343 checks.push(quote! {
344 if self.#field_ident.is_empty() {
345 __errors.add(#field_name, #msg);
346 }
347 });
348 } else if meta.path.is_ident("url") {
349 let msg = format!("{field_name} must be a valid URL (http:// or https://)");
350 checks.push(quote! {
351 if !_rws::validate::is_url(&self.#field_ident) {
352 __errors.add(#field_name, #msg);
353 }
354 });
355 } else if meta.path.is_ident("length") {
356 let mut min: Option<u64> = None;
357 let mut max: Option<u64> = None;
358 meta.parse_nested_meta(|inner| {
359 if inner.path.is_ident("min") {
360 let lit: LitInt = inner.value()?.parse()?;
361 min = Some(lit.base10_parse()?);
362 } else if inner.path.is_ident("max") {
363 let lit: LitInt = inner.value()?.parse()?;
364 max = Some(lit.base10_parse()?);
365 } else {
366 return Err(inner.error("expected `min` or `max`"));
367 }
368 Ok(())
369 })?;
370
371 let mut len_checks: Vec<proc_macro2::TokenStream> = Vec::new();
372 if let Some(n) = min {
373 let msg = format!("{field_name} must be at least {n} character(s) long");
374 let n_lit = proc_macro2::Literal::usize_suffixed(n as usize);
375 len_checks.push(quote! {
376 if __len < #n_lit { __errors.add(#field_name, #msg); }
377 });
378 }
379 if let Some(n) = max {
380 let msg = format!("{field_name} must be at most {n} character(s) long");
381 let n_lit = proc_macro2::Literal::usize_suffixed(n as usize);
382 len_checks.push(quote! {
383 if __len > #n_lit { __errors.add(#field_name, #msg); }
384 });
385 }
386 checks.push(quote! {
387 {
388 let __len = self.#field_ident.chars().count();
389 #(#len_checks)*
390 }
391 });
392 } else if meta.path.is_ident("range") {
393 let mut min: Option<f64> = None;
394 let mut max: Option<f64> = None;
395 meta.parse_nested_meta(|inner| {
396 if inner.path.is_ident("min") {
397 min = Some(lit_to_f64(&inner.value()?.parse::<Lit>()?)?);
398 } else if inner.path.is_ident("max") {
399 max = Some(lit_to_f64(&inner.value()?.parse::<Lit>()?)?);
400 } else {
401 return Err(inner.error("expected `min` or `max`"));
402 }
403 Ok(())
404 })?;
405
406 let mut rng_checks: Vec<proc_macro2::TokenStream> = Vec::new();
407 if let Some(n) = min {
408 let msg = format!("{field_name} must be at least {n}");
409 let n_lit = proc_macro2::Literal::f64_suffixed(n);
410 rng_checks.push(quote! {
411 if __val < #n_lit { __errors.add(#field_name, #msg); }
412 });
413 }
414 if let Some(n) = max {
415 let msg = format!("{field_name} must be at most {n}");
416 let n_lit = proc_macro2::Literal::f64_suffixed(n);
417 rng_checks.push(quote! {
418 if __val > #n_lit { __errors.add(#field_name, #msg); }
419 });
420 }
421 checks.push(quote! {
422 {
423 let __val = self.#field_ident as f64;
424 #(#rng_checks)*
425 }
426 });
427 } else {
428 return Err(meta.error(
429 "unknown validator; expected: email, required, url, length, range",
430 ));
431 }
432 Ok(())
433 })?;
434
435 Ok(checks)
436}
437
438fn lit_to_f64(lit: &Lit) -> syn::Result<f64> {
439 match lit {
440 Lit::Float(f) => Ok(f.base10_parse()?),
441 Lit::Int(i) => Ok(i.base10_parse::<i64>()? as f64),
442 _ => Err(syn::Error::new_spanned(lit, "expected a numeric literal")),
443 }
444}
445
446fn impl_from_request(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
447 let span = input.ident.span();
448 let name = &input.ident;
449
450 let fields = match &input.data {
451 Data::Struct(s) => match &s.fields {
452 Fields::Named(f) => &f.named,
453 _ => {
454 return Err(syn::Error::new(
455 span,
456 "#[derive(FromRequest)] only supports structs with named fields",
457 ))
458 }
459 },
460 _ => {
461 return Err(syn::Error::new(
462 span,
463 "#[derive(FromRequest)] can only be derived on structs",
464 ))
465 }
466 };
467
468 let extractions = fields.iter().map(|f| {
469 let ident = f.ident.as_ref().unwrap();
470 let ty = &f.ty;
471 quote! {
472 let #ident = <#ty as _rws::extract::FromRequest>::from_request(__req)?;
473 }
474 });
475
476 let field_names = fields.iter().map(|f| f.ident.as_ref().unwrap());
477
478 Ok(quote! {
479 const _: () = {
480 use ::rust_web_server as _rws;
481 impl _rws::extract::FromRequest for #name {
482 fn from_request(__req: &_rws::request::Request) -> ::core::result::Result<Self, _rws::response::Response> {
483 #(#extractions)*
484 ::core::result::Result::Ok(#name { #(#field_names),* })
485 }
486 }
487 };
488 })
489}
490
491#[proc_macro_derive(Config, attributes(config))]
537pub fn derive_config(input: TokenStream) -> TokenStream {
538 let ast = parse_macro_input!(input as DeriveInput);
539 impl_config(ast)
540 .unwrap_or_else(|e| e.to_compile_error())
541 .into()
542}
543
544fn impl_config(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
545 let span = input.ident.span();
546 let name = &input.ident;
547
548 let prefix = parse_config_prefix(&input.attrs)?;
550
551 let fields = match &input.data {
552 Data::Struct(s) => match &s.fields {
553 Fields::Named(f) => &f.named,
554 _ => {
555 return Err(syn::Error::new(
556 span,
557 "#[derive(Config)] only supports structs with named fields",
558 ))
559 }
560 },
561 _ => {
562 return Err(syn::Error::new(
563 span,
564 "#[derive(Config)] can only be derived on structs",
565 ))
566 }
567 };
568
569 let mut field_loads: Vec<proc_macro2::TokenStream> = Vec::new();
570 let mut field_names: Vec<&syn::Ident> = Vec::new();
571
572 for field in fields {
573 let ident = field.ident.as_ref().unwrap();
574 field_names.push(ident);
575
576 let (env_key, default) = parse_field_config(&field.attrs, ident, &prefix)?;
578
579 let is_option = is_option_type(&field.ty);
580
581 let load_expr = if is_option {
582 quote! {
583 _rws::config_binding::load_optional(&#env_key)?
584 }
585 } else if let Some(default_str) = default {
586 quote! {
587 _rws::config_binding::load_with_default(&#env_key, #default_str)?
588 }
589 } else {
590 quote! {
591 _rws::config_binding::load_required(&#env_key)?
592 }
593 };
594
595 field_loads.push(quote! {
596 let #ident = #load_expr;
597 });
598 }
599
600 Ok(quote! {
601 impl #name {
602 pub fn load() -> ::core::result::Result<Self, ::std::string::String> {
604 use ::rust_web_server as _rws;
605 #(#field_loads)*
606 ::core::result::Result::Ok(#name {
607 #(#field_names),*
608 })
609 }
610 }
611 })
612}
613
614fn parse_field_config(
617 attrs: &[syn::Attribute],
618 ident: &syn::Ident,
619 prefix: &str,
620) -> syn::Result<(proc_macro2::TokenStream, Option<LitStr>)> {
621 let mut env_name: Option<String> = None;
622 let mut default: Option<LitStr> = None;
623
624 for attr in attrs {
625 if !attr.path().is_ident("config") {
626 continue;
627 }
628 attr.parse_nested_meta(|meta| {
629 if meta.path.is_ident("env") {
630 let lit: LitStr = meta.value()?.parse()?;
631 env_name = Some(lit.value());
632 } else if meta.path.is_ident("default") {
633 default = Some(meta.value()?.parse()?);
634 } else {
635 return Err(meta.error("unknown config key; expected `env` or `default`"));
636 }
637 Ok(())
638 })?;
639 }
640
641 let key = format!(
642 "{}{}",
643 prefix,
644 env_name.unwrap_or_else(|| ident.to_string().to_uppercase())
645 );
646
647 Ok((quote! { #key }, default))
648}
649
650fn parse_config_prefix(attrs: &[syn::Attribute]) -> syn::Result<String> {
652 let mut prefix = String::new();
653 for attr in attrs {
654 if !attr.path().is_ident("config") {
655 continue;
656 }
657 attr.parse_nested_meta(|meta| {
658 if meta.path.is_ident("prefix") {
659 let lit: LitStr = meta.value()?.parse()?;
660 prefix = lit.value();
661 } else {
662 return Err(meta.error("unknown struct config key; expected `prefix`"));
663 }
664 Ok(())
665 })?;
666 }
667 Ok(prefix)
668}
669
670fn is_option_type(ty: &syn::Type) -> bool {
672 if let syn::Type::Path(tp) = ty {
673 if let Some(seg) = tp.path.segments.last() {
674 return seg.ident == "Option";
675 }
676 }
677 false
678}
679
680#[proc_macro_derive(Model, attributes(table, column, primary_key))]
716pub fn derive_model(input: TokenStream) -> TokenStream {
717 let ast = parse_macro_input!(input as DeriveInput);
718 impl_model(ast)
719 .unwrap_or_else(|e| e.to_compile_error())
720 .into()
721}
722
723fn impl_model(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
724 let span = input.ident.span();
725 let struct_name = &input.ident;
726
727 let table_name = parse_table_name(&input.attrs, struct_name)?;
729
730 let fields = match &input.data {
731 Data::Struct(s) => match &s.fields {
732 Fields::Named(f) => &f.named,
733 _ => {
734 return Err(syn::Error::new(
735 span,
736 "#[derive(Model)] only supports structs with named fields",
737 ))
738 }
739 },
740 _ => {
741 return Err(syn::Error::new(
742 span,
743 "#[derive(Model)] can only be derived on structs",
744 ))
745 }
746 };
747
748 struct FieldMeta {
750 field_ident: syn::Ident,
751 col_name: String,
752 #[allow(dead_code)]
753 is_pk: bool,
754 is_auto_increment: bool,
755 is_ignored: bool,
756 }
757
758 let mut pk_field: Option<FieldMeta> = None;
759 let mut regular_fields: Vec<FieldMeta> = Vec::new();
760
761 for field in fields {
762 let ident = field.ident.as_ref().unwrap().clone();
763 let mut col_name = ident.to_string();
764 let mut is_pk = false;
765 let mut is_auto_increment = false;
766 let mut is_ignored = false;
767
768 for attr in &field.attrs {
769 if attr.path().is_ident("ignore") {
770 is_ignored = true;
771 } else if attr.path().is_ident("primary_key") {
772 is_pk = true;
773 let _ = attr.parse_nested_meta(|meta| {
775 if meta.path.is_ident("auto_increment") {
776 is_auto_increment = true;
777 }
778 Ok(())
779 });
780 } else if attr.path().is_ident("column") {
781 let _ = attr.parse_nested_meta(|meta| {
782 if meta.path.is_ident("name") {
783 let lit: LitStr = meta.value()?.parse()?;
784 col_name = lit.value();
785 }
786 Ok(())
788 });
789 }
790 }
791
792 let meta = FieldMeta {
793 field_ident: ident,
794 col_name,
795 is_pk,
796 is_auto_increment,
797 is_ignored,
798 };
799
800 if is_pk {
801 pk_field = Some(meta);
802 } else {
803 regular_fields.push(meta);
804 }
805 }
806
807 let pk = pk_field.ok_or_else(|| {
808 syn::Error::new(span, "#[derive(Model)] requires exactly one #[primary_key] field")
809 })?;
810
811 let mut all_col_names: Vec<String> = Vec::new();
813 all_col_names.push(pk.col_name.clone());
814 for f in regular_fields.iter().filter(|f| !f.is_ignored) {
815 all_col_names.push(f.col_name.clone());
816 }
817
818 let pk_col_name = pk.col_name.clone();
819 let pk_field_ident = &pk.field_ident;
820 let auto_inc = pk.is_auto_increment;
821
822 let pk_from_row = {
824 let col = &pk.col_name;
825 let fident = &pk.field_ident;
826 quote! { #fident: row.get(#col)? }
827 };
828
829 let regular_from_row: Vec<proc_macro2::TokenStream> = fields
830 .iter()
831 .filter(|f| {
832 let ident_str = f.ident.as_ref().unwrap().to_string();
833 let is_pk_field = ident_str == pk_field_ident.to_string();
835 !is_pk_field
836 })
837 .map(|f| {
838 let fident = f.ident.as_ref().unwrap();
839 let is_ignored = f.attrs.iter().any(|a| a.path().is_ident("ignore"));
841 if is_ignored {
842 quote! { #fident: ::core::default::Default::default() }
843 } else {
844 let mut col = fident.to_string();
846 for attr in &f.attrs {
847 if attr.path().is_ident("column") {
848 let _ = attr.parse_nested_meta(|meta| {
849 if meta.path.is_ident("name") {
850 if let Ok(lit) = meta.value()?.parse::<LitStr>() {
851 col = lit.value();
852 }
853 }
854 Ok(())
855 });
856 }
857 }
858 quote! { #fident: row.get(#col)? }
859 }
860 })
861 .collect();
862
863 let pk_to_values = {
865 let col = &pk.col_name;
866 quote! {
867 (#col, ::rust_web_server::model::ToColumn::to_column(&self.#pk_field_ident))
868 }
869 };
870
871 let regular_to_values: Vec<proc_macro2::TokenStream> = fields
872 .iter()
873 .filter(|f| {
874 let ident_str = f.ident.as_ref().unwrap().to_string();
875 let is_pk_field = ident_str == pk_field_ident.to_string();
876 let is_ignored = f.attrs.iter().any(|a| a.path().is_ident("ignore"));
877 !is_pk_field && !is_ignored
878 })
879 .map(|f| {
880 let fident = f.ident.as_ref().unwrap();
881 let mut col = fident.to_string();
882 for attr in &f.attrs {
883 if attr.path().is_ident("column") {
884 let _ = attr.parse_nested_meta(|meta| {
885 if meta.path.is_ident("name") {
886 if let Ok(lit) = meta.value()?.parse::<LitStr>() {
887 col = lit.value();
888 }
889 }
890 Ok(())
891 });
892 }
893 }
894 quote! {
895 (#col, ::rust_web_server::model::ToColumn::to_column(&self.#fident))
896 }
897 })
898 .collect();
899
900 Ok(quote! {
902 impl ::rust_web_server::model::Model for #struct_name {
903 fn table_name() -> &'static str {
904 #table_name
905 }
906
907 fn column_names() -> &'static [&'static str] {
908 &[#(#all_col_names),*]
909 }
910
911 fn primary_key_name() -> &'static str {
912 #pk_col_name
913 }
914
915 fn primary_key_value(&self) -> ::rust_web_server::model::Value {
916 ::rust_web_server::model::ToColumn::to_column(&self.#pk_field_ident)
917 }
918
919 fn primary_key_auto_increment() -> bool {
920 #auto_inc
921 }
922
923 fn from_row(row: &::rust_web_server::model::ModelRow) -> ::core::result::Result<Self, ::rust_web_server::model::DbError> {
924 ::core::result::Result::Ok(#struct_name {
925 #pk_from_row,
926 #(#regular_from_row),*
927 })
928 }
929
930 fn to_values(&self) -> ::std::vec::Vec<(&'static str, ::rust_web_server::model::Value)> {
931 vec![
932 #pk_to_values,
933 #(#regular_to_values),*
934 ]
935 }
936 }
937
938 impl #struct_name {
939 pub fn repository(pool: &::rust_web_server::model::DbPool) -> ::rust_web_server::model::ModelRepository<#struct_name, i64> {
941 ::rust_web_server::model::ModelRepository::new(pool)
942 }
943
944 pub fn query(pool: &::rust_web_server::model::DbPool) -> ::rust_web_server::model::QueryBuilder<#struct_name> {
946 ::rust_web_server::model::QueryBuilder::new(pool)
947 }
948 }
949 })
950}
951
952fn parse_table_name(
953 attrs: &[syn::Attribute],
954 struct_name: &syn::Ident,
955) -> syn::Result<String> {
956 for attr in attrs {
957 if attr.path().is_ident("table") {
958 let mut name: Option<String> = None;
959 attr.parse_nested_meta(|meta| {
960 if meta.path.is_ident("name") {
961 let lit: LitStr = meta.value()?.parse()?;
962 name = Some(lit.value());
963 }
964 Ok(())
965 })?;
966 if let Some(n) = name {
967 return Ok(n);
968 }
969 }
970 }
971 Ok(struct_name.to_string().to_lowercase())
973}
974
975#[proc_macro_derive(Factory)]
1000pub fn derive_factory(input: TokenStream) -> TokenStream {
1001 let ast = parse_macro_input!(input as DeriveInput);
1002 impl_factory(ast)
1003 .unwrap_or_else(|e| e.to_compile_error())
1004 .into()
1005}
1006
1007fn impl_factory(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1008 let span = input.ident.span();
1009 let struct_name = &input.ident;
1010 if !input.generics.params.is_empty() {
1011 return Err(syn::Error::new(span, "#[derive(Factory)] doesn't support generic structs"));
1012 }
1013 let fields = match &input.data {
1014 Data::Struct(s) => match &s.fields {
1015 Fields::Named(f) => &f.named,
1016 _ => return Err(syn::Error::new(span, "#[derive(Factory)] only supports structs with named fields")),
1017 },
1018 _ => return Err(syn::Error::new(span, "#[derive(Factory)] can only be derived on structs")),
1019 };
1020
1021 let mut has_pk = false;
1022 let mut inits: Vec<proc_macro2::TokenStream> = Vec::new();
1023 for field in fields {
1024 let ident = field.ident.as_ref().unwrap();
1025 let ty = &field.ty;
1026 let name = ident.to_string();
1027 let name = name.strip_prefix("r#").unwrap_or(&name).to_string();
1028 let mut is_pk = false;
1029 let mut auto_increment = false;
1030 let mut ignored = false;
1031 for attr in &field.attrs {
1032 if attr.path().is_ident("ignore") {
1033 ignored = true;
1034 } else if attr.path().is_ident("primary_key") {
1035 is_pk = true;
1036 let _ = attr.parse_nested_meta(|meta| {
1037 if meta.path.is_ident("auto_increment") {
1038 auto_increment = true;
1039 }
1040 Ok(())
1041 });
1042 }
1043 }
1044 has_pk |= is_pk;
1045 let probe = quote! { (&&__Probe::<#ty>(::core::marker::PhantomData)) };
1046 inits.push(if ignored {
1047 quote! { #ident: ::core::default::Default::default() }
1048 } else if is_pk && !auto_increment {
1049 quote! { #ident: #probe.make_key(#name, __seq) }
1050 } else {
1051 quote! { #ident: #probe.make(#name, __seq) }
1052 });
1053 }
1054 if !has_pk {
1055 return Err(syn::Error::new(span, "#[derive(Factory)] needs the #[primary_key] field of #[derive(Model)]"));
1056 }
1057
1058 Ok(quote! {
1059 impl ::rust_web_server::test_client::factory::Factory for #struct_name {
1060 fn factory_default(__seq: u64) -> Self {
1061 #[allow(unused_imports)]
1062 use ::rust_web_server::test_client::factory::__private::{
1063 Probe as __Probe, ViaDefault as _, ViaValue as _,
1064 };
1065 #struct_name {
1066 #(#inits),*
1067 }
1068 }
1069 }
1070
1071 impl #struct_name {
1072 pub fn factory() -> ::rust_web_server::test_client::factory::ModelFactory<#struct_name> {
1074 ::rust_web_server::test_client::factory::ModelFactory::new()
1075 }
1076 }
1077 })
1078}
1079
1080#[proc_macro_derive(JsonSchema, attributes(serde))]
1106pub fn derive_json_schema(input: TokenStream) -> TokenStream {
1107 let ast = parse_macro_input!(input as DeriveInput);
1108 impl_json_schema(ast)
1109 .unwrap_or_else(|e| e.to_compile_error())
1110 .into()
1111}
1112
1113fn impl_json_schema(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1114 let name = &input.ident;
1115 let container = SerdeAttrs::parse(&input.attrs)?;
1116 let description = doc_comment(&input.attrs);
1117 let body = match &input.data {
1118 Data::Struct(s) => {
1119 let Fields::Named(fields) = &s.fields else {
1120 return Err(syn::Error::new_spanned(
1121 name,
1122 "#[derive(JsonSchema)] supports structs with named fields and enums of unit variants",
1123 ));
1124 };
1125 let mut properties = Vec::new();
1126 for field in &fields.named {
1127 let attrs = SerdeAttrs::parse(&field.attrs)?;
1128 if attrs.skip {
1129 continue;
1130 }
1131 let ident = field.ident.as_ref().expect("named field");
1132 let json_name = attrs.rename.clone().unwrap_or_else(|| {
1133 rename_with(ident.to_string().trim_start_matches("r#"), container.rename_all.as_deref(), false)
1134 });
1135 let ty = &field.ty;
1136 let field_doc = doc_comment(&field.attrs);
1137 let may_be_absent = attrs.default || container.default || attrs.skip_serializing_if;
1138 properties.push(quote! {
1139 __schema.property(
1140 #json_name,
1141 &_rws::schema::nested::<#ty>(),
1142 #field_doc,
1143 !#may_be_absent && !<#ty as _rws::schema::JsonSchema>::is_optional(),
1144 );
1145 });
1146 }
1147 quote! {
1148 let mut __schema = _rws::schema::ObjectSchema::new(#description);
1149 #(#properties)*
1150 __schema.finish()
1151 }
1152 }
1153 Data::Enum(e) => {
1154 let mut values = Vec::new();
1155 for variant in &e.variants {
1156 if !matches!(variant.fields, Fields::Unit) {
1157 return Err(syn::Error::new_spanned(
1158 variant,
1159 "#[derive(JsonSchema)] supports only unit enum variants",
1160 ));
1161 }
1162 let attrs = SerdeAttrs::parse(&variant.attrs)?;
1163 if attrs.skip {
1164 continue;
1165 }
1166 values.push(attrs.rename.unwrap_or_else(|| {
1167 rename_with(&variant.ident.to_string(), container.rename_all.as_deref(), true)
1168 }));
1169 }
1170 quote! { _rws::schema::string_enum(#description, &[#(#values),*]) }
1171 }
1172 Data::Union(_) => {
1173 return Err(syn::Error::new_spanned(name, "#[derive(JsonSchema)] does not support unions"));
1174 }
1175 };
1176
1177 let mut generics = input.generics.clone();
1178 for param in generics.type_params_mut() {
1179 param.bounds.push(syn::parse_quote!(::rust_web_server::schema::JsonSchema));
1180 }
1181 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1182 Ok(quote! {
1183 const _: () = {
1184 use ::rust_web_server as _rws;
1185 impl #impl_generics _rws::schema::JsonSchema for #name #ty_generics #where_clause {
1186 fn json_schema() -> ::std::string::String {
1187 #body
1188 }
1189 }
1190 };
1191 })
1192}
1193
1194#[derive(Default)]
1196struct SerdeAttrs {
1197 rename: Option<String>,
1198 rename_all: Option<String>,
1199 default: bool,
1200 skip: bool,
1201 skip_serializing_if: bool,
1202}
1203
1204impl SerdeAttrs {
1205 fn parse(attrs: &[syn::Attribute]) -> syn::Result<Self> {
1206 let mut out = SerdeAttrs::default();
1207 for attr in attrs.iter().filter(|a| a.path().is_ident("serde")) {
1208 attr.parse_nested_meta(|meta| {
1209 let key = meta.path.get_ident().map(|i| i.to_string()).unwrap_or_default();
1210 match key.as_str() {
1211 "rename" | "rename_all" => {
1212 if !meta.input.peek(Token![=]) {
1213 return Err(meta.error(format!(
1214 "#[derive(JsonSchema)] needs one `{key}` for both directions (one schema describes input and output)"
1215 )));
1216 }
1217 let value = meta.value()?.parse::<LitStr>()?.value();
1218 if key == "rename" {
1219 out.rename = Some(value);
1220 } else {
1221 if rename_with("a_b", Some(&value), false).is_empty() {
1222 return Err(meta.error(format!("unknown rename_all rule \"{value}\"")));
1223 }
1224 out.rename_all = Some(value);
1225 }
1226 }
1227 "default" => {
1228 out.default = true;
1229 if meta.input.peek(Token![=]) {
1230 meta.value()?.parse::<LitStr>()?;
1231 }
1232 }
1233 "skip" | "skip_serializing" | "skip_deserializing" => out.skip = true,
1234 "skip_serializing_if" => {
1235 out.skip_serializing_if = true;
1236 meta.value()?.parse::<LitStr>()?;
1237 }
1238 "flatten" | "tag" | "content" | "untagged" | "transparent" | "with" | "serialize_with"
1239 | "deserialize_with" | "remote" | "from" | "try_from" | "into" | "getter" => {
1240 return Err(meta.error(format!(
1241 "#[derive(JsonSchema)] does not support #[serde({key})]: the schema would not match the JSON"
1242 )));
1243 }
1244 _ => {
1245 if meta.input.peek(Token![=]) {
1249 meta.value()?.parse::<proc_macro2::TokenStream>()?;
1250 } else if meta.input.peek(syn::token::Paren) {
1251 meta.parse_nested_meta(|nested| {
1252 if nested.input.peek(Token![=]) {
1253 nested.value()?.parse::<proc_macro2::TokenTree>()?;
1254 }
1255 Ok(())
1256 })?;
1257 }
1258 }
1259 }
1260 Ok(())
1261 })?;
1262 }
1263 Ok(out)
1264 }
1265}
1266
1267fn rename_with(name: &str, rule: Option<&str>, variant: bool) -> String {
1271 let Some(rule) = rule else { return name.to_string() };
1272 let words: Vec<String> = if variant {
1274 let mut words = Vec::new();
1275 let mut current = String::new();
1276 for c in name.chars() {
1277 if c.is_uppercase() && !current.is_empty() {
1278 words.push(std::mem::take(&mut current));
1279 }
1280 current.extend(c.to_lowercase());
1281 }
1282 if !current.is_empty() {
1283 words.push(current);
1284 }
1285 words
1286 } else {
1287 name.split('_').map(str::to_string).collect()
1288 };
1289 let capitalize = |w: &String| {
1290 let mut chars = w.chars();
1291 chars.next().map(|c| c.to_uppercase().chain(chars).collect::<String>()).unwrap_or_default()
1292 };
1293 match rule {
1294 "lowercase" => words.concat(),
1295 "UPPERCASE" => words.concat().to_uppercase(),
1296 "PascalCase" => words.iter().map(capitalize).collect(),
1297 "camelCase" => {
1298 let pascal: String = words.iter().map(capitalize).collect();
1299 let mut chars = pascal.chars();
1300 chars.next().map(|c| c.to_lowercase().chain(chars).collect()).unwrap_or_default()
1301 }
1302 "snake_case" => words.join("_"),
1303 "SCREAMING_SNAKE_CASE" => words.join("_").to_uppercase(),
1304 "kebab-case" => words.join("-"),
1305 "SCREAMING-KEBAB-CASE" => words.join("-").to_uppercase(),
1306 _ => String::new(),
1307 }
1308}
1309
1310fn doc_comment(attrs: &[syn::Attribute]) -> String {
1312 let mut lines = Vec::new();
1313 for attr in attrs.iter().filter(|a| a.path().is_ident("doc")) {
1314 if let syn::Meta::NameValue(nv) = &attr.meta {
1315 if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value {
1316 let line = s.value().trim().to_string();
1317 if !line.is_empty() {
1318 lines.push(line);
1319 }
1320 }
1321 }
1322 }
1323 lines.join(" ")
1324}
1325
1326#[cfg(test)]
1327mod tests {
1328 use super::rename_with;
1329
1330 #[test]
1331 fn rename_all_rules_match_serde() {
1332 let field = |rule| rename_with("line_item_id", Some(rule), false);
1333 assert_eq!("lineitemid", field("lowercase"));
1334 assert_eq!("LINEITEMID", field("UPPERCASE"));
1335 assert_eq!("LineItemId", field("PascalCase"));
1336 assert_eq!("lineItemId", field("camelCase"));
1337 assert_eq!("line_item_id", field("snake_case"));
1338 assert_eq!("LINE_ITEM_ID", field("SCREAMING_SNAKE_CASE"));
1339 assert_eq!("line-item-id", field("kebab-case"));
1340 assert_eq!("LINE-ITEM-ID", field("SCREAMING-KEBAB-CASE"));
1341 let variant = |rule| rename_with("BackOrdered", Some(rule), true);
1342 assert_eq!("backordered", variant("lowercase"));
1343 assert_eq!("backOrdered", variant("camelCase"));
1344 assert_eq!("back_ordered", variant("snake_case"));
1345 assert_eq!("BACK-ORDERED", variant("SCREAMING-KEBAB-CASE"));
1346 assert_eq!("BackOrdered", variant("PascalCase"));
1347 assert_eq!("as_is", rename_with("as_is", None, false));
1348 assert_eq!("", rename_with("a_b", Some("Train-Case"), false));
1349 }
1350}