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