For-loop to a more elegant Rust-solution?

In my food platform there are specific categories which a restaurant can have. A restaurant can be in multiple categories.

I retrieve the following form to add a restaurant:

#[derive(Deserialize)]
pub struct AddRestaurantForm {
    //...
    pub restaurant_categories: std::vec::Vec<String>,
}

when a JSON-request is sent:

{
    //...
    "restaurant_categories":["sushi","italian"]
} 

The different categories are in a enum:

#[derive(Deserialize, Serialize, Clone, Debug)]
pub enum RestaurantCategory {
    Sushi,
    Italian,
    Pizza,
    Kebab,
}
impl FromStr for RestaurantCategory {
    type Err = ();

    fn from_str(input: &str) -> Result<RestaurantCategory, Self::Err> {
        match &*input.to_lowercase() {
            "sushi" => Ok(RestaurantCategory::Sushi),
            "italian" => Ok(RestaurantCategory::Italian),
            "pizza" => Ok(RestaurantCategory::Pizza),
            "kebab" => Ok(RestaurantCategory::Kebab),
            _ => Err(()),
        }
    }
}

The Restaurant struct:

#[derive(Deserialize, Serialize, Clone)]
pub struct Restaurant {
    //...
    categories: std::vec::Vec<RestaurantCategory>,
}

I need to convert the strings I retrieve in JSON to the enum-values so I can input them in the categories vec of a Restaurant. Right now I do it like this:

    let mut categories = std::vec::Vec::new();
    for category in form.restaurant_categories.clone().into_iter() {
        categories.push(RestaurantCategory::from_str(&category).unwrap());
    }

    let restaurant = Restaurant {
       //...
        categories: categories,
    };

But I was wondering if there was a way to write this more elegant? Could I use some kind of map-function to make the for-loop unnecessary?

E.g (pseudo-code):

 let restaurant = Restaurant {
       //...
        categories: form.restaurant_categories.map(category => category.RestaurantCategory::from_str(&category).unwrap())
    };

Basically I am just asking if there is a way to write this more elegant/efficient:

 let mut categories = std::vec::Vec::new();
    for category in form.restaurant_categories.clone().into_iter() {
        categories.push(RestaurantCategory::from_str(&category).unwrap());
    }

    let restaurant = Restaurant {
       //...
        categories: categories,
    };

What I tried:

let restaurant = Restaurant {
        id: 0,
        name: form.restaurant_name.to_string(),
        email: form.restaurant_email.to_string(),
        address: form.address.clone(),
        dishes: std::vec::Vec::new(),
        categories: form
            .restaurant_categories
            .clone()
            .iter()
            .map(|x| RestaurantCategory::from_str(x).unwrap()),
        image: None,
    };

Iterator::collect method can combine multiple results into single one:

use std::str::FromStr;
use std::error::Error;

#[derive(Clone, Debug)]
pub enum RestaurantCategory {
    Sushi,
    Italian,
    Pizza,
    Kebab,
}
#[derive(Debug)]
pub struct ParseRestaurantCategoryError;
impl std::fmt::Display for ParseRestaurantCategoryError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        "provided string is not valid category".fmt(f)
    }
}
impl Error for ParseRestaurantCategoryError {
    fn description(&self) -> &str {
        "failed to parse category"
    }
}
impl FromStr for RestaurantCategory {
    type Err = ParseRestaurantCategoryError;

    fn from_str(input: &str) -> Result<RestaurantCategory, Self::Err> {
        match &*input.to_lowercase() {
            "sushi" => Ok(RestaurantCategory::Sushi),
            "italian" => Ok(RestaurantCategory::Italian),
            "pizza" => Ok(RestaurantCategory::Pizza),
            "kebab" => Ok(RestaurantCategory::Kebab),
            _ => Err(ParseRestaurantCategoryError),
        }
    }
}

fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    let restaurant_categories = vec!["sushi", "italian"];
    let parsed: Vec<RestaurantCategory> = restaurant_categories.iter()
        .map(|c| c.parse())
        .collect::<Result<_, _>>()?;
    println!("{:?}", parsed);
    Ok(())
}
1 Like

I don't think one should need to write all that out if one's already deriving serde::Deserialize — shouldn't the serde::Deserialize implementation handle that? (I haven't used Serde in a while, though.)

2 Likes

Yes. This should be handled via Serde's deserialization rather than having to write the logic manually:

Playground.

1 Like

Yes, but it's not exactly simple to reuse the serde deserialization in FromStr, because you need to provide some impl Deserializer (the deserialization format). I'm not aware of any crate which automates this to go through serde. (There are multiple which will impl FromStr for enums themselves, though; strum comes to mind.)

The basic structure would be (fully untested)

impl FromStr for RestaurantCategory {
    type Error = ParseRestaurantCategoryError;
    fn from_str(s: &s) -> Result<Self, Self::Error> {
        Self::deserialize(RestaurantCategoryFromStrDeserializer(s))
    }
}

impl serde::de::Error for ParseRestaurantCategoryError { … }

#[derive(Copy, Clone)]
struct RestaurantCategoryFromStrDeserializer<'a>(&'a str);
impl<'de> Deserializer<'de> for RestaurantCategoryFromStrDeserializer<'de> {
    type Error = ParseRestaurantCategoryError;

    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
    where
        V: Visitor<'de>,
    {
        visitor.visit_borrowed_str(self.0)
    }

    forward_to_deserialize_any! {
        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
        bytes byte_buf option unit unit_struct newtype_struct seq tuple
        tuple_struct map struct enum identifier ignored_any
    }
}
1 Like

I believe that's basically StrDeserializer in serde::de::value - Rust?

3 Likes

Well I missed that when I looked :upside_down_face:

2 Likes

And I've spent a long time trying to figure out what those are even for, so it's nice to have an example use case!