Skip to content

Commit

Permalink
Derive FromParam for Enum #2826
Browse files Browse the repository at this point in the history
  • Loading branch information
loikki authored and the10thWiz committed Aug 8, 2024
1 parent 509a033 commit 38cebeb
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 0 deletions.
48 changes: 48 additions & 0 deletions core/codegen/src/derive/from_param.rs
Original file line number Diff line number Diff line change
@@ -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<Self, Self::Error> {
match param {
#(#matches)*
_ => Err(#_request::EnumFromParamError::new(
#_Cow::Borrowed(param),
#_Cow::Borrowed(&[#(#names)*]),
)),
}
}
}
}))
.to_tokens()
}
1 change: 1 addition & 0 deletions core/codegen/src/derive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pub mod from_form;
pub mod from_form_field;
pub mod responder;
pub mod uri_display;
pub mod from_param;
33 changes: 33 additions & 0 deletions core/codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions core/codegen/tests/from_param.rs
Original file line number Diff line number Diff line change
@@ -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());
}
1 change: 1 addition & 0 deletions core/codegen/tests/ui-fail-nightly/from_param.rs
53 changes: 53 additions & 0 deletions core/codegen/tests/ui-fail-nightly/from_param.stderr
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions core/codegen/tests/ui-fail-stable/from_param.rs
57 changes: 57 additions & 0 deletions core/codegen/tests/ui-fail-stable/from_param.stderr
Original file line number Diff line number Diff line change
@@ -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)
20 changes: 20 additions & 0 deletions core/codegen/tests/ui-fail/from_param.rs
Original file line number Diff line number Diff line change
@@ -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() {}
29 changes: 29 additions & 0 deletions core/lib/src/request/from_param.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -306,6 +310,31 @@ impl<'a, T: FromParam<'a>> FromParam<'a> for Option<T> {
}
}

/// 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
Expand Down
6 changes: 6 additions & 0 deletions core/lib/src/request/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

0 comments on commit 38cebeb

Please sign in to comment.