From 44a35e651741afb6c417da47d636e4380cdd225f Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Sat, 13 Mar 2021 10:17:02 +0100 Subject: feat: add get all repos from db --- Cargo.lock | 18 +++++++++-- Cargo.toml | 11 ++++--- schema.sql | 31 +++++++++++++++++++ src/db.rs | 8 +++++ src/errors.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 16 +++++----- src/repository/mod.rs | 2 ++ src/repository/models.rs | 36 ++++++++++++++++++++++ src/repository/routes.rs | 23 ++++++++++++++ 9 files changed, 210 insertions(+), 15 deletions(-) create mode 100644 schema.sql create mode 100644 src/db.rs create mode 100644 src/errors.rs create mode 100644 src/repository/mod.rs create mode 100644 src/repository/models.rs create mode 100644 src/repository/routes.rs diff --git a/Cargo.lock b/Cargo.lock index fb84b41..a9ca561 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -327,9 +327,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "async-trait" -version = "0.1.47" +version = "0.1.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e098e9c493fdf92832223594d9a164f96bdf17ba81a42aff86f85c76768726a" +checksum = "36ea56748e10732c49404c153638a15ec3d6211ec5ff35d9bb20e13b93576adf" dependencies = [ "proc-macro2", "quote", @@ -491,6 +491,7 @@ dependencies = [ "libc", "num-integer", "num-traits 0.2.14", + "serde 1.0.124", "time", "winapi 0.3.9", ] @@ -908,6 +909,7 @@ dependencies = [ "actix-rt", "actix-service", "actix-web", + "chrono", "config", "deadpool-postgres", "dotenv", @@ -918,6 +920,7 @@ dependencies = [ "tokio-pg-mapper", "tokio-pg-mapper-derive", "tokio-postgres", + "uuid", ] [[package]] @@ -1447,8 +1450,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfc08a7d94a80665de4a83942fa8db2fdeaf2f123fc0535e384dc4fff251efae" dependencies = [ "bytes 0.5.6", + "chrono", "fallible-iterator", "postgres-protocol", + "uuid", ] [[package]] @@ -2094,6 +2099,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "serde 1.0.124", +] + [[package]] name = "version_check" version = "0.9.2" diff --git a/Cargo.toml b/Cargo.toml index f0d9f2e..f57b112 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,11 @@ actix-web = "2.0.0" actix-http = "1.0.1" actix-service = "1.0.5" +tokio-pg-mapper = "0.1.4" +tokio-pg-mapper-derive = "0.1.4" +deadpool-postgres = "0.5.0" +tokio-postgres = { version = "0.5.1", features = ["with-uuid-0_8", "with-chrono-0_4"] } + slog = "2.4.1" slog-term = "2.5.0" slog-async = "2.4.0" @@ -17,7 +22,5 @@ slog-async = "2.4.0" dotenv = "0.15.0" config = "0.10.1" serde = { version = "1.0.104", features = ["derive"] } -deadpool-postgres = "0.5.0" -tokio-postgres = "0.5.1" -tokio-pg-mapper = "0.1.4" -tokio-pg-mapper-derive = "0.1" +chrono = { version = "0.4.19", features = ["serde"] } +uuid = { version = "0.8.2", features = ["serde"] } diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..9daa7f9 --- /dev/null +++ b/schema.sql @@ -0,0 +1,31 @@ +CREATE TABLE "repository" ( + id uuid PRIMARY KEY NOT NULL, + url varchar(255) UNIQUE NOT NULL, + created_at timestamp NOT NULL, + updated_at timestamp NOT NULL, + uploader_ip varchar(15) NOT NULL +); + +CREATE TABLE "email"( + email varchar(120) PRIMARY KEY NOT NULL, + hash_md5 varchar(32) UNIQUE NOT NULL +); + +CREATE TABLE "commit" ( + hash varchar(40) PRIMARY KEY NOT NULL, + tree varchar(40) REFERENCES commit(hash) NULL, + text text NOT NULL, + date timestamp NOT NULL, + author_email varchar(120) REFERENCES email(email) NOT NULL, + author_name varchar(120) NOT NULL, + committer_email varchar(120) REFERENCES email(email) NOT NULL, + committer_name varchar(120) NOT NULL, + repository_url varchar(256) REFERENCES repository(url) NOT NULL +); + +CREATE TABLE "branch" ( + id uuid PRIMARY KEY NOT NULL, + name varchar(120) NOT NULL, + repository_id uuid REFERENCES repository(id) NOT NULL, + head varchar(40) REFERENCES commit(hash) NULL +); diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..f547ab2 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,8 @@ +use crate::errors::AppError; +use deadpool_postgres::{Client, Pool, PoolError}; + +pub async fn get_client(pool: Pool) -> Result { + pool.get() + .await + .map_err(|err: PoolError| AppError::from(err)) +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..ee3ca71 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,80 @@ +use actix_web::{error::ResponseError, http::StatusCode, HttpResponse}; +use deadpool_postgres::PoolError; +use serde::Serialize; +use std::fmt; +use tokio_postgres::error::Error; + +#[derive(Debug)] +pub enum AppErrorType { + DbError, + NotFoundError, +} + +#[derive(Debug)] +pub struct AppError { + pub message: Option, + pub cause: Option, + pub error_type: AppErrorType, +} + +impl AppError { + pub fn message(&self) -> String { + match &*self { + AppError { + message: Some(message), + .. + } => message.clone(), + AppError { + message: None, + error_type: AppErrorType::NotFoundError, + .. + } => "The requested item was not found".to_string(), + _ => "An unexpected error has occurred".to_string(), + } + } +} + +impl From for AppError { + fn from(error: PoolError) -> AppError { + AppError { + message: None, + cause: Some(error.to_string()), + error_type: AppErrorType::DbError, + } + } +} + +impl From for AppError { + fn from(error: Error) -> AppError { + AppError { + message: None, + cause: Some(error.to_string()), + error_type: AppErrorType::DbError, + } + } +} + +impl fmt::Display for AppError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "{:?}", self) + } +} + +#[derive(Serialize)] +pub struct AppErrorResponse { + pub error: String, +} + +impl ResponseError for AppError { + fn status_code(&self) -> StatusCode { + match self.error_type { + AppErrorType::DbError => StatusCode::INTERNAL_SERVER_ERROR, + AppErrorType::NotFoundError => StatusCode::NOT_FOUND, + } + } + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).json(AppErrorResponse { + error: self.message(), + }) + } +} diff --git a/src/main.rs b/src/main.rs index 0b3d67f..176dae1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,15 @@ mod config; +mod db; +mod errors; -use actix_web::{middleware, web, App, HttpServer}; +mod repository; + +use actix_web::{middleware, App, HttpServer}; use dotenv::dotenv; use slog::info; use tokio_postgres::NoTls; -use crate::config::{Config, AppState}; - -async fn index(state: web::Data) -> &'static str { - info!(state.log, "GET `/` page"); - - "Hello from Rust!" -} +use crate::config::{AppState, Config}; #[actix_rt::main] async fn main() -> std::io::Result<()> { @@ -35,7 +33,7 @@ async fn main() -> std::io::Result<()> { log: log.clone(), }) .wrap(middleware::Logger::default()) - .route("/", web::get().to(index)) + .configure(repository::routes::config) }) .bind(format!("{}:{}", config.server.host, config.server.port))? .run() diff --git a/src/repository/mod.rs b/src/repository/mod.rs new file mode 100644 index 0000000..a0e1883 --- /dev/null +++ b/src/repository/mod.rs @@ -0,0 +1,2 @@ +pub mod models; +pub mod routes; diff --git a/src/repository/models.rs b/src/repository/models.rs new file mode 100644 index 0000000..ec5559b --- /dev/null +++ b/src/repository/models.rs @@ -0,0 +1,36 @@ +use crate::db::get_client; +use crate::errors::AppError; +use chrono::NaiveDateTime; +use deadpool_postgres::Pool; +use serde::{Deserialize, Serialize}; +use tokio_pg_mapper::FromTokioPostgresRow; +use tokio_pg_mapper_derive::PostgresMapper; +use uuid::Uuid; + +#[derive(Serialize, Deserialize, PostgresMapper)] +#[pg_mapper(table = "repository")] +pub struct Repository { + pub id: Uuid, + pub url: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub uploader_ip: String, +} + +impl Repository { + pub async fn find_all(pool: Pool) -> Result, AppError> { + let client = get_client(pool.clone()).await.unwrap(); + let statement = client + .prepare("SELECT * FROM repository ORDER BY updated_at DESC") + .await?; + + let repos = client + .query(&statement, &[]) + .await? + .iter() + .map(|row| Repository::from_row_ref(row).unwrap()) + .collect::>(); + + Ok(repos) + } +} diff --git a/src/repository/routes.rs b/src/repository/routes.rs new file mode 100644 index 0000000..ebfff8e --- /dev/null +++ b/src/repository/routes.rs @@ -0,0 +1,23 @@ +use crate::config::AppState; +use crate::repository::models::Repository; +use actix_web::{web, HttpResponse, Responder}; +use slog::info; + +async fn index(state: web::Data) -> impl Responder { + let result = Repository::find_all(state.pool.clone()).await; + match result { + Ok(repos) => { + info!(state.log, "GET /repo/ 200"); + HttpResponse::Ok().json(repos) + } + _ => { + info!(state.log, "GET /repo/ 500"); + HttpResponse::BadRequest() + .body("Error trying to read all repositories from database") + } + } +} + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(web::resource("/repo/").route(web::get().to(index))); +} -- cgit v1.2.3-18-g5258