Reputation: 38681
I have some menus with a tree structure, defined like this:
use rocket::serde::Deserialize;
use rocket::serde::Serialize;
use crate::model::diesel::dolphin::dolphin_models::MenuResource;
#[derive(Deserialize, Serialize)]
#[allow(non_snake_case)]
pub struct MenuResponse {
pub id: i32,
pub name: String,
pub name_zh: String,
pub parent_id: i32,
pub disableCheckbox: bool,
pub tree_id_path: String,
pub children: Vec<MenuResponse>
}
In the database I store the menus as a list with id
and parent_id
. I transform the list to a tree like this:
/**
** convert the list menu to tree recursive
**/
pub fn convert_menu_to_tree(root_menus: &Vec<MenuResource>, sub_menus: &Vec<MenuResource>) -> Vec<MenuResponse>{
let mut menu_res_list = Vec::new();
for root_menu in root_menus {
let mut origin_menu_res_list = Vec::new();
let mut menu_res = MenuResponse::from(root_menu);
for sub_menu in sub_menus{
if sub_menu.parent_id == root_menu.id {
let menu_res_sub = MenuResponse::from(sub_menu);
menu_res.children.push(menu_res_sub);
origin_menu_res_list.push(sub_menu.clone());
}
}
if !menu_res.children.is_empty() {
menu_res.children = convert_menu_to_tree(&origin_menu_res_list, sub_menus);
}
menu_res_list.push(menu_res);
}
return menu_res_list;
}
This code works fine. The problem is that I have another datatype I want to treat as tree structure like this. I have to write the list to tree conversion function for each one. Is it possible to write a function to handle all data structure like this? One function to convert all list objects to tree structure.
In Java, I could using generics and reflection to do it. How about in Rust?
I have tried to define the function the generic way like this:
/**
** convert the list menu to tree recursive
**/
pub fn convert_menu_to_tree<T,E>(root_menus: &Vec<T>, sub_menus: &Vec<T>) -> Vec<E>{
let mut menu_res_list = Vec::new();
for root_menu in root_menus {
let mut origin_menu_res_list = Vec::new();
let mut menu_res = E::from(root_menu);
for sub_menu in sub_menus{
if sub_menu.parent_id == root_menu.id {
let menu_res_sub = E::from(sub_menu);
menu_res.children.push(menu_res_sub);
origin_menu_res_list.push(sub_menu.clone());
}
}
if !menu_res.children.is_empty() {
menu_res.children = convert_menu_to_tree(&origin_menu_res_list, sub_menus);
}
menu_res_list.push(menu_res);
}
return menu_res_list;
}
But I don't know how to handle the children
attribute with T
or E
. Is is possible to set the value by name children
?
Upvotes: 0
Views: 265
Reputation: 22476
As your example wasn't quite a minimal reproducible example, I re-wrote it (sorry, stretches the definition of 'minimal' a little) to play around with:
use serde::Serialize;
#[derive(Clone)]
pub struct MenuResource {
parent_id: i32,
id: i32,
content: i32,
}
impl MenuResource {
pub fn new(parent_id: i32, id: i32, content: i32) -> Self {
Self {
parent_id,
id,
content,
}
}
}
#[derive(Debug, Serialize)]
pub struct MenuResponse {
pub id: i32,
pub content: i32,
pub children: Vec<MenuResponse>,
}
impl From<&MenuResource> for MenuResponse {
fn from(src: &MenuResource) -> Self {
Self {
id: src.id,
content: src.content,
children: vec![],
}
}
}
/**
** convert the list menu to tree recursive
**/
pub fn convert_menu_to_tree(
root_menus: &Vec<MenuResource>,
sub_menus: &Vec<MenuResource>,
) -> Vec<MenuResponse> {
let mut menu_res_list = Vec::new();
for root_menu in root_menus {
let mut origin_menu_res_list: Vec<MenuResource> = Vec::new();
let mut menu_res = MenuResponse::from(root_menu);
for sub_menu in sub_menus {
if sub_menu.parent_id == root_menu.id {
let menu_res_sub = MenuResponse::from(sub_menu);
menu_res.children.push(menu_res_sub);
origin_menu_res_list.push((*sub_menu).clone());
}
}
if !menu_res.children.is_empty() {
menu_res.children = convert_menu_to_tree(&origin_menu_res_list, sub_menus);
}
menu_res_list.push(menu_res);
}
return menu_res_list;
}
pub fn main() {
let root_menus = vec![MenuResource::new(0, 1, 101), MenuResource::new(0, 2, 102)];
let sub_menus = vec![
MenuResource::new(1, 3, 201),
MenuResource::new(1, 4, 202),
MenuResource::new(2, 5, 203),
MenuResource::new(3, 6, 204),
MenuResource::new(6, 7, 205),
];
let result = convert_menu_to_tree(&root_menus, &sub_menus);
println!("{}", serde_json::to_string_pretty(&result).unwrap());
}
Output:
[
{
"id": 1,
"content": 101,
"children": [
{
"id": 3,
"content": 201,
"children": [
{
"id": 6,
"content": 204,
"children": [
{
"id": 7,
"content": 205,
"children": []
}
]
}
]
},
{
"id": 4,
"content": 202,
"children": []
}
]
},
{
"id": 2,
"content": 102,
"children": [
{
"id": 5,
"content": 203,
"children": []
}
]
}
]
The Serialize
and json
is of course not strictly necessary, but the output is very unreadable otherwise, and I was too lazy to write a proper Display
impl.
Now, let's see what we need to make the convert_menu_to_tree
function more generic (let's rename it convert_to_tree
):
T
that we want to convert to a tree&T
and a list of children and creates the output typeid
and the parent_id
from T
.Therefore, let's define a trait IntoTree
for that type T
:
pub trait IntoTree: Sized {
type Output;
fn get_id(&self) -> i32;
fn get_parent_id(&self) -> i32;
fn convert(&self, children: Vec<Self::Output>) -> Self::Output;
}
Now here is an algorithm that puts it all together:
convert_to_tree.rs
use std::collections::HashMap;
pub trait IntoTree: Sized {
type Output;
fn get_id(&self) -> i32;
fn get_parent_id(&self) -> i32;
fn convert(&self, children: Vec<Self::Output>) -> Self::Output;
}
fn take_all_children<T>(parent_id: i32, sub_menus: &mut HashMap<i32, Vec<&T>>) -> Vec<T::Output>
where
T: IntoTree,
{
sub_menus
.remove(&parent_id)
.unwrap_or_default()
.iter()
.map(|child| {
let grandchildren = take_all_children(child.get_id(), sub_menus);
child.convert(grandchildren)
})
.collect()
}
pub fn convert_to_tree<T>(root_menus: &[T], sub_menus: &[T]) -> Vec<T::Output>
where
T: IntoTree,
{
let mut sub_menus_by_parent = HashMap::new();
for sub in sub_menus {
sub_menus_by_parent
.entry(sub.get_parent_id())
.or_insert_with(Vec::new)
.push(sub);
}
root_menus
.iter()
.map(|root_menu| {
let children = take_all_children(root_menu.get_id(), &mut sub_menus_by_parent);
root_menu.convert(children)
})
.collect()
}
main.rs
mod convert_to_tree;
use convert_to_tree::{convert_to_tree, IntoTree};
use serde::Serialize;
#[derive(Clone)]
pub struct MenuResource {
parent_id: i32,
id: i32,
content: i32,
}
impl MenuResource {
pub fn new(parent_id: i32, id: i32, content: i32) -> Self {
Self {
parent_id,
id,
content,
}
}
}
#[derive(Debug, Serialize)]
pub struct MenuResponse {
pub id: i32,
pub content: i32,
pub children: Vec<MenuResponse>,
}
impl IntoTree for MenuResource {
type Output = MenuResponse;
fn get_id(&self) -> i32 {
self.id
}
fn get_parent_id(&self) -> i32 {
self.parent_id
}
fn convert(&self, children: Vec<Self::Output>) -> Self::Output {
MenuResponse {
id: self.id,
content: self.content,
children,
}
}
}
pub fn main() {
let root_menus = vec![MenuResource::new(0, 1, 101), MenuResource::new(0, 2, 102)];
let sub_menus = vec![
MenuResource::new(1, 3, 201),
MenuResource::new(1, 4, 202),
MenuResource::new(2, 5, 203),
MenuResource::new(3, 6, 204),
MenuResource::new(6, 7, 205),
];
let result = convert_to_tree(&root_menus, &sub_menus);
println!("{}", serde_json::to_string_pretty(&result).unwrap());
}
With this, you can now convert every type that implements IntoTree
to a tree.
The conversion should be very fast as it first builds a HashMap
of ids and children. Then, it just takes the children for every node out of the HashMap
.
Just for reference, another solution that is closer to your code:
convert_to_tree.rs
pub trait IntoTree: Sized + Clone {
type Output;
fn get_id(&self) -> i32;
fn get_parent_id(&self) -> i32;
fn convert(&self, children: Vec<Self::Output>) -> Self::Output;
}
pub fn convert_to_tree<T>(root_menus: &[T], sub_menus: &[T]) -> Vec<T::Output>
where
T: IntoTree,
{
let mut menu_res_list = Vec::new();
for root_menu in root_menus {
let mut origin_menu_res_list: Vec<T> = Vec::new();
for sub_menu in sub_menus {
if sub_menu.get_parent_id() == root_menu.get_id() {
origin_menu_res_list.push(sub_menu.clone());
}
}
let children = convert_to_tree(&origin_menu_res_list, sub_menus);
let menu_res = root_menu.convert(children);
menu_res_list.push(menu_res);
}
return menu_res_list;
}
What I don't like about this solution is the use of .clone
and the Clone
requirement for T
.
Upvotes: 2