diff --git a/core/codegen/src/derive/from_param.rs b/core/codegen/src/derive/from_param.rs new file mode 100644 index 00000000..06e498a7 --- /dev/null +++ b/core/codegen/src/derive/from_param.rs @@ -0,0 +1,48 @@ +use crate::exports::*; +use devise::ext::SpanDiagnosticExt; +use devise::Support; +use devise::*; +use proc_macro2::TokenStream; +use quote::quote; +use syn::ext::IdentExt; + +pub fn derive_from_param(input: proc_macro::TokenStream) -> TokenStream { + DeriveGenerator::build_for(input, quote!(impl<'a> #_request::FromParam<'a>)) + .support(Support::Enum) + .validator(ValidatorBuild::new().fields_validate(|_, fields| { + if !fields.is_empty() { + return Err(fields + .span() + .error("Only enums without data fields are supported")); + } + Ok(()) + })) + .inner_mapper(MapperBuild::new().enum_map(|_, data| { + let matches = data.variants().map(|field| { + let field_name = field.ident.unraw(); + quote!( + stringify!(#field_name) => Ok(Self::#field), + ) + }); + let names = data.variants().map(|field| { + let field_name = field.ident.unraw(); + quote!( + #_Cow::Borrowed(stringify!(#field_name)), + ) + }); + + quote! { + type Error = #_request::EnumFromParamError<'a>; + fn from_param(param: &'a str) -> Result { + match param { + #(#matches)* + _ => Err(#_request::EnumFromParamError::new( + #_Cow::Borrowed(param), + #_Cow::Borrowed(&[#(#names)*]), + )), + } + } + } + })) + .to_tokens() +} diff --git a/core/codegen/src/derive/mod.rs b/core/codegen/src/derive/mod.rs index 134279e7..ef77c749 100644 --- a/core/codegen/src/derive/mod.rs +++ b/core/codegen/src/derive/mod.rs @@ -3,3 +3,4 @@ pub mod from_form; pub mod from_form_field; pub mod responder; pub mod uri_display; +pub mod from_param; diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs index 39401f1c..3d6b957c 100644 --- a/core/codegen/src/lib.rs +++ b/core/codegen/src/lib.rs @@ -774,6 +774,39 @@ pub fn derive_from_form(input: TokenStream) -> TokenStream { emit!(derive::from_form::derive_from_form(input)) } +/// Derive for the [`FromParam`] trait. +/// +/// The [`FromParam`] derive can be applied to enums with nullary +/// (zero-length) fields. To implement FromParam, the function matches each variant +/// to its stringified field name (case sensitive): +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// # +/// use rocket::request::FromParam; +/// +/// #[derive(FromParam, Debug, PartialEq)] +/// enum MyParam { +/// A, +/// B, +/// } +/// +/// assert_eq!(MyParam::from_param("A").unwrap(), MyParam::A); +/// assert_eq!(MyParam::from_param("B").unwrap(), MyParam::B); +/// assert!(MyParam::from_param("a").is_err()); +/// assert!(MyParam::from_param("b").is_err()); +/// assert!(MyParam::from_param("c").is_err()); +/// assert!(MyParam::from_param("C").is_err()); +/// ``` +/// +/// Now `MyParam` can be used in an endpoint and will accept either `A` or `B`. +/// [`FromParam`]: ../rocket/request/trait.FromParam.html +/// +#[proc_macro_derive(FromParam)] +pub fn derive_from_param(input: TokenStream) -> TokenStream { + emit!(derive::from_param::derive_from_param(input)) +} + /// Derive for the [`Responder`] trait. /// /// The [`Responder`] derive can be applied to enums and structs with named diff --git a/core/codegen/tests/from_param.rs b/core/codegen/tests/from_param.rs new file mode 100644 index 00000000..a0dc54a5 --- /dev/null +++ b/core/codegen/tests/from_param.rs @@ -0,0 +1,22 @@ +use rocket::request::FromParam; + +#[derive(Debug, FromParam, PartialEq)] +enum Test { + Test1, + Test2, + r#for, +} + +#[test] +fn derive_from_param() { + let test1 = Test::from_param("Test1").expect("Should be valid"); + assert_eq!(test1, Test::Test1); + + let test2 = Test::from_param("Test2").expect("Should be valid"); + assert_eq!(test2, Test::Test2); + let test2 = Test::from_param("for").expect("Should be valid"); + assert_eq!(test2, Test::r#for); + + let test3 = Test::from_param("not_test"); + assert!(test3.is_err()); +} diff --git a/core/codegen/tests/ui-fail-nightly/from_param.rs b/core/codegen/tests/ui-fail-nightly/from_param.rs new file mode 120000 index 00000000..fcbf79fc --- /dev/null +++ b/core/codegen/tests/ui-fail-nightly/from_param.rs @@ -0,0 +1 @@ +../ui-fail/from_param.rs \ No newline at end of file diff --git a/core/codegen/tests/ui-fail-nightly/from_param.stderr b/core/codegen/tests/ui-fail-nightly/from_param.stderr new file mode 100644 index 00000000..697bb4e4 --- /dev/null +++ b/core/codegen/tests/ui-fail-nightly/from_param.stderr @@ -0,0 +1,53 @@ +error: named structs are not supported + --> tests/ui-fail-nightly/from_param.rs:4:1 + | +4 | / struct Foo1 { +5 | | a: String +6 | | } + | |_^ + | +note: error occurred while deriving `FromParam` + --> tests/ui-fail-nightly/from_param.rs:3:10 + | +3 | #[derive(FromParam)] + | ^^^^^^^^^ + = note: this error originates in the derive macro `FromParam` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: named structs are not supported + --> tests/ui-fail-nightly/from_param.rs:9:1 + | +9 | struct Foo2 {} + | ^^^^^^^^^^^^^^ + | +note: error occurred while deriving `FromParam` + --> tests/ui-fail-nightly/from_param.rs:8:10 + | +8 | #[derive(FromParam)] + | ^^^^^^^^^ + = note: this error originates in the derive macro `FromParam` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: Only enums without data fields are supported + --> tests/ui-fail-nightly/from_param.rs:13:6 + | +13 | A(String), + | ^^^^^^^^ + | +note: error occurred while deriving `FromParam` + --> tests/ui-fail-nightly/from_param.rs:11:10 + | +11 | #[derive(FromParam)] + | ^^^^^^^^^ + = note: this error originates in the derive macro `FromParam` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: tuple structs are not supported + --> tests/ui-fail-nightly/from_param.rs:18:1 + | +18 | struct Foo4(usize); + | ^^^^^^^^^^^^^^^^^^^ + | +note: error occurred while deriving `FromParam` + --> tests/ui-fail-nightly/from_param.rs:17:10 + | +17 | #[derive(FromParam)] + | ^^^^^^^^^ + = note: this error originates in the derive macro `FromParam` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/core/codegen/tests/ui-fail-stable/from_param.rs b/core/codegen/tests/ui-fail-stable/from_param.rs new file mode 120000 index 00000000..fcbf79fc --- /dev/null +++ b/core/codegen/tests/ui-fail-stable/from_param.rs @@ -0,0 +1 @@ +../ui-fail/from_param.rs \ No newline at end of file diff --git a/core/codegen/tests/ui-fail-stable/from_param.stderr b/core/codegen/tests/ui-fail-stable/from_param.stderr new file mode 100644 index 00000000..97a9e65d --- /dev/null +++ b/core/codegen/tests/ui-fail-stable/from_param.stderr @@ -0,0 +1,57 @@ +error: named structs are not supported + --> tests/ui-fail-stable/from_param.rs:4:1 + | +4 | / struct Foo1 { +5 | | a: String +6 | | } + | |_^ + +error: [note] error occurred while deriving `FromParam` + --> tests/ui-fail-stable/from_param.rs:3:10 + | +3 | #[derive(FromParam)] + | ^^^^^^^^^ + | + = note: this error originates in the derive macro `FromParam` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: named structs are not supported + --> tests/ui-fail-stable/from_param.rs:9:1 + | +9 | struct Foo2 {} + | ^^^^^^^^^^^^^^ + +error: [note] error occurred while deriving `FromParam` + --> tests/ui-fail-stable/from_param.rs:8:10 + | +8 | #[derive(FromParam)] + | ^^^^^^^^^ + | + = note: this error originates in the derive macro `FromParam` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: Only enums without data fields are supported + --> tests/ui-fail-stable/from_param.rs:13:6 + | +13 | A(String), + | ^^^^^^^^ + +error: [note] error occurred while deriving `FromParam` + --> tests/ui-fail-stable/from_param.rs:11:10 + | +11 | #[derive(FromParam)] + | ^^^^^^^^^ + | + = note: this error originates in the derive macro `FromParam` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: tuple structs are not supported + --> tests/ui-fail-stable/from_param.rs:18:1 + | +18 | struct Foo4(usize); + | ^^^^^^^^^^^^^^^^^^^ + +error: [note] error occurred while deriving `FromParam` + --> tests/ui-fail-stable/from_param.rs:17:10 + | +17 | #[derive(FromParam)] + | ^^^^^^^^^ + | + = note: this error originates in the derive macro `FromParam` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/core/codegen/tests/ui-fail/from_param.rs b/core/codegen/tests/ui-fail/from_param.rs new file mode 100644 index 00000000..45018a38 --- /dev/null +++ b/core/codegen/tests/ui-fail/from_param.rs @@ -0,0 +1,20 @@ +use rocket::request::FromParam; + +#[derive(FromParam)] +struct Foo1 { + a: String +} + +#[derive(FromParam)] +struct Foo2 {} + +#[derive(FromParam)] +enum Foo3 { + A(String), + B(String) +} + +#[derive(FromParam)] +struct Foo4(usize); + +fn main() {} diff --git a/core/lib/src/request/from_param.rs b/core/lib/src/request/from_param.rs index 9df98422..efecac96 100644 --- a/core/lib/src/request/from_param.rs +++ b/core/lib/src/request/from_param.rs @@ -1,6 +1,10 @@ +use std::borrow::Cow; +use std::fmt; use std::str::FromStr; use std::path::PathBuf; +use cookie::Display; + use crate::error::Empty; use crate::either::Either; use crate::http::uri::{Segments, error::PathError, fmt::Path}; @@ -306,6 +310,31 @@ impl<'a, T: FromParam<'a>> FromParam<'a> for Option { } } +/// Error type for automatically derived `FromParam` enums +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct EnumFromParamError<'a> { + pub value: Cow<'a, str>, + pub options: Cow<'static, [Cow<'static, str>]>, +} + +impl<'a> EnumFromParamError<'a> { + pub fn new(value: Cow<'a, str>, options: Cow<'static, [Cow<'static, str>]>) -> Self { + Self { + value, + options, + } + } +} + +impl fmt::Display for EnumFromParamError<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Unexpected value {:?}, expected one of {:?}", self.value, self.options) + } +} + +impl std::error::Error for EnumFromParamError<'_> {} + /// Trait to convert _many_ dynamic path segment strings to a concrete value. /// /// This is the `..` analog to [`FromParam`], and its functionality is identical diff --git a/core/lib/src/request/mod.rs b/core/lib/src/request/mod.rs index 0393f96b..47ffaf20 100644 --- a/core/lib/src/request/mod.rs +++ b/core/lib/src/request/mod.rs @@ -12,6 +12,12 @@ pub use self::request::Request; pub use self::from_request::{FromRequest, Outcome}; pub use self::from_param::{FromParam, FromSegments}; +#[doc(hidden)] +pub use self::from_param::EnumFromParamError; + +#[doc(hidden)] +pub use rocket_codegen::FromParam; + #[doc(inline)] pub use crate::response::flash::FlashMessage;