From 7ded3ef4304078fbfd5a1ada25bcd67e7542c135 Mon Sep 17 00:00:00 2001 From: finga Date: Thu, 2 Feb 2023 06:51:04 +0100 Subject: [PATCH] Creation of a reminder via web api Concurrently run the reminder service as well as an web endpoint to create a new reminder. Note that a new reminder does not notify the reminder service yet. Cargo update dependencies --- Cargo.lock | 36 +++++++++++++-------------- Cargo.toml | 2 +- src/api.rs | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 4 ++- src/main.rs | 68 ++++++++++++++++++++++++-------------------------- 5 files changed, 123 insertions(+), 56 deletions(-) create mode 100644 src/api.rs diff --git a/Cargo.lock b/Cargo.lock index 5b70df6..b8ad3d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] name = "async-trait" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff18d764974428cf3a9328e23fc5c986f5fbed46e6cd4cdf42544df5d297ec1" +checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" dependencies = [ "proc-macro2", "quote", @@ -95,9 +95,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" @@ -289,36 +289,36 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" [[package]] name = "futures-io" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" +checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" [[package]] name = "futures-task" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" +checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" [[package]] name = "futures-util" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" dependencies = [ "futures-core", "futures-io", @@ -835,9 +835,9 @@ dependencies = [ [[package]] name = "quoted_printable" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb" +checksum = "a24039f627d8285853cc90dcddf8c1ebfaa91f834566948872b225b9a28ed1b6" [[package]] name = "r2d2" @@ -994,9 +994,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c68e921cef53841b8925c2abadd27c9b891d9613bdc43d6b823062866df38e8" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" dependencies = [ "serde", ] diff --git a/Cargo.toml b/Cargo.toml index b446843..3b358cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ categories = ["utility"] anyhow = "1" tracing = "0.1" tracing-subscriber = "0.3" -time = "0.3" +time = { version = "0.3", features = ["serde-human-readable"] } lettre = { version = "0.10", features = ["tracing"] } diesel = { version = "2", features = ["postgres", "r2d2", "time"] } serde = { version = "1", features = ["derive"] } diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..a7dfd3c --- /dev/null +++ b/src/api.rs @@ -0,0 +1,69 @@ +use crate::{schema, NewReminder}; +use anyhow::Error; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Response, Result}, + Json, +}; +use diesel::{ + prelude::*, + r2d2::{ConnectionManager, Pool}, +}; +use serde::Deserialize; +use time::OffsetDateTime; +use tracing::{error, trace}; + +pub struct ServerError(Error); + +impl IntoResponse for ServerError { + fn into_response(self) -> Response { + error!("{}", self.0); + (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong.").into_response() + } +} + +impl From for ServerError +where + E: Into, +{ + fn from(err: E) -> Self { + Self(err.into()) + } +} + +#[derive(Debug, Deserialize)] +pub struct CreateReminder { + #[serde(with = "time::serde::iso8601")] + planned: OffsetDateTime, + title: String, + message: String, + receiver: String, +} + +#[allow(clippy::unused_async)] +pub async fn create_reminder( + State(db_pool): State>>, + Json(data): Json, +) -> Result { + let reminder = NewReminder { + created: OffsetDateTime::now_utc(), + planned: data.planned, + title: &data.title, + message: &data.message, + receiver: &data.receiver, + }; + + trace!(?data, "received data"); + + diesel::insert_into(schema::reminders::table) + .values(&reminder) + .execute(&mut db_pool.get()?)?; + + Ok((StatusCode::CREATED, "Reminder created".to_string())) +} + +#[allow(clippy::unused_async)] +pub async fn not_found() -> impl IntoResponse { + (StatusCode::NOT_FOUND, "Page not found") +} diff --git a/src/config.rs b/src/config.rs index 627a7be..fe40485 100644 --- a/src/config.rs +++ b/src/config.rs @@ -106,7 +106,9 @@ impl Config { xdg::BaseDirectories::with_prefix(env!("CARGO_BIN_NAME"))?.get_config_file(file); match Self::load_file(&user_config) { Ok(config) => return Ok(config), - Err(error) => warn!(file = ?user_config, "cannot load configuration: {:#}", Error::msg(error)), + Err(error) => { + warn!(file = ?user_config, "cannot load configuration: {:#}", Error::msg(error)); + } } let global_config = format!("/etc/{}/{}", env!("CARGO_BIN_NAME"), file); diff --git a/src/main.rs b/src/main.rs index 708e6cd..e52aee9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,18 @@ -use anyhow::Result; -use axum::{http::StatusCode, response::IntoResponse, Router}; +use anyhow::{Error, Result}; +use axum::{routing::post, Router}; use clap::Parser; use diesel::{ prelude::*, - r2d2::{ConnectionManager, Pool, PooledConnection}, + r2d2::{ConnectionManager, Pool}, }; use lettre::{Message, SmtpTransport, Transport}; -use std::{env, net::SocketAddr}; -use time::{Duration, OffsetDateTime}; +use std::{env, net::SocketAddr, time::Duration}; +use time::OffsetDateTime; use tower::ServiceBuilder; use tower_http::trace::TraceLayer; use tracing::{debug, info, trace}; +mod api; mod args; mod config; mod models; @@ -21,8 +22,22 @@ use args::Args; use config::Config; use models::{NewReminder, Reminder}; +fn get_connection_pool(config: &Config) -> Result>> { + Ok( + Pool::builder().build(ConnectionManager::::new(format!( + "postgresql://{}:{}@{}:{}/{}", + config.database.user, + config.database.pass, + config.database.host, + config.database.port, + config.database.name + )))?, + ) +} + +#[allow(clippy::cognitive_complexity)] fn remind( - db: &mut PooledConnection>, + db_pool: &mut Pool>, mailer: &SmtpTransport, ) -> Result<()> { info!("checking for reminders"); @@ -30,7 +45,7 @@ fn remind( let result = schema::reminders::dsl::reminders .filter(schema::reminders::executed.is_null()) .order(schema::reminders::planned.asc()) - .load::(db)?; + .load::(&mut db_pool.get()?)?; if result.is_empty() { info!("no reminders present, parking indefinitely"); @@ -49,14 +64,14 @@ fn remind( diesel::update(&reminder) .set(schema::reminders::executed.eq(Some(OffsetDateTime::now_utc()))) - .execute(db)?; + .execute(&mut db_pool.get()?)?; debug!("email sent to {}", reminder.receiver); } else { let duration = reminder.planned - OffsetDateTime::now_utc(); info!(?duration, "parking reminder"); - std::thread::park_timeout(::try_from(duration)?); + std::thread::park_timeout(::try_from(duration)?); return Ok(()); } } @@ -65,11 +80,6 @@ fn remind( Ok(()) } -#[allow(clippy::unused_async)] -async fn not_found() -> impl IntoResponse { - (StatusCode::NOT_FOUND, "Page not found") -} - #[tokio::main] async fn main() -> Result<()> { let args = Args::parse(); @@ -87,38 +97,24 @@ async fn main() -> Result<()> { ); let config = Config::load_config(args.config)?; - let db_pool = Pool::builder().build(ConnectionManager::::new(format!( - "postgresql://{}:{}@{}:{}/{}", - config.database.user, - config.database.pass, - config.database.host, - config.database.port, - config.database.name - )))?; - let test_reminder = NewReminder { - created: OffsetDateTime::now_utc(), - planned: (OffsetDateTime::now_utc() + Duration::MINUTE), - title: "Test title", - message: "Test message", - receiver: "finga@localhost", - }; + let mut db_pool = get_connection_pool(&config)?; - diesel::insert_into(schema::reminders::table) - .values(&test_reminder) - .execute(&mut db_pool.get()?)?; - - std::thread::spawn(move || { + std::thread::spawn(move || -> Result<(), Error> { let mailer = SmtpTransport::unencrypted_localhost(); loop { - remind(&mut db_pool.get().unwrap(), &mailer).unwrap(); + remind(&mut db_pool, &mailer)?; } }); + let db_pool = get_connection_pool(&config)?; + let app = Router::new() + .route("/v1/reminder", post(api::create_reminder)) .layer(ServiceBuilder::new().layer(TraceLayer::new_for_http())) - .fallback(not_found); + .fallback(api::not_found) + .with_state(db_pool); let addr = SocketAddr::from((config.server.address, config.server.port));