diff --git a/blog-server-api/src/endpoints/authors/handler.rs b/blog-server-api/src/endpoints/authors/handler.rs index b614825..beb269e 100644 --- a/blog-server-api/src/endpoints/authors/handler.rs +++ b/blog-server-api/src/endpoints/authors/handler.rs @@ -7,7 +7,6 @@ use blog_server_services::traits::author_service::AuthorService; use screw_api::request::ApiRequest; use screw_api::response::ApiResponse; use std::sync::Arc; -use tokio::join; async fn handler( offset: Option, @@ -17,7 +16,7 @@ async fn handler( let offset = offset.unwrap_or(0).max(0); let limit = limit.unwrap_or(50).max(0).min(50); - let (authors_result, total_result) = join!( + let (authors_result, total_result) = tokio::join!( author_service.authors(&offset, &limit), author_service.authors_count(), ); diff --git a/blog-server-api/src/endpoints/comments/handler.rs b/blog-server-api/src/endpoints/comments/handler.rs new file mode 100644 index 0000000..8544b2f --- /dev/null +++ b/blog-server-api/src/endpoints/comments/handler.rs @@ -0,0 +1,75 @@ +use super::request_content::CommentsRequestContent; +use super::response_content_failure::CommentsResponseContentFailure; +use super::response_content_failure::CommentsResponseContentFailure::*; +use super::response_content_success::CommentsResponseContentSuccess; +use crate::extensions::Resolve; +use blog_server_services::traits::comment_service::CommentService; +use blog_server_services::traits::post_service::PostService; +use screw_api::request::ApiRequest; +use screw_api::response::ApiResponse; +use std::sync::Arc; + +async fn handler( + post_slug: String, + offset: Option, + limit: Option, + comment_service: Arc>, + post_service: Arc>, +) -> Result { + if post_slug.is_empty() { + return Err(PostSlugEmpty); + } + + let post = post_service + .post_by_slug(&post_slug) + .await + .map_err(|e| DatabaseError { + reason: e.to_string(), + })? + .ok_or(PostNotFound)?; + + let offset = offset.unwrap_or(0).max(0); + let limit = limit.unwrap_or(200).max(0).min(200); + + let (comments_result, total_result) = tokio::join!( + comment_service.comments_by_post_id(&post.id, &offset, &limit), + comment_service.comments_count_by_post_id(&post.id), + ); + + let comments = comments_result + .map_err(|e| DatabaseError { + reason: e.to_string(), + })? + .into_iter() + .map(|a| a.into()) + .collect(); + + let total = total_result.map_err(|e| DatabaseError { + reason: e.to_string(), + })?; + + Ok(CommentsResponseContentSuccess { + comments, + total, + offset, + limit, + }) +} + +pub async fn http_handler( + request: ApiRequest, +) -> ApiResponse +where + Extensions: Resolve>> + Resolve>>, +{ + ApiResponse::from( + handler( + request.content.post_slug, + request.content.offset, + request.content.limit, + request.content.comment_service, + request.content.post_service, + ) + .await, + ) +} diff --git a/blog-server-api/src/endpoints/comments/mod.rs b/blog-server-api/src/endpoints/comments/mod.rs new file mode 100644 index 0000000..3426965 --- /dev/null +++ b/blog-server-api/src/endpoints/comments/mod.rs @@ -0,0 +1,6 @@ +mod handler; +mod request_content; +mod response_content_failure; +mod response_content_success; + +pub use handler::http_handler; diff --git a/blog-server-api/src/endpoints/comments/request_content.rs b/blog-server-api/src/endpoints/comments/request_content.rs new file mode 100644 index 0000000..bb0ab53 --- /dev/null +++ b/blog-server-api/src/endpoints/comments/request_content.rs @@ -0,0 +1,42 @@ +use crate::extensions::Resolve; +use blog_server_services::traits::comment_service::*; +use blog_server_services::traits::post_service::*; +use screw_api::request::{ApiRequestContent, ApiRequestOriginContent}; +use std::sync::Arc; + +pub struct CommentsRequestContent { + pub(super) post_slug: String, + pub(super) offset: Option, + pub(super) limit: Option, + pub(super) comment_service: Arc>, + pub(super) post_service: Arc>, +} + +impl ApiRequestContent for CommentsRequestContent +where + Extensions: Resolve>> + Resolve>>, +{ + type Data = (); + + fn create(origin_content: ApiRequestOriginContent) -> Self { + Self { + post_slug: origin_content + .path + .get("post_slug") + .map(|n| n.to_owned()) + .unwrap_or_default(), + offset: origin_content + .query + .get("offset") + .map(|v| v.parse().ok()) + .flatten(), + limit: origin_content + .query + .get("limit") + .map(|v| v.parse().ok()) + .flatten(), + comment_service: origin_content.extensions.resolve(), + post_service: origin_content.extensions.resolve(), + } + } +} diff --git a/blog-server-api/src/endpoints/comments/response_content_failure.rs b/blog-server-api/src/endpoints/comments/response_content_failure.rs new file mode 100644 index 0000000..3b17992 --- /dev/null +++ b/blog-server-api/src/endpoints/comments/response_content_failure.rs @@ -0,0 +1,50 @@ +use hyper::StatusCode; +use screw_api::response::{ApiResponseContentBase, ApiResponseContentFailure}; + +pub enum CommentsResponseContentFailure { + DatabaseError { reason: String }, + PostSlugEmpty, + PostNotFound, +} + +impl ApiResponseContentBase for CommentsResponseContentFailure { + fn status_code(&self) -> &'static StatusCode { + match self { + CommentsResponseContentFailure::DatabaseError { reason: _ } => { + &StatusCode::INTERNAL_SERVER_ERROR + } + CommentsResponseContentFailure::PostSlugEmpty => &StatusCode::BAD_REQUEST, + CommentsResponseContentFailure::PostNotFound => &StatusCode::NOT_FOUND, + } + } +} + +impl ApiResponseContentFailure for CommentsResponseContentFailure { + fn identifier(&self) -> &'static str { + match self { + CommentsResponseContentFailure::DatabaseError { reason: _ } => { + "COMMENTS_DATABASE_ERROR" + } + CommentsResponseContentFailure::PostSlugEmpty => "COMMENTS_POST_SLUG_EMPTY", + CommentsResponseContentFailure::PostNotFound => "COMMENTS_POST_NOT_FOUND", + } + } + + fn reason(&self) -> Option { + Some(match self { + CommentsResponseContentFailure::DatabaseError { reason } => { + if cfg!(debug_assertions) { + format!("database error: {}", reason) + } else { + "internal database error".to_string() + } + } + CommentsResponseContentFailure::PostSlugEmpty => { + "comments root post slug is empty in request URL".to_string() + } + CommentsResponseContentFailure::PostNotFound => { + "comments root post record not found in database".to_string() + } + }) + } +} diff --git a/blog-server-api/src/endpoints/comments/response_content_success.rs b/blog-server-api/src/endpoints/comments/response_content_success.rs new file mode 100644 index 0000000..6873be2 --- /dev/null +++ b/blog-server-api/src/endpoints/comments/response_content_success.rs @@ -0,0 +1,35 @@ +use crate::entities::Comment; +use hyper::StatusCode; +use screw_api::response::{ApiResponseContentBase, ApiResponseContentSuccess}; +use serde::Serialize; + +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CommentsResponseContentSuccess { + pub(super) comments: Vec, + pub(super) total: i64, + pub(super) offset: i64, + pub(super) limit: i64, +} + +impl ApiResponseContentBase for CommentsResponseContentSuccess { + fn status_code(&self) -> &'static StatusCode { + &StatusCode::OK + } +} + +impl ApiResponseContentSuccess for CommentsResponseContentSuccess { + type Data = Self; + + fn identifier(&self) -> &'static str { + "COMMENTS_OK" + } + + fn description(&self) -> Option { + Some("comments list returned".to_string()) + } + + fn data(&self) -> &Self::Data { + self + } +} diff --git a/blog-server-api/src/endpoints/mod.rs b/blog-server-api/src/endpoints/mod.rs index 8af480d..cc98090 100644 --- a/blog-server-api/src/endpoints/mod.rs +++ b/blog-server-api/src/endpoints/mod.rs @@ -1,6 +1,7 @@ pub mod author; pub mod author_me; pub mod authors; +pub mod comments; pub mod login; pub mod post; pub mod posts; diff --git a/blog-server-api/src/entities/comment.rs b/blog-server-api/src/entities/comment.rs new file mode 100644 index 0000000..a9d562c --- /dev/null +++ b/blog-server-api/src/entities/comment.rs @@ -0,0 +1,27 @@ +use super::*; +use blog_server_services::traits::comment_service::Comment as ServiceComment; +use serde::Serialize; + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Comment { + pub post_id: i64, + pub created_at: i64, + pub content: String, + pub short_author: ShortAuthor, +} + +impl Into for ServiceComment { + fn into(self) -> Comment { + Comment { + post_id: self.base.post_id, + created_at: self.base.created_at, + content: self.base.content, + short_author: ShortAuthor { + slug: self.author_slug, + first_name: self.author_first_name, + last_name: self.author_last_name, + }, + } + } +} diff --git a/blog-server-api/src/entities/mod.rs b/blog-server-api/src/entities/mod.rs index ef0a13b..43ca4f9 100644 --- a/blog-server-api/src/entities/mod.rs +++ b/blog-server-api/src/entities/mod.rs @@ -1,7 +1,9 @@ mod author; +mod comment; mod post; mod tag; pub use author::*; +pub use comment::*; pub use post::*; pub use tag::*; diff --git a/blog-server-api/src/entities/post.rs b/blog-server-api/src/entities/post.rs index e4e8913..d037bd9 100644 --- a/blog-server-api/src/entities/post.rs +++ b/blog-server-api/src/entities/post.rs @@ -7,7 +7,7 @@ use serde::Serialize; pub struct Post { pub title: String, pub slug: String, - pub summary: Option, + pub summary: String, pub created_at: i64, pub content: Option, pub short_author: ShortAuthor, diff --git a/blog-server-api/src/extensions.rs b/blog-server-api/src/extensions.rs index 746a04c..6a0c4c8 100644 --- a/blog-server-api/src/extensions.rs +++ b/blog-server-api/src/extensions.rs @@ -1,5 +1,8 @@ -use blog_server_services::impls::{create_rbatis_author_service, create_rbatis_post_service}; +use blog_server_services::impls::{ + create_rbatis_author_service, create_rbatis_comment_service, create_rbatis_post_service, +}; use blog_server_services::traits::author_service::AuthorService; +use blog_server_services::traits::comment_service::CommentService; use blog_server_services::traits::post_service::PostService; use rbatis::rbatis::RBatis; use std::sync::Arc; @@ -9,13 +12,16 @@ pub trait Resolve: Send + Sync { } pub trait ExtensionsProviderType: - Resolve>> + Resolve>> + Resolve>> + + Resolve>> + + Resolve>> { } struct ExtensionsProvider { author_service: Arc>, post_service: Arc>, + comment_service: Arc>, } impl ExtensionsProviderType for ExtensionsProvider {} @@ -32,9 +38,16 @@ impl Resolve>> for ExtensionsProvider { } } +impl Resolve>> for ExtensionsProvider { + fn resolve(&self) -> Arc> { + self.comment_service.clone() + } +} + pub fn make_extensions(rbatis: RBatis) -> impl ExtensionsProviderType { ExtensionsProvider { author_service: Arc::new(create_rbatis_author_service(rbatis.clone())), post_service: Arc::new(create_rbatis_post_service(rbatis.clone())), + comment_service: Arc::new(create_rbatis_comment_service(rbatis.clone())), } } diff --git a/blog-server-api/src/router.rs b/blog-server-api/src/router.rs index 4633858..97ba42c 100644 --- a/blog-server-api/src/router.rs +++ b/blog-server-api/src/router.rs @@ -80,6 +80,11 @@ pub fn make_router( .and_path("/posts") .and_handler(posts::http_handler), ) + .route( + route::first::Route::with_method(&hyper::Method::GET) + .and_path("/comments/{post_slug:[^/]*}") + .and_handler(comments::http_handler), + ) .route( route::first::Route::with_method(&hyper::Method::POST) .and_path("/login") diff --git a/blog-server-services/src/impls/mod.rs b/blog-server-services/src/impls/mod.rs index a2777ae..c923bcf 100644 --- a/blog-server-services/src/impls/mod.rs +++ b/blog-server-services/src/impls/mod.rs @@ -1,5 +1,7 @@ mod rbatis_author_service; +mod rbatis_comment_service; mod rbatis_post_service; pub use rbatis_author_service::create_rbatis_author_service; +pub use rbatis_comment_service::create_rbatis_comment_service; pub use rbatis_post_service::create_rbatis_post_service; diff --git a/blog-server-services/src/impls/rbatis_comment_service.rs b/blog-server-services/src/impls/rbatis_comment_service.rs new file mode 100644 index 0000000..7104be6 --- /dev/null +++ b/blog-server-services/src/impls/rbatis_comment_service.rs @@ -0,0 +1,74 @@ +use crate::traits::comment_service::{BaseComment, Comment, CommentService}; +use rbatis::rbatis::RBatis; +use screw_components::dyn_result::{DError, DResult}; + +pub fn create_rbatis_comment_service(rb: RBatis) -> Box { + Box::new(RbatisCommentService { rb }) +} + +impl_insert!(BaseComment {}, "post_comment"); + +impl Comment { + #[py_sql( + " + SELECT COUNT(1) \ + FROM post_comment \ + WHERE post_comment.post_id = #{post_id} + " + )] + async fn count_by_post_id(rb: &RBatis, post_id: &i64) -> rbatis::Result { + impled!() + } + #[py_sql( + " + SELECT \ + post_comment.*, \ + author.slug AS author_slug, \ + author.first_name AS author_first_name, \ + author.last_name AS author_last_name \ + FROM post_comment \ + JOIN author ON post_comment.author_id = author.id \ + WHERE post_comment.post_id = #{post_id} \ + LIMIT #{limit} \ + OFFSET #{offset} \ + " + )] + async fn select_all_with_post_id_and_limit_and_offset( + rb: &RBatis, + post_id: &i64, + limit: &i64, + offset: &i64, + ) -> rbatis::Result> { + impled!() + } +} + +struct RbatisCommentService { + rb: RBatis, +} + +#[async_trait] +impl CommentService for RbatisCommentService { + async fn comments_count_by_post_id(&self, post_id: &i64) -> DResult { + Ok(Comment::count_by_post_id(&self.rb, post_id).await?) + } + async fn comments_by_post_id( + &self, + post_id: &i64, + offset: &i64, + limit: &i64, + ) -> DResult> { + Ok( + Comment::select_all_with_post_id_and_limit_and_offset(&self.rb, post_id, limit, offset) + .await?, + ) + } + async fn create_comment(&self, comment: &BaseComment) -> DResult { + let insert_result = BaseComment::insert(&mut self.rb.clone(), comment).await?; + let last_insert_id = insert_result + .last_insert_id + .as_i64() + .ok_or::("wrond last_insert_id".into())?; + Ok(last_insert_id) + } +} diff --git a/blog-server-services/src/traits/comment_service.rs b/blog-server-services/src/traits/comment_service.rs new file mode 100644 index 0000000..7912411 --- /dev/null +++ b/blog-server-services/src/traits/comment_service.rs @@ -0,0 +1,35 @@ +use screw_components::dyn_result::DResult; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct BaseComment { + pub post_id: i64, + pub author_id: i64, + pub created_at: i64, + pub published: u8, + pub content: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct Comment { + pub id: i64, + pub author_slug: String, + pub author_first_name: Option, + pub author_last_name: Option, + #[serde(flatten)] + pub base: BaseComment, +} + +#[async_trait] +pub trait CommentService: Send + Sync { + async fn comments_count_by_post_id(&self, post_id: &i64) -> DResult; + async fn comments_by_post_id( + &self, + post_id: &i64, + offset: &i64, + limit: &i64, + ) -> DResult>; + async fn create_comment(&self, post: &BaseComment) -> DResult; +} diff --git a/blog-server-services/src/traits/mod.rs b/blog-server-services/src/traits/mod.rs index aac465b..f2ace01 100644 --- a/blog-server-services/src/traits/mod.rs +++ b/blog-server-services/src/traits/mod.rs @@ -1,2 +1,3 @@ pub mod author_service; +pub mod comment_service; pub mod post_service; diff --git a/blog-server-services/src/traits/post_service.rs b/blog-server-services/src/traits/post_service.rs index 20d45e9..033a151 100644 --- a/blog-server-services/src/traits/post_service.rs +++ b/blog-server-services/src/traits/post_service.rs @@ -7,7 +7,7 @@ pub struct BasePost { pub author_id: i64, pub title: String, pub slug: String, - pub summary: Option, + pub summary: String, pub published: u8, pub created_at: i64, pub content: Option, diff --git a/table_pg.sql b/table_pg.sql index efcb3e0..115c167 100644 --- a/table_pg.sql +++ b/table_pg.sql @@ -23,7 +23,7 @@ CREATE TABLE post ( author_id BIGINT NOT NULL, title VARCHAR(75) NOT NULL, slug VARCHAR(100) NOT NULL, - summary VARCHAR(255) NULL, + summary VARCHAR(255) NOT NULL, published SMALLINT NOT NULL DEFAULT 0, created_at TIMESTAMP(0) NOT NULL, content TEXT NULL DEFAULT NULL, @@ -73,7 +73,7 @@ CREATE TABLE post_comment ( author_id BIGINT NOT NULL, published SMALLINT NOT NULL DEFAULT 0, created_at TIMESTAMP(0) NOT NULL, - content TEXT NULL DEFAULT NULL, + content TEXT NOT NULL, PRIMARY KEY (id), CONSTRAINT fk_comment_post FOREIGN KEY (post_id)