wrbapp/src/util/model/member/migration.rs
2024-10-09 12:14:56 +02:00

308 lines
9.7 KiB
Rust

use crate::util::surrealdb::DB;
use super::{Member, MembersMigration};
use once_cell::sync::Lazy;
use std::collections::HashMap;
use surrealdb::sql::statements::{BeginStatement, CommitStatement};
use tokio::sync::Mutex;
// Create a store for saving information when migrating to a new members list
static MEMBERS_STORE: Lazy<Mutex<MembersStore>> = Lazy::new(|| Mutex::new(MembersStore::new()));
struct MembersStore {
store: HashMap<u16, MembersMigration>,
count: u16,
}
impl MembersStore {
fn new() -> Self {
Self {
store: HashMap::new(),
count: 0,
}
}
fn insert(&mut self, input: MembersMigration) -> u16 {
let count = self.count + 1;
self.store.insert(count, input);
count
}
fn get(&self, key: &u16) -> Option<&MembersMigration> {
self.store.get(key)
}
fn remove(&mut self, key: &u16) -> Option<MembersMigration> {
self.store.remove(key)
}
}
// Create a row for the csv file
#[derive(Debug, serde::Deserialize)]
struct Row {
#[serde(rename = "Relatiecode")]
id: String,
#[serde(rename = "Roepnaam")]
first_name: String,
#[serde(rename = "Tussenvoegsel(s)")]
middle_name: String,
#[serde(rename = "Achternaam")]
last_name: String,
#[serde(rename = "E-mail")]
email: String,
#[serde(rename = "Verenigingssporten")]
hours: String,
#[serde(rename = "Diploma dropdown 1")]
diploma: Option<String>,
}
impl Row {
fn to_member(&self) -> Member {
Member {
id: self.id.trim().to_string(),
name: super::Name {
first: self.first_name.clone(),
full: self.generate_full_name(),
},
hours: self.generate_hours(),
groups: Vec::new(),
diploma: self.diploma.clone(),
registration_token: None,
}
}
// Get the hour data from the raw string
fn generate_hours(&self) -> Vec<String> {
let mut hours: Vec<String> = Vec::new();
let group_parts: Vec<&str> = self.hours.split(", ").collect();
for group in group_parts {
let hour_parts: Vec<&str> = group.split(" - ").collect();
for part in hour_parts {
if &*part != "Groep" {
hours.push(part.to_string());
}
}
}
hours
}
// Generate the full name from 3 parts
fn generate_full_name(&self) -> String {
let mut parts: Vec<&str> = vec![];
parts.push(&self.first_name);
parts.push(&self.middle_name);
parts.push(&self.last_name);
let filtered_strings: Vec<&str> = parts.into_iter().filter(|s| !s.is_empty()).collect();
filtered_strings.join(" ")
}
}
impl MembersMigration {
// Convert the raw csv file to rust objects
fn csv_to_rows(input: String) -> Result<Vec<Row>, Box<dyn std::error::Error>> {
let mut members: Vec<Row> = vec![];
let mut rdr = csv::Reader::from_reader(input.as_bytes());
for result in rdr.deserialize() {
let row: Row = result?;
members.push(row);
}
Ok(members)
}
// Covert the rows to formatted members
fn rows_to_members(rows: Vec<Row>) -> Vec<Member> {
let mut members: Vec<Member> = vec![];
for row in rows {
members.push(row.to_member());
}
members
}
// Compare the new members list with the current
fn combine_members_lists(
new_members_list: Vec<Member>,
current_members_list: Vec<Member>,
) -> MembersMigration {
let current_members_map: HashMap<String, Member> = current_members_list
.clone()
.into_iter()
.map(|m| (m.id.clone(), m))
.collect();
let new_members_map: HashMap<String, Member> = new_members_list
.clone()
.into_iter()
.map(|m| (m.id.clone(), m))
.collect();
let mut inserted_members: Vec<Member> = vec![];
let mut updated_members: Vec<Member> = vec![];
let mut removed_members: Vec<Member> = vec![];
for current_member in current_members_list {
if let Some(new_member) = new_members_map.get(&current_member.id) {
// Update existing member
let new_member_clone = new_member.clone();
updated_members.push(Member {
id: current_member.id,
name: new_member_clone.name,
hours: new_member_clone.hours,
groups: current_member.groups,
diploma: new_member_clone.diploma,
registration_token: current_member.registration_token,
})
} else {
// Remove member
removed_members.push(current_member);
}
}
for new_member in new_members_list {
// Insert new member
if !current_members_map.contains_key(&new_member.id) {
inserted_members.push(new_member);
}
}
Self {
inserted: inserted_members,
updated: updated_members,
removed: removed_members,
}
}
pub async fn migrate_proposal(
csv: String,
) -> Result<(u16, MembersMigration), Box<dyn std::error::Error>> {
let rows = Self::csv_to_rows(csv)?;
let new_members = Self::rows_to_members(rows);
let current_members = Member::fetch_all().await?;
let members_migration = Self::combine_members_lists(new_members, current_members);
let count = MEMBERS_STORE.lock().await.insert(members_migration.clone());
Ok((count, members_migration))
}
pub async fn migrate(id: u16) -> Result<(), Box<dyn std::error::Error>> {
let mut members_store = MEMBERS_STORE.lock().await;
let members_migration = match members_store.get(&id) {
Some(mm) => mm,
None => return Err("Could not get members from store".into()),
};
let mut transaction = DB.query(BeginStatement::default());
for member in members_migration.updated.clone() {
let id = member.id.clone();
transaction = transaction.query(format!("UPDATE type::thing('member', $id_{id}) SET name.first = $name_first_{id}, name.full = $name_full_{id}, hours = $hours_{id}, groups = $groups_{id}, diploma = $diploma_{id};"))
.bind((format!("id_{id}"), member.id))
.bind((format!("name_first_{id}"), member.name.first))
.bind((format!("name_full_{id}"), member.name.full))
.bind((format!("hours_{id}"), member.hours))
.bind((format!("groups_{id}"), member.groups))
.bind((format!("diploma_{id}"), member.diploma));
}
for member in members_migration.inserted.clone() {
let id = member.id.clone();
transaction = transaction.query(format!("CREATE type::thing('member', $id_{id}) SET name.first = $name_first_{id}, name.full = $name_full_{id}, hours = $hours_{id}, groups = $groups_{id}, diploma = $diploma_{id};"))
.bind((format!("id_{id}"), member.id))
.bind((format!("name_first_{id}"), member.name.first))
.bind((format!("name_full_{id}"), member.name.full))
.bind((format!("hours_{id}"), member.hours))
.bind((format!("groups_{id}"), member.groups))
.bind((format!("diploma_{id}"), member.diploma));
}
for member in members_migration.removed.clone() {
let id = member.id.clone();
transaction = transaction
.query(format!("DELETE type::thing('member', $id_{id});"))
.bind((format!("id_{id}"), member.id));
}
transaction
.query(CommitStatement::default())
.await?
.check()?;
members_store.remove(&id);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn csv_to_members_test() -> Result<(), String> {
let data = "Relatiecode,Volledige naam (1),Roepnaam,Tussenvoegsel(s),Achternaam,E-mail,2e E-mail,Verenigingssporten,Diploma dropdown 1
D000001,\"Last, First\",First,,Last,first.last@example.com,first.last@example.nl,\"Groep - Wedstrijd - Zaterdag, Groep - Z5 - Zaterdag\",LS1
D000002,\"Last2, First2\",First2,,Last2,first1.last@example.nl,,Groep - Z5 - Zaterdag,ZR4".to_string();
let expected = vec![
Member {
id: "D000001".to_string(),
name: super::super::Name {
first: "First".to_string(),
full: "First Last".to_string(),
},
hours: Vec::from([
"Wedstrijd".to_string(),
"Z5".to_string(),
"Zaterdag".to_string(),
]),
groups: vec![],
diploma: Some("LS1".to_string()),
registration_token: None,
},
Member {
id: "D000002".to_string(),
name: super::super::Name {
first: "First2".to_string(),
full: "First2 Last2".to_string(),
},
hours: Vec::from(["Z5".to_string(), "Zaterdag".to_string()]),
groups: Vec::new(),
diploma: Some("ZR4".to_string()),
registration_token: None,
},
];
let rows = match MembersMigration::csv_to_rows(data) {
Ok(r) => r,
Err(err) => return Err(err.to_string()),
};
let members = MembersMigration::rows_to_members(rows);
assert_eq!(expected, members);
Ok(())
}
}