diff --git a/backend/public/src/config.rs b/backend/public/src/config.rs new file mode 100644 index 0000000..029adde --- /dev/null +++ b/backend/public/src/config.rs @@ -0,0 +1,11 @@ +use std::path::PathBuf; + +pub fn config() -> Configuration { + Configuration { + env: dotenvy::dotenv(), + } +} + +pub struct Configuration { + env: Result, +} diff --git a/backend/public/src/datasources/comments.rs b/backend/public/src/datasources/comments.rs index 6ae311e..8e1a2f6 100644 --- a/backend/public/src/datasources/comments.rs +++ b/backend/public/src/datasources/comments.rs @@ -1,2 +1,8 @@ +use crate::routes::comments::CommentInput; +use sqlx::PgPool; + pub struct CommentsDatasource; -impl CommentsDatasource {} +impl CommentsDatasource { + pub async fn get_posts_comments(pool: PgPool) {} + pub async fn insert_comment(pool: PgPool, comment_input: CommentInput) {} +} diff --git a/backend/public/src/datasources/posts.rs b/backend/public/src/datasources/posts.rs index 78b4a28..22c1a3a 100644 --- a/backend/public/src/datasources/posts.rs +++ b/backend/public/src/datasources/posts.rs @@ -1,2 +1,11 @@ +use sqlx::PgPool; + pub struct PostsDatasource; -impl PostsDatasource {} +impl PostsDatasource { + pub async fn get_all(pool: PgPool) { + sqlx::query("SELECT title, body, created_at FROM posts ORDER BY created_at DESC LIMIT 10") + .fetch_all(&pool) + .await; + } + pub async fn get_one(pool: PgPool) {} +} diff --git a/backend/public/src/main.rs b/backend/public/src/main.rs index 90c8c82..b054430 100644 --- a/backend/public/src/main.rs +++ b/backend/public/src/main.rs @@ -1,29 +1,37 @@ -use axum::{ - routing::{get, post}, - Router, -}; -use sqlx::postgres::PgPoolOptions; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - +use axum::Router; +use config::config; +use sqlx::{postgres::PgPoolOptions, PgPool}; use std::time::Duration; use tokio::net::TcpListener; +use tokio::signal; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +mod config; mod datasources; mod routes; +pub struct AppState { + db: PgPool, +} + #[tokio::main] async fn main() { - let _ = dotenvy::dotenv(); + let _ = config(); tracing_subscriber::registry() .with( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| format!("{}=debug", env!("CARGO_CRATE_NAME")).into()), + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { + format!( + "{}=debug,tower_http=debug,axum=trace", + env!("CARGO_CRATE_NAME") + ) + .into() + }), ) .with(tracing_subscriber::fmt::layer()) .init(); - let db_connection_str = std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://postgres:password@localhost".to_string()); + let db_connection_str = std::env::var("DATABASE_URL").unwrap(); + // .unwrap_or_else(|_| "postgres://postgres:password@localhost".to_string()); // set up connection pool let pool = PgPoolOptions::new() @@ -31,16 +39,48 @@ async fn main() { .acquire_timeout(Duration::from_secs(3)) .connect(&db_connection_str) .await - .expect("can't connect to database"); + .expect("Failed to connect to database"); + + let app_state = AppState { db: pool.clone() }; // build our application with some routes let app = Router::new() - .route("/", get(routes::root::RootRoute::root)) - .fallback(routes::root::RootRoute::not_found) - .with_state(pool); + .nest("/", routes::root::RootRoute::routes()) + .nest("/posts", routes::posts::PostsRoute::routes(&app_state)) + .nest( + "/comments", + routes::comments::CommentsRoute::routes(&app_state), + ); // run it with hyper let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap(); tracing::debug!("listening on {}", listener.local_addr().unwrap()); - axum::serve(listener, app).await.unwrap(); + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await + .unwrap(); +} + +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("Failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("Failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } } diff --git a/backend/public/src/routes/comments.rs b/backend/public/src/routes/comments.rs index aedde54..f804ae9 100644 --- a/backend/public/src/routes/comments.rs +++ b/backend/public/src/routes/comments.rs @@ -1,2 +1,39 @@ +use crate::{datasources::comments::CommentsDatasource, AppState}; +use axum::{ + extract::{Form, State}, + routing::{get, post}, + Json, +}; +use serde::Deserialize; +use sqlx::PgPool; + +#[derive(Deserialize, Debug)] +pub struct CommentInput { + name: String, + body: String, + post_id: i32, +} + pub struct CommentsRoute; -impl CommentsRoute {} +impl CommentsRoute { + pub fn routes(app_state: &AppState) -> axum::Router { + // add more comment routes here! + axum::Router::new() + .route("/post/:id", get(CommentsRoute::get_post_comments)) + .route("/add", post(CommentsRoute::insert_comment)) + .with_state(app_state.db) + } + + async fn get_post_comments(State(pool): State) -> Json<()> { + let results = CommentsDatasource::get_posts_comments(pool).await; + Json {} + } + + async fn insert_comment( + State(pool): State, + Form(comment_input): Form, + ) -> bool { + let results = CommentsDatasource::insert_comment(pool, comment_input).await; + true + } +} diff --git a/backend/public/src/routes/posts.rs b/backend/public/src/routes/posts.rs index 92fdd55..c19b5b0 100644 --- a/backend/public/src/routes/posts.rs +++ b/backend/public/src/routes/posts.rs @@ -1,2 +1,32 @@ +use crate::{datasources::posts::PostsDatasource, AppState}; +use axum::{extract::State, routing::get, Json, Router}; +use sqlx::PgPool; + pub struct PostsRoute; -impl PostsRoute {} +impl PostsRoute { + pub fn routes(app_state: &AppState) -> Router { + // add more post routes here! + Router::new() + .route("/", get(PostsRoute::get_all)) + .route("/:id", get(PostsRoute::get_one)) + .with_state(app_state.db) + } + + // get all posts + async fn get_all(State(pool): State) -> Json<()> { + let results = PostsDatasource::get_all(pool).await; + Json {} + } + + // get one post + async fn get_one(State(pool): State) -> Json<()> { + let results = PostsDatasource::get_one(pool).await; + Json {} + } + + // get the top three posts with the highest view count + async fn get_popular_posts(State(pool): State) -> Json<()> {} + + // get the top three posts with the most comments + async fn get_hot_posts(State(pool): State) -> Json<()> {} +} diff --git a/backend/public/src/routes/root.rs b/backend/public/src/routes/root.rs index b0f91d3..56b4399 100644 --- a/backend/public/src/routes/root.rs +++ b/backend/public/src/routes/root.rs @@ -1,15 +1,23 @@ use axum::{ http::StatusCode, response::{Html, IntoResponse}, + routing::get, + Router, }; pub struct RootRoute; impl RootRoute { - pub async fn root() -> Html<&'static str> { + pub fn routes() -> Router { + Router::new() + .route("/", get(RootRoute::root)) + .fallback(RootRoute::not_found) + } + + async fn root() -> Html<&'static str> { Html("

Copyright Wyatt J. Miller 2024

") } - pub async fn not_found() -> impl IntoResponse { + async fn not_found() -> impl IntoResponse { (StatusCode::NOT_FOUND, "¯\\_(ツ)_/¯") } }