summaryrefslogtreecommitdiff
path: root/src/boot
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2024-10-05 22:42:43 -0400
committerOwen Jacobson <owen@grimoire.ca>2024-10-05 22:47:12 -0400
commit6a10fcaf64938da52b326ea80013d9f30ed62a6c (patch)
tree08a3860b68391514390f42872ccc1cb4c6e6afd2 /src/boot
parent1fb26ad31d385ddc628e1b73d6a8764981ca6885 (diff)
Separate `/api/boot` into its own module.
Diffstat (limited to 'src/boot')
-rw-r--r--src/boot/app.rs54
-rw-r--r--src/boot/mod.rs74
-rw-r--r--src/boot/routes.rs27
-rw-r--r--src/boot/routes/test.rs14
4 files changed, 169 insertions, 0 deletions
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<Snapshot, sqlx::Error> {
+ 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<Channel>,
+}
+
+#[derive(serde::Serialize)]
+pub struct Channel {
+ pub id: channel::Id,
+ pub name: String,
+ pub messages: Vec<Message>,
+}
+
+impl Channel {
+ fn new(
+ channel: channel::Channel,
+ messages: impl IntoIterator<Item = message::Message>,
+ ) -> 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<message::Message> 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<App> {
+ Router::new().route("/api/boot", get(boot))
+}
+
+async fn boot(State(app): State<App>, login: Login) -> Result<Json<Boot>, 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);
+}