Improve `FromParam` derive docs and error values.

This commit improves the docs for the `FromParam` derive macro and
exposes a new `InvalidOption` error value, which is returned when the
derived `FromParam` implementation fails.
This commit is contained in:
Sergio Benitez 2024-08-09 18:44:50 -07:00
parent 15062ded09
commit 1f82d4bbcd
8 changed files with 106 additions and 72 deletions

View File

@ -1,45 +1,40 @@
use crate::exports::*;
use devise::ext::SpanDiagnosticExt;
use devise::Support;
use devise::*; use devise::*;
use proc_macro2::TokenStream; use devise::ext::SpanDiagnosticExt;
use quote::quote; use quote::quote;
use proc_macro2::TokenStream;
use syn::ext::IdentExt; use syn::ext::IdentExt;
use crate::exports::*;
pub fn derive_from_param(input: proc_macro::TokenStream) -> TokenStream { pub fn derive_from_param(input: proc_macro::TokenStream) -> TokenStream {
DeriveGenerator::build_for(input, quote!(impl<'a> #_request::FromParam<'a>)) DeriveGenerator::build_for(input, quote!(impl<'a> #_request::FromParam<'a>))
.support(Support::Enum) .support(Support::Enum)
.validator(ValidatorBuild::new().fields_validate(|_, fields| { .validator(ValidatorBuild::new().fields_validate(|_, fields| {
if !fields.is_empty() { if !fields.is_empty() {
return Err(fields return Err(fields.span().error("variants with data fields are not supported"));
.span()
.error("Only enums without data fields are supported"));
} }
Ok(()) Ok(())
})) }))
.inner_mapper(MapperBuild::new().enum_map(|_, data| { .inner_mapper(MapperBuild::new().enum_map(|_, data| {
let matches = data.variants().map(|field| { let matches = data.variants().map(|field| {
let field_name = field.ident.unraw(); let field_name = field.ident.unraw();
quote!( quote!(stringify!(#field_name) => Ok(Self::#field))
stringify!(#field_name) => Ok(Self::#field),
)
}); });
let names = data.variants().map(|field| { let names = data.variants().map(|field| {
let field_name = field.ident.unraw(); let field_name = field.ident.unraw();
quote!( quote!(stringify!(#field_name))
#_Cow::Borrowed(stringify!(#field_name)),
)
}); });
quote! { quote! {
type Error = #_request::EnumFromParamError<'a>; type Error = #_error::InvalidOption<'a>;
fn from_param(param: &'a str) -> Result<Self, Self::Error> { fn from_param(param: &'a str) -> Result<Self, Self::Error> {
match param { match param {
#(#matches)* #(#matches,)*
_ => Err(#_request::EnumFromParamError::new( _ => Err(#_error::InvalidOption::new(param, &[#(#names),*])),
#_Cow::Borrowed(param),
#_Cow::Borrowed(&[#(#names)*]),
)),
} }
} }
} }

View File

@ -776,32 +776,41 @@ pub fn derive_from_form(input: TokenStream) -> TokenStream {
/// Derive for the [`FromParam`] trait. /// Derive for the [`FromParam`] trait.
/// ///
/// The [`FromParam`] derive can be applied to enums with nullary /// This [`FromParam`] derive can be applied to C-like enums whose variants have
/// (zero-length) fields. To implement FromParam, the function matches each variant /// no fields. The generated implementation case-sensitively matches each
/// to its stringified field name (case sensitive): /// variant to its stringified field name. If there is no match, an error
/// of type [`InvalidOption`] is returned.
///
/// [`FromParam`]: ../rocket/request/trait.FromParam.html
/// [`InvalidOption`]: ../rocket/error/struct.InvalidOption.html
///
/// # Example
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
/// #
/// use rocket::request::FromParam; /// use rocket::request::FromParam;
/// ///
/// #[derive(FromParam, Debug, PartialEq)] /// #[derive(FromParam, Debug, PartialEq)]
/// enum MyParam { /// enum MyParam {
/// A, /// A,
/// B, /// Bob,
/// } /// }
/// ///
/// assert_eq!(MyParam::from_param("A").unwrap(), MyParam::A); /// assert_eq!(MyParam::from_param("A").unwrap(), MyParam::A);
/// assert_eq!(MyParam::from_param("B").unwrap(), MyParam::B); /// assert_eq!(MyParam::from_param("Bob").unwrap(), MyParam::Bob);
/// assert!(MyParam::from_param("a").is_err()); /// assert!(MyParam::from_param("a").is_err());
/// assert!(MyParam::from_param("b").is_err()); /// assert!(MyParam::from_param("bob").is_err());
/// assert!(MyParam::from_param("c").is_err()); /// assert!(MyParam::from_param("c").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
/// ///
/// // Now `MyParam` can be used in an route to accept either `A` or `B`.
/// #[get("/<param>")]
/// fn index(param: MyParam) -> &'static str {
/// match param {
/// MyParam::A => "A",
/// MyParam::Bob => "Bob",
/// }
/// }
#[proc_macro_derive(FromParam)] #[proc_macro_derive(FromParam)]
pub fn derive_from_param(input: TokenStream) -> TokenStream { pub fn derive_from_param(input: TokenStream) -> TokenStream {
emit!(derive::from_param::derive_from_param(input)) emit!(derive::from_param::derive_from_param(input))

View File

@ -1,5 +1,6 @@
use rocket::request::FromParam; use rocket::request::FromParam;
#[allow(non_camel_case_types)]
#[derive(Debug, FromParam, PartialEq)] #[derive(Debug, FromParam, PartialEq)]
enum Test { enum Test {
Test1, Test1,
@ -9,14 +10,16 @@ enum Test {
#[test] #[test]
fn derive_from_param() { fn derive_from_param() {
let test1 = Test::from_param("Test1").expect("Should be valid"); assert_eq!(Test::from_param("Test1").unwrap(), Test::Test1);
assert_eq!(test1, Test::Test1); assert_eq!(Test::from_param("Test2").unwrap(), Test::Test2);
assert_eq!(Test::from_param("for").unwrap(), Test::r#for);
let test2 = Test::from_param("Test2").expect("Should be valid"); let err = Test::from_param("For").unwrap_err();
assert_eq!(test2, Test::Test2); assert_eq!(err.value, "For");
let test2 = Test::from_param("for").expect("Should be valid"); assert_eq!(err.options, &["Test1", "Test2", "for"]);
assert_eq!(test2, Test::r#for);
let err = Test::from_param("not_test").unwrap_err();
assert_eq!(err.value, "not_test");
assert_eq!(err.options, &["Test1", "Test2", "for"]);
let test3 = Test::from_param("not_test");
assert!(test3.is_err());
} }

View File

@ -26,7 +26,7 @@ note: error occurred while deriving `FromParam`
| ^^^^^^^^^ | ^^^^^^^^^
= note: this error originates in the derive macro `FromParam` (in Nightly builds, run with -Z macro-backtrace for more info) = 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 error: variants with data fields are not supported
--> tests/ui-fail-nightly/from_param.rs:13:6 --> tests/ui-fail-nightly/from_param.rs:13:6
| |
13 | A(String), 13 | A(String),

View File

@ -28,7 +28,7 @@ error: [note] error occurred while deriving `FromParam`
| |
= note: this error originates in the derive macro `FromParam` (in Nightly builds, run with -Z macro-backtrace for more info) = 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 error: variants with data fields are not supported
--> tests/ui-fail-stable/from_param.rs:13:6 --> tests/ui-fail-stable/from_param.rs:13:6
| |
13 | A(String), 13 | A(String),

View File

@ -89,6 +89,60 @@ pub enum ErrorKind {
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Empty; pub struct Empty;
/// An error that occurs when a value doesn't match one of the expected options.
///
/// This error is returned by the [`FromParam`] trait implementation generated
/// by the [`FromParam` derive](macro@rocket::FromParam) when the value of a
/// dynamic path segment does not match one of the expected variants. The
/// `value` field will contain the value that was provided, and `options` will
/// contain each of possible stringified variants.
///
/// [`FromParam`]: trait@rocket::request::FromParam
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::error::InvalidOption;
///
/// #[derive(FromParam)]
/// enum MyParam {
/// FirstOption,
/// SecondOption,
/// ThirdOption,
/// }
///
/// #[get("/<param>")]
/// fn hello(param: Result<MyParam, InvalidOption<'_>>) {
/// if let Err(e) = param {
/// assert_eq!(e.options, &["FirstOption", "SecondOption", "ThirdOption"]);
/// }
/// }
/// ```
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct InvalidOption<'a> {
/// The value that was provided.
pub value: &'a str,
/// The expected values: a slice of strings, one for each possible value.
pub options: &'static [&'static str],
}
impl<'a> InvalidOption<'a> {
#[doc(hidden)]
pub fn new(value: &'a str, options: &'static [&'static str]) -> Self {
Self { value, options }
}
}
impl fmt::Display for InvalidOption<'_> {
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 InvalidOption<'_> {}
impl Error { impl Error {
#[inline(always)] #[inline(always)]
pub(crate) fn new(kind: ErrorKind) -> Error { pub(crate) fn new(kind: ErrorKind) -> Error {

View File

@ -1,10 +1,6 @@
use std::borrow::Cow;
use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use std::path::PathBuf; use std::path::PathBuf;
use cookie::Display;
use crate::error::Empty; use crate::error::Empty;
use crate::either::Either; use crate::either::Either;
use crate::http::uri::{Segments, error::PathError, fmt::Path}; use crate::http::uri::{Segments, error::PathError, fmt::Path};
@ -16,6 +12,11 @@ use crate::http::uri::{Segments, error::PathError, fmt::Path};
/// a dynamic segment `<param>` where `param` has some type `T` that implements /// a dynamic segment `<param>` where `param` has some type `T` that implements
/// `FromParam`, `T::from_param` will be called. /// `FromParam`, `T::from_param` will be called.
/// ///
/// # Deriving
///
/// The `FromParam` trait can be automatically derived for C-like enums. See
/// [`FromParam` derive](macro@rocket::FromParam) for more information.
///
/// # Forwarding /// # Forwarding
/// ///
/// If the conversion fails, the incoming request will be forwarded to the next /// If the conversion fails, the incoming request will be forwarded to the next
@ -310,31 +311,6 @@ 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. /// Trait to convert _many_ dynamic path segment strings to a concrete value.
/// ///
/// This is the `..` analog to [`FromParam`], and its functionality is identical /// This is the `..` analog to [`FromParam`], and its functionality is identical

View File

@ -12,9 +12,6 @@ pub use self::request::Request;
pub use self::from_request::{FromRequest, Outcome}; pub use self::from_request::{FromRequest, Outcome};
pub use self::from_param::{FromParam, FromSegments}; pub use self::from_param::{FromParam, FromSegments};
#[doc(hidden)]
pub use self::from_param::EnumFromParamError;
#[doc(hidden)] #[doc(hidden)]
pub use rocket_codegen::FromParam; pub use rocket_codegen::FromParam;