From 6a10fcaf64938da52b326ea80013d9f30ed62a6c Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Sat, 5 Oct 2024 22:42:43 -0400 Subject: Separate `/api/boot` into its own module. --- src/boot/app.rs | 54 ++++++++++++++++++++++++++++++++++++ src/boot/mod.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++++++ src/boot/routes.rs | 27 ++++++++++++++++++ src/boot/routes/test.rs | 14 ++++++++++ 4 files changed, 169 insertions(+) create mode 100644 src/boot/app.rs create mode 100644 src/boot/mod.rs create mode 100644 src/boot/routes.rs create mode 100644 src/boot/routes/test.rs (limited to 'src/boot') diff --git a/src/boot/app.rs b/src/boot/app.rs new file mode 100644 index 0000000..fc84b3a --- /dev/null +++ b/src/boot/app.rs @@ -0,0 +1,54 @@ +use sqlx::sqlite::SqlitePool; + +use super::{Channel, Snapshot}; +use crate::{ + channel::repo::Provider as _, event::repo::Provider as _, message::repo::Provider as _, +}; + +pub struct Boot<'a> { + db: &'a SqlitePool, +} + +impl<'a> Boot<'a> { + pub const fn new(db: &'a SqlitePool) -> Self { + Self { db } + } + + pub async fn snapshot(&self) -> Result { + let mut tx = self.db.begin().await?; + let resume_point = tx.sequence().current().await?; + let channels = tx.channels().all(resume_point.into()).await?; + + let channels = { + let mut snapshots = Vec::with_capacity(channels.len()); + + let channels = channels.into_iter().filter_map(|channel| { + channel + .as_of(resume_point) + .map(|snapshot| (channel, snapshot)) + }); + + for (channel, snapshot) in channels { + let messages = tx + .messages() + .in_channel(&channel, resume_point.into()) + .await?; + + let messages = messages + .into_iter() + .filter_map(|message| message.as_of(resume_point)); + + snapshots.push(Channel::new(snapshot, messages)); + } + + snapshots + }; + + tx.commit().await?; + + Ok(Snapshot { + resume_point, + channels, + }) + } +} diff --git a/src/boot/mod.rs b/src/boot/mod.rs new file mode 100644 index 0000000..bd0da0a --- /dev/null +++ b/src/boot/mod.rs @@ -0,0 +1,74 @@ +pub mod app; +mod routes; + +use crate::{ + channel, + event::{Instant, Sequence}, + login::Login, + message, +}; + +pub use self::routes::router; + +#[derive(serde::Serialize)] +pub struct Snapshot { + pub resume_point: Sequence, + pub channels: Vec, +} + +#[derive(serde::Serialize)] +pub struct Channel { + pub id: channel::Id, + pub name: String, + pub messages: Vec, +} + +impl Channel { + fn new( + channel: channel::Channel, + messages: impl IntoIterator, + ) -> Self { + // The declarations are like this to guarantee that we aren't omitting any important fields from the corresponding types. + let channel::Channel { id, name } = channel; + + Self { + id, + name, + messages: messages.into_iter().map(Message::from).collect(), + } + } +} + +#[derive(serde::Serialize)] +pub struct Message { + #[serde(flatten)] + pub sent: Instant, + pub sender: Login, + // Named this way for serialization reasons + #[allow(clippy::struct_field_names)] + pub message: Body, +} + +impl From for Message { + fn from(message: message::Message) -> Self { + let message::Message { + sent, + channel: _, + sender, + id, + body, + } = message; + + Self { + sent, + sender, + message: Body { id, body }, + } + } +} + +#[derive(serde::Serialize)] +pub struct Body { + id: message::Id, + body: String, +} diff --git a/src/boot/routes.rs b/src/boot/routes.rs new file mode 100644 index 0000000..80f70bd --- /dev/null +++ b/src/boot/routes.rs @@ -0,0 +1,27 @@ +use axum::{ + extract::{Json, State}, + routing::get, + Router, +}; + +use super::Snapshot; +use crate::{app::App, error::Internal, login::Login}; + +#[cfg(test)] +mod test; + +pub fn router() -> Router { + Router::new().route("/api/boot", get(boot)) +} + +async fn boot(State(app): State, login: Login) -> Result, Internal> { + let snapshot = app.boot().snapshot().await?; + Ok(Boot { login, snapshot }.into()) +} + +#[derive(serde::Serialize)] +struct Boot { + login: Login, + #[serde(flatten)] + snapshot: Snapshot, +} diff --git a/src/boot/routes/test.rs b/src/boot/routes/test.rs new file mode 100644 index 0000000..5f2ba6f --- /dev/null +++ b/src/boot/routes/test.rs @@ -0,0 +1,14 @@ +use axum::extract::{Json, State}; + +use crate::{boot::routes, test::fixtures}; + +#[tokio::test] +async fn returns_identity() { + let app = fixtures::scratch_app().await; + let login = fixtures::login::fictitious(); + let Json(response) = routes::boot(State(app), login.clone()) + .await + .expect("boot always succeeds"); + + assert_eq!(login, response.login); +} -- cgit v1.2.3