2

I have a model here:

#[derive(Debug, Serialize, Deserialize)]
pub struct ArticleModel {
        #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
        pub id: Option<ObjectId>,
        pub text: String,
        pub author: String,
        pub edited_date: Option<DateTime<Utc>>,
        pub posted_date: Option<DateTime<Utc>>,
        pub is_archived: bool,
        pub tags: Vec<String>,
        pub read_time_in_min: i32, // <-- Take note of this field
        pub word_count: i32, // <-- Take note of this field
    }

that in my api handlers, I try to convert the body of a request into like so:

#[post("/article")]
pub async fn create_article(
    data: Data<ArticleRepo>,
    new_article: Json<ArticleModel>, // <-- HERE, using Json extractor
) -> HttpResponse {
    let mut created_article = new_article.into_inner(); // <-- HERE getting its value
    created_article.id = None;

    let article_detail = data.create_article_repo(created_article).await;

    match article_detail {
        Ok(article) => HttpResponse::Ok().json(article),
        Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
    }
}

Except, I don't want to have to pass the fields read_time_in_min and word_count up in the body of the request. I'm trying to have them calculated based on what the text field says. These functions take &text as an input, and output an i32.

I can't figure out how to lay this out. I've thought to create an ArticleModel impl block that has an associated new function that takes in the required params, and then outputs a new instance of Self that has the calculated values, except, then, I can't deserialize ArticleModel from my handlers, since I have to deserialize into a struct and can't call a new function there. I also won't have passed up the 2 calculated fields in the body of the request, meaning it'll return a json parsing error.

How do I get around this?

cafce25
  • 15,907
  • 4
  • 25
  • 31
Matthew Trent
  • 2,611
  • 1
  • 17
  • 30
  • 1
    Have you considered splitting the request body data type and the repository domain model data type? It seems strange to me that the `id` field is mutated from the client's request. My first try would be this: A struct for creating (that does not include the unnecessary fields) and a struct that is used on the repository level that includes these fields. And then perhaps a `From` implementation. – Kendas Jan 13 '23 at 05:30
  • @Kendas. That seems like a really good idea. It would allow me to then serialize only the data I need, and then `From` it into the actual `ArticleModel` I use internally if I'm understanding you correctly. First Rust project, so I'm learning :) – Matthew Trent Jan 13 '23 at 05:35
  • 2
    This is not strictly a Rust thing. You can always begin with structs (or classes) that travel all the way from the API level to the data storage. But at some point you may run into an issue. It is then that you can revisit the initial design and perhaps split the structs by what they are used for. With time you'll get a feel for when to do this. Keep up the learning. – Kendas Jan 13 '23 at 05:41

2 Answers2

2

You can add the [serde(skip_deserializing)] attribute to read_time_in_min & word_count so that they don't have to be included in the request body. You can also wrap them in an Option as well, but that might look worse, and will also allow users to control those variables which may not be ideal.

Here's an example:

#[derive(Debug, Serialize, Deserialize)]
pub struct ArticleModel {
        #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
        pub id: Option<ObjectId>,
        pub text: String,
        pub author: String,
        pub edited_date: Option<DateTime<Utc>>,
        pub posted_date: Option<DateTime<Utc>>,
        pub is_archived: bool,
        pub tags: Vec<String>,
        #[serde(skip_deserializing)]
        pub read_time_in_min: i32, 
        #[serde(skip_deserializing)]
        pub word_count: i32,
    }
Bale
  • 553
  • 1
  • 8
  • 19
  • The `#[serde(skip_deserializing)]` for those fields is brilliant. But, then it allows the users to edit them hypothetically if they send the data up in the body. Is there anyway to get around that? Thanks for the response! – Matthew Trent Jan 13 '23 at 05:28
0

Credit to @Kendas for the solution from comments

I implemented the From trait for the type ArticleModel which converts the new type I expect in the api handlers, ArticleDeserializableModel, and converts it to the storable ArticleModel that I work with internally, which then allowed me to include the calculations.

impl From<ArticleDeserializableModel> for ArticleModel {
    fn from(article_deserializable_model: ArticleDeserializableModel) -> ArticleModel {
        ArticleModel {
            id: None,
            text: article_deserializable_model.text.to_owned(),
            author: article_deserializable_model.author,
            edited_date: None,
            posted_date: None,
            is_archived: article_deserializable_model.is_archived,
            tags: article_deserializable_model.tags,
            read_time_in_min: ArticleModel::word_count_to_read_time_in_min(
                ArticleModel::count_words(&article_deserializable_model.text),
            ),
            word_count: ArticleModel::count_words(&article_deserializable_model.text),
        }
    }
} 
Matthew Trent
  • 2,611
  • 1
  • 17
  • 30