Started on member migration
This commit is contained in:
48
server/src/auth.rs
Normal file
48
server/src/auth.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use axum::{extract::FromRequestParts, http::request::Parts, RequestPartsExt};
|
||||
use axum_extra::{
|
||||
headers::{authorization::Bearer, Authorization},
|
||||
typed_header::TypedHeaderRejectionReason,
|
||||
TypedHeader,
|
||||
};
|
||||
use bearer::verify_bearer;
|
||||
pub use error::AuthError;
|
||||
|
||||
mod bearer;
|
||||
mod error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Permissions<'a>(pub HashSet<&'a str>);
|
||||
|
||||
// Middleware for getting permissions
|
||||
impl<S> FromRequestParts<S> for Permissions<'_>
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = crate::Error;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
// First check if the request has a beaerer token to authenticate
|
||||
match parts.extract::<TypedHeader<Authorization<Bearer>>>().await {
|
||||
Ok(bearer) => {
|
||||
verify_bearer(bearer.token().to_string()).map_err(|_| AuthError::InvalidToken)?;
|
||||
|
||||
let permissions = Permissions {
|
||||
0: HashSet::from(["root"]),
|
||||
};
|
||||
|
||||
return Ok(permissions);
|
||||
}
|
||||
Err(err) => match err.reason() {
|
||||
TypedHeaderRejectionReason::Missing => (),
|
||||
TypedHeaderRejectionReason::Error(_err) => {
|
||||
return Err(AuthError::InvalidToken.into())
|
||||
}
|
||||
_ => return Err(AuthError::Unexpected.into()),
|
||||
},
|
||||
};
|
||||
|
||||
Err(AuthError::Unexpected.into())
|
||||
}
|
||||
}
|
8
server/src/auth/bearer.rs
Normal file
8
server/src/auth/bearer.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
pub fn verify_bearer(token: String) -> Result<(), ()> {
|
||||
let env_api_token = dotenvy::var("API_TOKEN").map_err(|_| ())?;
|
||||
|
||||
match env_api_token == token {
|
||||
true => Ok(()),
|
||||
false => Err(()),
|
||||
}
|
||||
}
|
20
server/src/auth/error.rs
Normal file
20
server/src/auth/error.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthError {
|
||||
NoPermssions,
|
||||
InvalidToken,
|
||||
Unexpected,
|
||||
}
|
||||
|
||||
impl Display for AuthError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoPermssions => write!(f, "{}", "No permissions"),
|
||||
Self::InvalidToken => write!(f, "{}", "Invalid token"),
|
||||
Self::Unexpected => write!(f, "{}", "Unexpected error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AuthError {}
|
@@ -1,3 +1,5 @@
|
||||
mod postgres;
|
||||
pub use postgres::apply_migrations;
|
||||
pub use postgres::connect;
|
||||
|
||||
pub mod model;
|
||||
|
5
server/src/database/model.rs
Normal file
5
server/src/database/model.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod member;
|
||||
pub mod session;
|
||||
pub mod user;
|
||||
|
||||
pub use member::Member;
|
54
server/src/database/model/member.rs
Normal file
54
server/src/database/model/member.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use rand::distr::{Alphanumeric, SampleString};
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Debug, Validate)]
|
||||
pub struct Member {
|
||||
#[validate(length(equal = 7))]
|
||||
pub id: String,
|
||||
pub first_name: String,
|
||||
pub full_name: String,
|
||||
pub registration_token: Option<String>,
|
||||
pub diploma: Option<String>,
|
||||
pub hours: Vec<String>,
|
||||
pub groups: Vec<String>,
|
||||
}
|
||||
|
||||
impl Member {
|
||||
pub async fn insert_multiple(
|
||||
transaction: &mut sqlx::Transaction<'_, Postgres>,
|
||||
members: Vec<Self>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
let mut query_builder = QueryBuilder::new(
|
||||
"INSERT INTO members(id, first_name, full_name, registration_token, diploma, hours, groups) "
|
||||
);
|
||||
|
||||
query_builder.push_values(members.into_iter(), |mut b, member| {
|
||||
let registration_token = Alphanumeric.sample_string(&mut rand::rng(), 16);
|
||||
|
||||
b.push_bind(member.id);
|
||||
b.push_bind(member.first_name);
|
||||
b.push_bind(member.full_name);
|
||||
b.push_bind(registration_token);
|
||||
b.push_bind(member.diploma);
|
||||
b.push_bind(member.hours);
|
||||
b.push_bind(member.groups);
|
||||
});
|
||||
|
||||
let query = query_builder.build();
|
||||
query.execute(&mut **transaction).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_multiple(
|
||||
transaction: &mut sqlx::Transaction<'_, Postgres>,
|
||||
members: Vec<Self>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
for member in members {
|
||||
sqlx::query!("UPDATE ONLY members SET first_name = $1, full_name = $2, diploma = $3, hours = $4, groups = $5 WHERE id = $6", member.first_name, member.full_name, member.diploma, &member.hours, &member.groups, member.id).execute(&mut **transaction).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
6
server/src/database/model/session.rs
Normal file
6
server/src/database/model/session.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
struct Session {
|
||||
id: u32,
|
||||
user_id: u32,
|
||||
token: String,
|
||||
expires: chrono::NaiveDateTime,
|
||||
}
|
8
server/src/database/model/user.rs
Normal file
8
server/src/database/model/user.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
#[derive(validator::Validate)]
|
||||
struct User {
|
||||
pub id: uuid::Uuid,
|
||||
#[validate(email)]
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub admin: bool,
|
||||
}
|
@@ -1 +1,18 @@
|
||||
use routes::member::migrate::MigrationStore;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub mod auth;
|
||||
pub mod database;
|
||||
pub mod model;
|
||||
pub mod routes;
|
||||
pub mod util;
|
||||
|
||||
pub use util::error::Error;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub pool: Pool<Postgres>,
|
||||
pub migration_store: Arc<Mutex<MigrationStore>>,
|
||||
}
|
||||
|
@@ -1,9 +1,13 @@
|
||||
use axum::{http::StatusCode, routing::get, Router};
|
||||
use tokio::net::TcpListener;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::Router;
|
||||
use tokio::{net::TcpListener, sync::Mutex};
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
use wrbapp_server::database;
|
||||
use wrbapp_server::routes::member::migrate::MigrationStore;
|
||||
use wrbapp_server::routes::routes;
|
||||
use wrbapp_server::{database, AppState};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -22,12 +26,19 @@ async fn main() {
|
||||
.await
|
||||
.expect("Database migrations failed");
|
||||
|
||||
database::connect()
|
||||
let pool = database::connect()
|
||||
.await
|
||||
.expect("Database connection failed");
|
||||
|
||||
let migration_store = Arc::new(Mutex::new(MigrationStore::default()));
|
||||
|
||||
let app_state = AppState {
|
||||
pool,
|
||||
migration_store,
|
||||
};
|
||||
|
||||
// Serve app
|
||||
let app = Router::new().route("/", get(hello_world));
|
||||
let app = Router::new().merge(routes()).with_state(app_state);
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:3000")
|
||||
.await
|
||||
@@ -39,7 +50,3 @@ async fn main() {
|
||||
.await
|
||||
.expect("Error while serving axum application");
|
||||
}
|
||||
|
||||
async fn hello_world() -> Result<String, (StatusCode, String)> {
|
||||
Ok("Hello world".to_string())
|
||||
}
|
||||
|
6
server/src/model.rs
Normal file
6
server/src/model.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod member;
|
||||
pub mod session;
|
||||
pub mod user;
|
||||
|
||||
pub use member::Member;
|
||||
pub use user::User;
|
46
server/src/model/member.rs
Normal file
46
server/src/model/member.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
pub struct Name {
|
||||
pub first: String,
|
||||
pub full: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
pub struct Member {
|
||||
pub id: String,
|
||||
pub name: Name,
|
||||
pub registration_token: Option<String>,
|
||||
pub diploma: Option<String>,
|
||||
pub hours: Vec<String>,
|
||||
pub groups: Vec<String>,
|
||||
}
|
||||
|
||||
use crate::database::model::Member as DbMember;
|
||||
impl From<DbMember> for Member {
|
||||
fn from(value: DbMember) -> Self {
|
||||
Member {
|
||||
id: value.id,
|
||||
name: Name {
|
||||
first: value.first_name,
|
||||
full: value.full_name,
|
||||
},
|
||||
registration_token: value.registration_token,
|
||||
diploma: value.diploma,
|
||||
hours: value.hours,
|
||||
groups: value.groups,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Member> for DbMember {
|
||||
fn from(value: Member) -> Self {
|
||||
DbMember {
|
||||
id: value.id,
|
||||
first_name: value.name.first,
|
||||
full_name: value.name.full,
|
||||
registration_token: None,
|
||||
diploma: value.diploma,
|
||||
hours: value.hours,
|
||||
groups: value.groups,
|
||||
}
|
||||
}
|
||||
}
|
0
server/src/model/session.rs
Normal file
0
server/src/model/session.rs
Normal file
5
server/src/model/user.rs
Normal file
5
server/src/model/user.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub struct User {
|
||||
pub id: uuid::Uuid,
|
||||
pub email: String,
|
||||
pub admin: bool,
|
||||
}
|
30
server/src/routes.rs
Normal file
30
server/src/routes.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use member::migrate::{migrate_confirm, migrate_request};
|
||||
|
||||
use crate::{auth::Permissions, AppState};
|
||||
|
||||
pub mod auth;
|
||||
pub mod member;
|
||||
pub mod user;
|
||||
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(root))
|
||||
// .route("/member/:id", get())
|
||||
.route("/members/migrate_request", post(migrate_request))
|
||||
.route("/members/migrate_confirm", post(migrate_confirm))
|
||||
}
|
||||
|
||||
async fn root(
|
||||
State(state): State<AppState>,
|
||||
permissions: Permissions<'_>,
|
||||
) -> Result<String, (StatusCode, String)> {
|
||||
tracing::info!("{:?}", permissions);
|
||||
|
||||
Ok("Hello world".to_string())
|
||||
}
|
1
server/src/routes/auth.rs
Normal file
1
server/src/routes/auth.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
1
server/src/routes/member.rs
Normal file
1
server/src/routes/member.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod migrate;
|
260
server/src/routes/member/migrate.rs
Normal file
260
server/src/routes/member/migrate.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use axum::{extract::State, Json};
|
||||
use itertools::Itertools;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
auth::{AuthError, Permissions},
|
||||
database::model::Member as DbMember,
|
||||
model::{member::Name, Member},
|
||||
util::convert_vec,
|
||||
AppState,
|
||||
};
|
||||
|
||||
pub async fn migrate_request<'a>(
|
||||
State(state): State<AppState>,
|
||||
permissions: Permissions<'a>,
|
||||
body: String,
|
||||
) -> Result<Json<MigrationResponse>, crate::Error> {
|
||||
if !permissions.0.contains("root") {
|
||||
return Err(AuthError::NoPermssions.into());
|
||||
}
|
||||
|
||||
// Convert the input CSV to a vector of members
|
||||
let members_new: Vec<Member> = Row::from_csv_multiple(&body)?
|
||||
.into_iter()
|
||||
.map(|m| m.into())
|
||||
.collect();
|
||||
|
||||
// TODO: Write function to get members from database
|
||||
let members_old: Vec<Member> = Vec::new();
|
||||
|
||||
let members_diff = generate_diff(members_new, members_old);
|
||||
|
||||
let count = state
|
||||
.migration_store
|
||||
.lock()
|
||||
.await
|
||||
.insert(members_diff.clone());
|
||||
|
||||
Ok(Json(MigrationResponse::from((count, members_diff))))
|
||||
}
|
||||
|
||||
pub async fn migrate_confirm<'a>(
|
||||
State(state): State<AppState>,
|
||||
permissions: Permissions<'a>,
|
||||
body: String,
|
||||
) -> Result<(), crate::Error> {
|
||||
if !permissions.0.contains("root") {
|
||||
return Err(AuthError::NoPermssions.into());
|
||||
}
|
||||
|
||||
// TODO: Implement better error naming
|
||||
let count = match body.trim().parse::<u32>() {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Err(crate::Error::NotFound),
|
||||
};
|
||||
|
||||
let mut store = state.migration_store.lock().await;
|
||||
|
||||
let members_diff = match store.remove(&count) {
|
||||
Some(m) => m,
|
||||
None => return Err(crate::Error::NotFound),
|
||||
};
|
||||
|
||||
migrate_transaction(&state.pool, members_diff).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn migrate_transaction(pool: &PgPool, members_diff: MembersDiff) -> Result<(), sqlx::Error> {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
// DbMember::insert_multiple(&mut transaction, convert_vec(members_diff.insert)).await?;
|
||||
DbMember::update_multiple(&mut transaction, convert_vec(members_diff.update)).await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Create a row for the csv file
|
||||
#[derive(Debug, serde::Deserialize, Clone)]
|
||||
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>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MembersDiff {
|
||||
insert: Vec<Member>,
|
||||
update: Vec<Member>,
|
||||
remove: Vec<Member>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct MigrationResponse {
|
||||
count: u32,
|
||||
insert: Vec<(String, Name)>,
|
||||
update: Vec<(String, Name)>,
|
||||
remove: Vec<(String, Name)>,
|
||||
}
|
||||
|
||||
pub struct MigrationStore {
|
||||
pub store: HashMap<u32, MembersDiff>,
|
||||
pub count: u32,
|
||||
}
|
||||
|
||||
impl Default for MigrationStore {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
count: 0,
|
||||
store: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Row {
|
||||
fn from_csv_multiple(input: &str) -> Result<Vec<Self>, csv::Error> {
|
||||
let mut rdr = csv::ReaderBuilder::new()
|
||||
.delimiter(b';')
|
||||
.from_reader(input.as_bytes());
|
||||
|
||||
let members: Result<Vec<Row>, csv::Error> = rdr.deserialize().collect();
|
||||
|
||||
members
|
||||
}
|
||||
|
||||
fn hours_parsed(&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.into_iter().unique().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Name> for Row {
|
||||
fn into(self) -> Name {
|
||||
Name {
|
||||
first: self.first_name,
|
||||
full: "Temporarely full name".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Member> for Row {
|
||||
fn into(self) -> Member {
|
||||
let name: Name = self.clone().into();
|
||||
|
||||
Member {
|
||||
id: self.id.clone(),
|
||||
name,
|
||||
registration_token: None,
|
||||
diploma: self.diploma.clone(),
|
||||
hours: self.hours_parsed(),
|
||||
groups: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u32, MembersDiff)> for MigrationResponse {
|
||||
fn from(value: (u32, MembersDiff)) -> Self {
|
||||
let members_insert: Vec<(String, Name)> =
|
||||
value.1.insert.into_iter().map(|m| (m.id, m.name)).collect();
|
||||
let members_update: Vec<(String, Name)> =
|
||||
value.1.update.into_iter().map(|m| (m.id, m.name)).collect();
|
||||
let members_remove: Vec<(String, Name)> =
|
||||
value.1.remove.into_iter().map(|m| (m.id, m.name)).collect();
|
||||
|
||||
Self {
|
||||
count: value.0,
|
||||
insert: members_insert,
|
||||
update: members_update,
|
||||
remove: members_remove,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MigrationStore {
|
||||
fn insert(&mut self, members_diff: MembersDiff) -> u32 {
|
||||
let count = self.count + 1;
|
||||
self.store.insert(count, members_diff);
|
||||
self.count = count;
|
||||
count
|
||||
}
|
||||
|
||||
fn get(&self, id: &u32) -> Option<&MembersDiff> {
|
||||
self.store.get(id)
|
||||
}
|
||||
|
||||
fn remove(&mut self, id: &u32) -> Option<MembersDiff> {
|
||||
self.store.remove(id)
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_diff(members_new: Vec<Member>, members_old: Vec<Member>) -> MembersDiff {
|
||||
let members_old_map: HashMap<String, Member> = members_old
|
||||
.iter()
|
||||
.map(|m| (m.id.clone(), m.clone()))
|
||||
.collect();
|
||||
|
||||
let members_new_map: HashMap<String, Member> = members_new
|
||||
.iter()
|
||||
.map(|m| (m.id.clone(), m.clone()))
|
||||
.collect();
|
||||
|
||||
let mut members_insert: Vec<Member> = Vec::new();
|
||||
let mut members_update: Vec<Member> = Vec::new();
|
||||
let mut members_remove: Vec<Member> = Vec::new();
|
||||
|
||||
for old_member in members_old {
|
||||
if let Some(new_member) = members_new_map.get(&old_member.id) {
|
||||
members_update.push(Member {
|
||||
id: old_member.id,
|
||||
name: new_member.name.clone(),
|
||||
registration_token: old_member.registration_token,
|
||||
diploma: new_member.diploma.clone(),
|
||||
hours: new_member.hours.clone(),
|
||||
groups: old_member.groups,
|
||||
})
|
||||
} else {
|
||||
members_remove.push(old_member);
|
||||
}
|
||||
}
|
||||
|
||||
for new_member in members_new {
|
||||
if !members_old_map.contains_key(&new_member.id) {
|
||||
members_insert.push(new_member);
|
||||
}
|
||||
}
|
||||
|
||||
MembersDiff {
|
||||
insert: members_insert,
|
||||
update: members_update,
|
||||
remove: members_remove,
|
||||
}
|
||||
}
|
1
server/src/routes/user.rs
Normal file
1
server/src/routes/user.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
4
server/src/util.rs
Normal file
4
server/src/util.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod error;
|
||||
mod helpers;
|
||||
|
||||
pub use helpers::convert_vec;
|
52
server/src/util/error.rs
Normal file
52
server/src/util/error.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use crate::auth::AuthError;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("CSV error: {0}")]
|
||||
Csv(#[from] csv::Error),
|
||||
|
||||
#[error("Auth error: {0}")]
|
||||
Auth(#[from] crate::auth::AuthError),
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] sqlx::Error),
|
||||
|
||||
#[error("Resource not found")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> Response {
|
||||
tracing::error!("Error... {:?}", self);
|
||||
|
||||
let (status, error_message) = match self {
|
||||
Error::Auth(AuthError::NoPermssions) => {
|
||||
(StatusCode::UNAUTHORIZED, String::from("No permissions"))
|
||||
}
|
||||
Error::Auth(AuthError::InvalidToken) => {
|
||||
(StatusCode::BAD_REQUEST, String::from("Invalid token"))
|
||||
}
|
||||
Error::Auth(AuthError::Unexpected) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
String::from("Unexpected error occured"),
|
||||
),
|
||||
Error::Csv(err) => (StatusCode::BAD_REQUEST, err.to_string()),
|
||||
Error::NotFound => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
String::from("Could not find resource"),
|
||||
),
|
||||
Error::Database(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
|
||||
};
|
||||
|
||||
let body = Json(serde_json::json!({
|
||||
"error": error_message
|
||||
}));
|
||||
|
||||
(status, body).into_response()
|
||||
}
|
||||
}
|
6
server/src/util/helpers.rs
Normal file
6
server/src/util/helpers.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub fn convert_vec<T, U>(vec: Vec<T>) -> Vec<U>
|
||||
where
|
||||
U: From<T>,
|
||||
{
|
||||
vec.into_iter().map(U::from).collect()
|
||||
}
|
Reference in New Issue
Block a user