Rafael B. Tauil
Rafael B. Tauil

Reputation: 3

How to map or transform a vector of structs into another vector of structs?

Minimal Reproducible Example

pub struct User {
    pub id: i32,
    pub name: String,
    pub match_id: i32,
}

pub struct Match {
    pub id: i32,
    pub name: String,
}

pub struct MatchWithUsers {
    pub id: i32,
    pub name: String,
    pub users: Vec<User>,
}

fn main() {
    let query_result: Vec<(Match, Option<User>)> = vec![
        (
            Match {
                id: 1,
                name: String::from("1st match"),
            },
            Some(User {
                id: 1,
                name: String::from("Jack"),
                match_id: 1,
            }),
        ),
        (
            Match {
                id: 2,
                name: String::from("2nd match"),
            },
            Some(User {
                id: 2,
                name: String::from("John"),
                match_id: 2,
            }),
        ),
        (
            Match {
                id: 3,
                name: String::from("3rd match"),
            },
            None,
        ),
    ];
    let mut response: Vec<MatchWithUsers> = Vec::new();

    for (m, u) in &query_result {
        let existing_match = &response
            .into_iter()
            .find(|match_with_user| match_with_user.id == m.id);

        match existing_match {
            Some(found_match) => {
                println!("Inser user into match: {}", found_match.name);
                match u {
                    Some(mut user) => {
                        found_match.users.push(user);
                    }
                    None => println!("No users."),
                }
            }
            None => {
                println!("No existing match. Add to response.");
                let user = u.as_ref().unwrap();
                response.push(MatchWithUsers {
                    id: m.id,
                    name: m.name.clone(),
                    users: vec![],
                });
            }
        }
    }

    println!("Response with: {}", response.len());
}
warning: unused variable: `user`
  --> src/main.rs:69:21
   |
69 |                 let user = u.as_ref().unwrap();
   |                     ^^^^ help: consider prefixing with an underscore: `_user`
   |
   = note: `#[warn(unused_variables)]` on by default

warning: variable does not need to be mutable
  --> src/main.rs:61:26
   |
61 |                     Some(mut user) => {
   |                          ----^^^^
   |                          |
   |                          help: remove this `mut`
   |
   = note: `#[warn(unused_mut)]` on by default

error[E0382]: use of moved value: `response`
  --> src/main.rs:53:31
   |
50 |     let mut response: Vec<MatchWithUsers> = Vec::new();
   |         ------------ move occurs because `response` has type `std::vec::Vec<MatchWithUsers>`, which does not implement the `Copy` trait
...
53 |         let existing_match = &response
   |                               ^^^^^^^^ value moved here, in previous iteration of loop

error[E0507]: cannot move out of `u.0` which is behind a shared reference
  --> src/main.rs:60:23
   |
60 |                 match u {
   |                       ^
61 |                     Some(mut user) => {
   |                          --------
   |                          |
   |                          data moved here
   |                          move occurs because `user` has type `User`, which does not implement the `Copy` trait

error[E0596]: cannot borrow `found_match.users` as mutable, as it is behind a `&` reference
  --> src/main.rs:62:25
   |
62 |                         found_match.users.push(user);
   |                         ^^^^^^^^^^^^^^^^^ `found_match` is a `&` reference, so the data it refers to cannot be borrowed as mutable

Playground

My problem

I have an API test project using Rocket and Diesel.

The following method does a Diesel query and should map the result to a JSON response. This is a response of all matches in the database with thier users. The users should be nested in each Match node.

My solution attempt

  1. I create a vector.
  2. Iterate in the query result;
  3. Check if the match already exists in my vector, if it does, add the user info (coming from the query result) and add it to the users attribute in the current MatchWithUser, otherwise just add a struct (MatchWithUsers) to the vector.
pub fn show_all_matches2() -> Vec<MatchWithUsers> {
    use schema::*;

    let connection = establish_connection();

    let query_result: Vec<(Match, Option<User>)> = matches::table
        .left_join(users::table.on(users::match_id.eq(matches::id)))
        .load(&connection)
        .expect("Error loading matches");

    let mut response: Vec<MatchWithUsers> = Vec::new();

    for (m, u) in &query_result {
        let existing_match = &response
            .into_iter()
            .find(|match_with_user| match_with_user.id == m.id);

        match existing_match {
            Some(mut found_match) => {
                println!("Inser user into match: {}", found_match.name);
                found_match.users.push(u.unwrap());
            }
            None => {
                println!("No existing match. Add to response.");
                let user = u.as_ref().unwrap();
                response.push(MatchWithUsers {
                    id: m.id,
                    name: m.name.clone(),
                    players_count: m.players_count,
                    users: vec![User {
                        id: user.id,
                        name: user.name.clone(),
                        match_id: user.match_id,
                    }],
                });
            }
        }
    }

    response
}

Structs

use crate::schema::{matches, users};
use serde::{Deserialize, Serialize};

#[derive(Queryable, Identifiable, Associations, Serialize, Deserialize)]
#[belongs_to(Match)]
#[table_name = "users"]
pub struct User {
    pub id: i32,
    pub name: String,
    pub match_id: i32,
}

#[derive(Queryable, Identifiable, Serialize, Deserialize)]
#[table_name = "matches"]
pub struct Match {
    pub id: i32,
    pub name: String,
    pub players_count: i32,
}

#[derive(Serialize, Deserialize)]
pub struct MatchWithUsers {
    pub id: i32,
    pub name: String,
    pub players_count: i32,
    pub users: Vec<User>,
}

Errors

errors when compiling

$ cargo run
   Compiling got_board_api_v3 v0.1.0 (/Users/tauil/Projects/got/got-board-api-v3)
error[E0382]: use of moved value: `response`
  --> src/lib.rs:54:31
   |
51 |     let mut response: Vec<MatchWithUsers> = Vec::new();
   |         ------------ move occurs because `response` has type `std::vec::Vec<models::MatchWithUsers>`, which does not implement the `Copy` trait
...
54 |         let existing_match = &response
   |                               ^^^^^^^^ value moved here, in previous iteration of loop

error[E0507]: cannot move out of `existing_match.0` which is behind a shared reference
  --> src/lib.rs:58:15
   |
58 |         match existing_match {
   |               ^^^^^^^^^^^^^^
59 |             Some(mut found_match) => {
   |                  ---------------
   |                  |
   |                  data moved here
   |                  move occurs because `found_match` has type `models::MatchWithUsers`, which does not implement the `Copy` trait

error[E0507]: cannot move out of `*u` which is behind a shared reference
  --> src/lib.rs:61:40
   |
61 |                 found_match.users.push(u.unwrap());
   |                                        ^
   |                                        |
   |                                        move occurs because `*u` has type `std::option::Option<models::User>`, which does not implement the `Copy` trait
   |                                        help: consider borrowing the `Option`'s content: `u.as_ref()`

Query result in the Postgres client:

select * from matches left join users on matches.id = users.match_id;

 id |        name         | players_count | id |   name    | match_id
----+---------------------+---------------+----+-----------+----------
  1 | My first match      |             3 |  1 | Rafael    |        1
  1 | My first match      |             3 |  2 | Leandro   |        1
  1 | My first match      |             3 |  3 | Vagner    |        1
  2 | Just a second match |             4 |  4 | Vagner    |        2
  2 | Just a second match |             4 |  5 | Leandro   |        2
  2 | Just a second match |             4 |  6 | Rafael    |        2
  2 | Just a second match |             4 |  7 | Wanderson |        2
  3 | Amazing match       |             6 |    |           |
(8 rows)

Upvotes: 0

Views: 3928

Answers (1)

Jmb
Jmb

Reputation: 23463

You can get a list of matches with the users associated with each match very easily if you use group_by from the itertools crate and if you ensure that your query results are sorted by match ID:

use itertools::Itertools; // 0.9.0
let response: Vec<_> = query_result
    .into_iter()
    // Note that this assumes that `query_result` is sorted
    // by match id since `group_by` only considers
    // consecutive matches.
    .group_by(|(m, _)| m.id)
    .into_iter()
    .map(|(id, mut g)| {
        // Now `g` is an iterator of `(Match, Option<User>)`
        // where all the matches are the same. We take the
        // first item to get the match information. Note
        // that it is safe to unwrap here because `group_by`
        // would never call us with an empty `g`.
        let (m, u) = g.next().unwrap();
        MatchWithUsers {
            id: id,
            name: m.name,
            // We got the first user along with the match
            // information, now we append the other users
            // from the remaining items in `g`.
            users: u
                .into_iter()
                .chain(g.flat_map(|(_, u)| u.into_iter()))
                .collect(),
        }
    })
    .collect();

Playground

Upvotes: 1

Related Questions