summaryrefslogtreecommitdiff
path: root/src/channel
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2024-09-18 22:49:38 -0400
committerOwen Jacobson <owen@grimoire.ca>2024-09-20 16:42:44 -0400
commit22348bfa35f009e62abe2f30863e0434079a1fe2 (patch)
treec5b5b5e660a1ee2a05785f4669102c1023b6e7b0 /src/channel
parentaafdeb9ffaf9a993ca4462b3422667e04469b2e3 (diff)
Remove the HTML client, and expose a JSON API.
This API structure fell out of a conversation with Kit. Described loosely: kit: ok kit: Here's what I'm picturing in a client kit: list channels, make-new-channel, zero to one active channels, post-to-active. kit: login/sign-up, logout owen: you will likely also want "am I logged in" here kit: sure, whoami
Diffstat (limited to 'src/channel')
-rw-r--r--src/channel/app.rs32
-rw-r--r--src/channel/routes.rs68
2 files changed, 81 insertions, 19 deletions
diff --git a/src/channel/app.rs b/src/channel/app.rs
index 2f37878..8ae0c3c 100644
--- a/src/channel/app.rs
+++ b/src/channel/app.rs
@@ -31,13 +31,17 @@ impl<'a> Channels<'a> {
Self { db, broadcaster }
}
- pub async fn create(&self, name: &str) -> Result<(), InternalError> {
+ pub async fn create(&self, name: &str) -> Result<Channel, CreateError> {
let mut tx = self.db.begin().await?;
- let channel = tx.channels().create(name).await?;
- self.broadcaster.register_channel(&channel);
+ let channel = tx
+ .channels()
+ .create(name)
+ .await
+ .map_err(|err| CreateError::from_duplicate_name(err, name))?;
+ self.broadcaster.register_channel(&channel.id);
tx.commit().await?;
- Ok(())
+ Ok(channel)
}
pub async fn all(&self) -> Result<Vec<Channel>, InternalError> {
@@ -122,6 +126,26 @@ impl<'a> Channels<'a> {
}
#[derive(Debug, thiserror::Error)]
+pub enum CreateError {
+ #[error("channel named {0} already exists")]
+ DuplicateName(String),
+ #[error(transparent)]
+ DatabaseError(#[from] sqlx::Error),
+}
+
+impl CreateError {
+ fn from_duplicate_name(error: sqlx::Error, name: &str) -> Self {
+ if let Some(error) = error.as_database_error() {
+ if error.is_unique_violation() {
+ return Self::DuplicateName(name.into());
+ }
+ }
+
+ Self::from(error)
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
pub enum InternalError {
#[error(transparent)]
DatabaseError(#[from] sqlx::Error),
diff --git a/src/channel/routes.rs b/src/channel/routes.rs
index 847e0b4..383ec58 100644
--- a/src/channel/routes.rs
+++ b/src/channel/routes.rs
@@ -1,23 +1,43 @@
use axum::{
- extract::{Form, Path, State},
+ extract::{Json, Path, State},
http::StatusCode,
- response::{IntoResponse, Redirect, Response},
- routing::post,
+ response::{IntoResponse, Response},
+ routing::{get, post},
Router,
};
-use super::app::EventsError;
+use super::app::{self, EventsError};
use crate::{
app::App,
clock::RequestedAt,
error::InternalError,
- repo::{channel, login::Login},
+ repo::{
+ channel::{self, Channel},
+ login::Login,
+ },
};
pub fn router() -> Router<App> {
Router::new()
- .route("/create", post(on_create))
- .route("/:channel/send", post(on_send))
+ .route("/api/channels", get(list_channels))
+ .route("/api/channels", post(on_create))
+ .route("/api/channels/:channel", post(on_send))
+}
+
+async fn list_channels(State(app): State<App>, _: Login) -> Result<Channels, InternalError> {
+ let channels = app.channels().all().await?;
+ let response = Channels(channels);
+
+ Ok(response)
+}
+
+struct Channels(Vec<Channel>);
+
+impl IntoResponse for Channels {
+ fn into_response(self) -> Response {
+ let Self(channels) = self;
+ Json(channels).into_response()
+ }
}
#[derive(serde::Deserialize)]
@@ -28,11 +48,29 @@ struct CreateRequest {
async fn on_create(
State(app): State<App>,
_: Login, // requires auth, but doesn't actually care who you are
- Form(form): Form<CreateRequest>,
-) -> Result<impl IntoResponse, InternalError> {
- app.channels().create(&form.name).await?;
+ Json(form): Json<CreateRequest>,
+) -> Result<Json<Channel>, CreateError> {
+ let channel = app
+ .channels()
+ .create(&form.name)
+ .await
+ .map_err(CreateError)?;
- Ok(Redirect::to("/"))
+ Ok(Json(channel))
+}
+
+struct CreateError(app::CreateError);
+
+impl IntoResponse for CreateError {
+ fn into_response(self) -> Response {
+ let Self(error) = self;
+ match error {
+ duplicate @ app::CreateError::DuplicateName(_) => {
+ (StatusCode::BAD_REQUEST, duplicate.to_string()).into_response()
+ }
+ other => InternalError::from(other).into_response(),
+ }
+ }
}
#[derive(serde::Deserialize)]
@@ -45,15 +83,15 @@ async fn on_send(
RequestedAt(sent_at): RequestedAt,
State(app): State<App>,
login: Login,
- Form(form): Form<SendRequest>,
-) -> Result<impl IntoResponse, ErrorResponse> {
+ Json(form): Json<SendRequest>,
+) -> Result<StatusCode, ErrorResponse> {
app.channels()
.send(&login, &channel, &form.message, &sent_at)
.await
// Could impl `From` here, but it's more code and this is used once.
.map_err(ErrorResponse)?;
- Ok(Redirect::to(&format!("/{}", channel)))
+ Ok(StatusCode::ACCEPTED)
}
struct ErrorResponse(EventsError);
@@ -65,7 +103,7 @@ impl IntoResponse for ErrorResponse {
not_found @ EventsError::ChannelNotFound(_) => {
(StatusCode::NOT_FOUND, not_found.to_string()).into_response()
}
- EventsError::DatabaseError(error) => InternalError::from(error).into_response(),
+ other => InternalError::from(other).into_response(),
}
}
}