Add route for getting most popular posts
This commit is contained in:
parent
6979e73307
commit
3ee97b5d43
4 changed files with 84 additions and 8 deletions
|
|
@ -18,8 +18,7 @@ impl Env {
|
||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let port = env::var("PORT")
|
let port = env::var("PORT").unwrap_or_else(|_| "3000".to_string());
|
||||||
.unwrap_or_else(|_| "3000".to_string());
|
|
||||||
|
|
||||||
Env {
|
Env {
|
||||||
postgres_connection_string,
|
postgres_connection_string,
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ async fn main() {
|
||||||
|
|
||||||
let state = AppState::new().await;
|
let state = AppState::new().await;
|
||||||
let port = &state.env.port;
|
let port = &state.env.port;
|
||||||
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}"))
|
||||||
tracing::info!("listening on {}", listener.local_addr().unwrap());
|
|
||||||
axum::serve(listener, upvoters::app(state))
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
tracing::info!("listening on {}", listener.local_addr().unwrap());
|
||||||
|
axum::serve(listener, upvoters::app(state)).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
Json, Router,
|
Json, Router,
|
||||||
extract::{Path, State},
|
extract::{Path, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{delete, get, post},
|
routing::{delete, get, post},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::{CookieJar, cookie::Cookie};
|
use axum_extra::extract::{CookieJar, cookie::Cookie};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
state::AppState,
|
state::AppState,
|
||||||
votes::{
|
votes::{
|
||||||
repository::{delete_vote, insert_new_vote},
|
repository::{delete_vote, get_top_n_slugs, insert_new_vote},
|
||||||
service::get_votes_and_voted,
|
service::get_votes_and_voted,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -22,6 +23,7 @@ pub fn router() -> Router<AppState> {
|
||||||
.route("/posts/{slug}/vote", post(upvote_handler))
|
.route("/posts/{slug}/vote", post(upvote_handler))
|
||||||
.route("/posts/{slug}/vote", delete(delete_vote_handler))
|
.route("/posts/{slug}/vote", delete(delete_vote_handler))
|
||||||
.route("/posts/{slug}/votes", get(get_votes_and_voted_handler))
|
.route("/posts/{slug}/votes", get(get_votes_and_voted_handler))
|
||||||
|
.route("/posts/best", get(get_best_handler))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_or_init_voter_id(jar: CookieJar) -> (CookieJar, Uuid) {
|
fn get_or_init_voter_id(jar: CookieJar) -> (CookieJar, Uuid) {
|
||||||
|
|
@ -110,6 +112,30 @@ async fn get_votes_and_voted_handler(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct BestVotesQuery {
|
||||||
|
top: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_best_handler(
|
||||||
|
jar: CookieJar,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
query: Query<BestVotesQuery>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match get_top_n_slugs(query.top as i64, &state.db).await {
|
||||||
|
Ok(top_slugs) => {
|
||||||
|
info!(n=%query.top, "fetched most popular slugs successfully");
|
||||||
|
(StatusCode::OK, jar, Json(top_slugs)).into_response()
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
error = %err,
|
||||||
|
"failed to fetch popular slugs"
|
||||||
|
);
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, jar, "Failed to upvote").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use axum::{
|
use axum::{
|
||||||
|
|
@ -333,4 +359,54 @@ mod tests {
|
||||||
|
|
||||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_best_returns_top_slugs_limited_by_query() {
|
||||||
|
let db = test_pool().await;
|
||||||
|
let slugs = [
|
||||||
|
"get_best_handler_top_post_1",
|
||||||
|
"get_best_handler_top_post_2",
|
||||||
|
"get_best_handler_top_post_3",
|
||||||
|
];
|
||||||
|
|
||||||
|
for slug in slugs {
|
||||||
|
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
for voter_id in [
|
||||||
|
Uuid::from_u128(0x100),
|
||||||
|
Uuid::from_u128(0x101),
|
||||||
|
Uuid::from_u128(0x102),
|
||||||
|
] {
|
||||||
|
insert_new_vote(slugs[0], &voter_id, &db).await.unwrap();
|
||||||
|
}
|
||||||
|
for voter_id in [Uuid::from_u128(0x200), Uuid::from_u128(0x201)] {
|
||||||
|
insert_new_vote(slugs[1], &voter_id, &db).await.unwrap();
|
||||||
|
}
|
||||||
|
insert_new_vote(slugs[2], &Uuid::from_u128(0x300), &db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let app = crate::app(test_state(db.clone()));
|
||||||
|
let request = Request::builder()
|
||||||
|
.method("GET")
|
||||||
|
.uri("/posts/best?top=2")
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = app.oneshot(request).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
||||||
|
let json: Value = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(json.as_array().unwrap().len(), 2);
|
||||||
|
assert_eq!(json[0]["slug"], slugs[0]);
|
||||||
|
assert_eq!(json[0]["vote_count"], 3);
|
||||||
|
assert_eq!(json[1]["slug"], slugs[1]);
|
||||||
|
assert_eq!(json[1]["vote_count"], 2);
|
||||||
|
|
||||||
|
for slug in slugs {
|
||||||
|
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::prelude::FromRow;
|
use sqlx::prelude::FromRow;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
@ -11,7 +12,7 @@ pub struct Vote {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Struct representing the best slug posts
|
/// Struct representing the best slug posts
|
||||||
#[derive(Debug, FromRow)]
|
#[derive(Debug, FromRow, Serialize, Deserialize)]
|
||||||
pub struct BestSlugs {
|
pub struct BestSlugs {
|
||||||
pub slug: String,
|
pub slug: String,
|
||||||
pub vote_count: i64,
|
pub vote_count: i64,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue