Search code examples
rustinsertrust-diesel

implement Diesel's Insertable


I'm building a (gasp) blog backed by Rust's Diesel ORM. I would like the URL of a post to include the "slug" of its title. So, posts should be queryable by slug. Therefore I wish to generate the slug from the title using the slugify crate, and then store the slug in a corresponding column of the posts table in the database.

Because posts will also have a numerical ID to be generated by the DB, I wish to parse incoming posts into another struct, NewPost. Then NewPost should implement Diesel's Insertable, so that to record a new post in the DB it would suffice to call the resulting insert_into method. However, it does not work simply to derive Insertable, because the value for the slug attribute needs to be generated first.

One option would be to introduce an intermediate struct, SluggedNewPost, and implement for it the From<NewPost> and Insertable traits:

struct NewPost<'a> {
    title: &'a str,
    content: &'a str,
}

#[derive(Insertable)]
#[table_name="posts"]
struct SluggedNewPost<'a> {
    title: &'a str,
    content: &'a str,
    slug: String,
}

impl <'a> From<NewPost<'a>> for SluggedNewPost<'a> {
    fn from(newpost: NewPost<'a> ) -> Self {
        SluggedNewPost {title: &'a newpost.title,
                        content: newpost.content,
                        slug: slugify(newpost.title)}
    }
}

This works for my limited purposes. But it seems more elegant to implement the Insertable method on NewPost directly. I tried to follow the suggestion of this answer, but failed because I do not understand the code generated by the macro expansion (e.g., what is the result of dereferencing the id entry in the values tuple?).

Is it the wrong approach altogether to try to implement Insertable manually? Or in doing this am I missing something very easy? It seems like this sort of thing should be feasible pretty economically.


Solution

  • The probably best approach here is not to have a distinct SluggedNewPost. Diesels #[derive(Insertable)] is designed to be used for cases where you already have an existing struct, so that you can just put the derive there and things work. For cases where some additional calculation, like creating a password hash or calculating your slug the more direct tuple based insert variant is preferred. You even can mix both variants, which seems to be a good idea in this case. So your resulting code could look somehow like

    
    #[derive(Insertable)]
    #[table_name = "posts"]
    struct NewPost<'a> {
        title: &'a str,
        content: &'a str,
    }
    
    fn insert_with_slug(new_post: NewPost, conn: &PgConnection) -> QueryResult<()> {
        diesel::insert_into(posts::table)
            .values((new_post, posts::slug.eq(slugify(new_post.title))
            .execute(conn)?;
        Ok(())
    }