diff --git a/README.md b/README.md
index f2f39bc..3f57e7b 100644
--- a/README.md
+++ b/README.md
@@ -68,26 +68,35 @@ $ APIURL=http://localhost:8080/api zsh e2e/run-api-tests.sh
## Architecture
-### Flow from Request to Response
+- Clean Architecture
+- DI container using Constructor Injection with dynamic dispatch (`/src/di.rs`)
```mermaid
sequenceDiagram
actor Client
- participant Middleware as Middleware
/middleware/*
- participant Controller as Controller
/[feature]/api.rs
- participant Service as Service
/[feature]/service.rs
+ autonumber
+ participant Route as Middleware + Route
/src/app/drivers/{middlewares, routes}
+ participant Controller as Controller
/src/app/features/[feature]/controllers.rs
+ participant Presenter as Presenter
/src/app/features/[feature]/presenters.rs
+ participant Usecase as Usecase
/src/app/features/[feature]/usecases.rs
+ participant Repository as Repository
/src/app/features/[feature]/repositories.rs
+ participant Entity as Entity
/src/app/features/[feature]/entities.rs
participant DB
- Client ->> Middleware: request
- Middleware ->> Controller: -
- Controller ->> Controller: Assign to Request Object
(/[feature]/request.rs)
- Controller ->> Service: -
- Service ->> DB: -
-
- DB ->> Service: -
- Service ->> Controller: -
- Controller ->> Controller: Convert into Response Object
(/[feature]/response.rs)
- Controller ->> Client: response
+ %% left to right
+ Client -->> Route: Request
+ Route ->> Controller:
+ Controller ->> Usecase:
+ Usecase ->> Repository:
+ Repository ->> Entity:
+ Entity ->> DB:
+
+ %% right to left
+ DB ->> Entity:
+ Entity ->> Repository:
+ Repository ->> Usecase:
+ Usecase ->> Presenter:
+ Presenter -->> Client: Response
```
## LICENSE
diff --git a/src/app/article/api.rs b/src/app/article/api.rs
deleted file mode 100644
index ee6b81b..0000000
--- a/src/app/article/api.rs
+++ /dev/null
@@ -1,153 +0,0 @@
-use super::{
- model::{Article, DeleteArticle},
- request,
- response::{MultipleArticlesResponse, SingleArticleResponse},
- service,
-};
-use crate::middleware::auth;
-use crate::middleware::state::AppState;
-use crate::utils::api::ApiResponse;
-use actix_web::{web, HttpRequest, HttpResponse};
-use serde::Deserialize;
-
-type ArticleTitleSlug = String;
-
-#[derive(Deserialize)]
-pub struct ArticlesListQueryParameter {
- tag: Option,
- author: Option,
- favorited: Option,
- limit: Option,
- offset: Option,
-}
-
-pub async fn index(
- state: web::Data,
- params: web::Query,
-) -> ApiResponse {
- let conn = &mut state.get_conn()?;
- let offset = std::cmp::min(params.offset.to_owned().unwrap_or(0), 100);
- let limit = params.limit.unwrap_or(20);
-
- let (articles_list, articles_count) = service::fetch_articles_list(
- conn,
- service::FetchArticlesList {
- tag: params.tag.clone(),
- author: params.author.clone(),
- favorited: params.favorited.clone(),
- offset,
- limit,
- },
- )?;
-
- let res = MultipleArticlesResponse::from((articles_list, articles_count));
- Ok(HttpResponse::Ok().json(res))
-}
-
-#[derive(Deserialize)]
-pub struct FeedQueryParameter {
- limit: Option,
- offset: Option,
-}
-
-pub async fn feed(
- state: web::Data,
- req: HttpRequest,
- params: web::Query,
-) -> ApiResponse {
- let conn = &mut state.get_conn()?;
- let current_user = auth::get_current_user(&req)?;
- let offset = std::cmp::min(params.offset.to_owned().unwrap_or(0), 100);
- let limit = params.limit.unwrap_or(20);
- let (articles_list, articles_count) = service::fetch_following_articles(
- conn,
- &service::FetchFollowedArticlesSerivce {
- current_user,
- offset,
- limit,
- },
- )?;
-
- let res = MultipleArticlesResponse::from((articles_list, articles_count));
- Ok(HttpResponse::Ok().json(res))
-}
-
-pub async fn show(state: web::Data, path: web::Path) -> ApiResponse {
- let conn = &mut state.get_conn()?;
- let article_title_slug = path.into_inner();
- let (article, profile, favorite_info, tags_list) =
- service::fetch_article_by_slug(conn, &service::FetchArticleBySlug { article_title_slug })?;
- let res = SingleArticleResponse::from((article, profile, favorite_info, tags_list));
- Ok(HttpResponse::Ok().json(res))
-}
-
-pub async fn create(
- state: web::Data,
- req: HttpRequest,
- form: web::Json,
-) -> ApiResponse {
- let conn = &mut state.get_conn()?;
- let current_user = auth::get_current_user(&req)?;
- let (article, profile, favorite_info, tag_list) = service::create(
- conn,
- &service::CreateArticleSerivce {
- title: form.article.title.clone(),
- slug: Article::convert_title_to_slug(&form.article.title),
- description: form.article.description.clone(),
- body: form.article.body.clone(),
- tag_name_list: form.article.tag_list.to_owned(),
- current_user,
- },
- )?;
- let res = SingleArticleResponse::from((article, profile, favorite_info, tag_list));
- Ok(HttpResponse::Ok().json(res))
-}
-
-pub async fn update(
- state: web::Data,
- req: HttpRequest,
- path: web::Path,
- form: web::Json,
-) -> ApiResponse {
- let conn = &mut state.get_conn()?;
- let current_user = auth::get_current_user(&req)?;
- let article_title_slug = path.into_inner();
- let article_slug = &form
- .article
- .title
- .as_ref()
- .map(|_title| Article::convert_title_to_slug(_title));
-
- let (article, profile, favorite_info, tag_list) = service::update_article(
- conn,
- &service::UpdateArticleService {
- current_user,
- article_title_slug,
- slug: article_slug.to_owned(),
- title: form.article.title.clone(),
- description: form.article.description.clone(),
- body: form.article.body.clone(),
- },
- )?;
-
- let res = SingleArticleResponse::from((article, profile, favorite_info, tag_list));
- Ok(HttpResponse::Ok().json(res))
-}
-
-pub async fn delete(
- state: web::Data,
- req: HttpRequest,
- path: web::Path,
-) -> ApiResponse {
- let conn = &mut state.get_conn()?;
- let current_user = auth::get_current_user(&req)?;
- let article_title_slug = path.into_inner();
- Article::delete(
- conn,
- &DeleteArticle {
- slug: article_title_slug,
- author_id: current_user.id,
- },
- )?;
- Ok(HttpResponse::Ok().json(()))
-}
diff --git a/src/app/article/mod.rs b/src/app/article/mod.rs
deleted file mode 100644
index c4c0061..0000000
--- a/src/app/article/mod.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-pub mod api;
-pub mod model;
-pub mod request;
-pub mod response;
-pub mod service;
diff --git a/src/app/article/service.rs b/src/app/article/service.rs
deleted file mode 100644
index 9972950..0000000
--- a/src/app/article/service.rs
+++ /dev/null
@@ -1,390 +0,0 @@
-use crate::app::article::model::{Article, CreateArticle, UpdateArticle};
-use crate::app::favorite::model::{Favorite, FavoriteInfo};
-use crate::app::follow::model::Follow;
-use crate::app::profile::model::Profile;
-use crate::app::tag::model::{CreateTag, Tag};
-use crate::app::user::model::User;
-use crate::error::AppError;
-use crate::schema::articles::dsl::*;
-use crate::schema::{articles, tags, users};
-use diesel::pg::PgConnection;
-use diesel::prelude::*;
-use uuid::Uuid;
-
-pub struct CreateArticleSerivce {
- pub slug: String,
- pub title: String,
- pub description: String,
- pub body: String,
- pub tag_name_list: Option>,
- pub current_user: User,
-}
-pub fn create(
- conn: &mut PgConnection,
- params: &CreateArticleSerivce,
-) -> Result<(Article, Profile, FavoriteInfo, Vec), AppError> {
- let article = Article::create(
- conn,
- &CreateArticle {
- author_id: params.current_user.id,
- slug: params.slug.clone(),
- title: params.title.clone(),
- description: params.description.clone(),
- body: params.body.clone(),
- },
- )?;
- let tag_list = create_tag_list(conn, ¶ms.tag_name_list, &article.id)?;
-
- let profile = params
- .current_user
- .fetch_profile(conn, &article.author_id)?;
-
- let favorite_info = {
- let is_favorited = article.is_favorited_by_user_id(conn, ¶ms.current_user.id)?;
- let favorites_count = article.fetch_favorites_count(conn)?;
- FavoriteInfo {
- is_favorited,
- favorites_count,
- }
- };
-
- Ok((article, profile, favorite_info, tag_list))
-}
-
-fn create_tag_list(
- conn: &mut PgConnection,
- tag_name_list: &Option>,
- article_id: &Uuid,
-) -> Result, AppError> {
- let list = tag_name_list
- .as_ref()
- .map(|tag_name_list| {
- let records = tag_name_list
- .iter()
- .map(|name| CreateTag { name, article_id })
- .collect();
- Tag::create_list(conn, records)
- })
- .unwrap_or_else(|| Ok(vec![]));
- list
-}
-
-pub struct FetchArticlesList {
- pub tag: Option,
- pub author: Option,
- pub favorited: Option,
- pub offset: i64,
- pub limit: i64,
-}
-
-type ArticlesCount = i64;
-type ArticlesListInner = (Article, Profile, FavoriteInfo);
-type ArticlesList = Vec<(ArticlesListInner, Vec)>;
-pub fn fetch_articles_list(
- conn: &mut PgConnection,
- params: FetchArticlesList,
-) -> Result<(ArticlesList, ArticlesCount), AppError> {
- use diesel::prelude::*;
-
- let query = {
- let mut query = articles::table.inner_join(users::table).into_boxed();
-
- if let Some(tag_name) = ¶ms.tag {
- let ids = Tag::fetch_article_ids_by_name(conn, tag_name)
- .expect("could not fetch tagged article ids."); // TODO: use ? or error handling
- query = query.filter(articles::id.eq_any(ids));
- }
-
- if let Some(author_name) = ¶ms.author {
- let ids = Article::fetch_ids_by_author_name(conn, author_name)
- .expect("could not fetch authors id."); // TODO: use ? or error handling
- query = query.filter(articles::id.eq_any(ids));
- }
-
- if let Some(username) = ¶ms.favorited {
- let ids = Favorite::fetch_favorited_article_ids_by_username(conn, username)
- .expect("could not fetch favorited articles id."); // TODO: use ? or error handling
-
- query = query.filter(articles::id.eq_any(ids));
- }
-
- query
- };
- let articles_count = query
- .select(diesel::dsl::count(articles::id))
- .first::(conn)?;
-
- let list = {
- let query = {
- let mut query = articles::table.inner_join(users::table).into_boxed();
-
- if let Some(tag_name) = ¶ms.tag {
- let ids = Tag::fetch_article_ids_by_name(conn, tag_name)
- .expect("could not fetch tagged article ids."); // TODO: use ? or error handling
- query = query.filter(articles::id.eq_any(ids));
- }
-
- if let Some(author_name) = ¶ms.author {
- let ids = Article::fetch_ids_by_author_name(conn, author_name)
- .expect("could not fetch authors id."); // TODO: use ? or error handling
- query = query.filter(articles::id.eq_any(ids));
- }
-
- if let Some(username) = ¶ms.favorited {
- let ids = Favorite::fetch_favorited_article_ids_by_username(conn, username)
- .expect("could not fetch favorited articles id."); // TODO: use ? or error handling
-
- query = query.filter(articles::id.eq_any(ids));
- }
-
- query
- };
- let article_and_user_list =
- query
- .offset(params.offset)
- .limit(params.limit)
- .load::<(Article, User)>(conn)?;
-
- let tags_list = {
- let articles_list = article_and_user_list
- .clone()
- .into_iter()
- .map(|(article, _)| article)
- .collect::>();
- let tags_list = Tag::belonging_to(&articles_list)
- .order(tags::name.asc())
- .load::(conn)?;
- let tags_list: Vec> = tags_list.grouped_by(&articles_list);
- tags_list
- };
-
- let favorites_count_list = {
- let list: Result, _> = article_and_user_list
- .clone()
- .into_iter()
- .map(|(article, _)| article.fetch_favorites_count(conn))
- .collect();
-
- list?
- };
-
- article_and_user_list
- .into_iter()
- .zip(favorites_count_list)
- .map(|((article, user), favorites_count)| {
- (
- article,
- Profile {
- username: user.username,
- bio: user.bio,
- image: user.image,
- following: false, // NOTE: because not authz
- },
- FavoriteInfo {
- is_favorited: false, // NOTE: because not authz
- favorites_count,
- },
- )
- })
- .zip(tags_list)
- .collect::>()
- };
-
- Ok((list, articles_count))
-}
-
-pub struct FetchArticle {
- pub article_id: Uuid,
- pub current_user: User,
-}
-pub fn fetch_article(
- conn: &mut PgConnection,
- FetchArticle {
- article_id,
- current_user,
- }: &FetchArticle,
-) -> Result<(Article, Profile, FavoriteInfo, Vec), AppError> {
- let (article, author) = Article::find_with_author(conn, article_id)?;
-
- let profile = current_user.fetch_profile(conn, &author.id)?;
-
- let favorite_info = {
- let is_favorited = article.is_favorited_by_user_id(conn, ¤t_user.id)?;
- let favorites_count = article.fetch_favorites_count(conn)?;
- FavoriteInfo {
- is_favorited,
- favorites_count,
- }
- };
-
- let tags_list = Tag::belonging_to(&article).load::(conn)?;
-
- Ok((article, profile, favorite_info, tags_list))
-}
-
-pub struct FetchArticleBySlug {
- pub article_title_slug: String,
-}
-pub fn fetch_article_by_slug(
- conn: &mut PgConnection,
- params: &FetchArticleBySlug,
-) -> Result<(Article, Profile, FavoriteInfo, Vec), AppError> {
- let (article, author) = Article::fetch_by_slug_with_author(conn, ¶ms.article_title_slug)?;
-
- let profile = author.fetch_profile(conn, &author.id)?;
-
- let tags_list = Tag::belonging_to(&article).load::(conn)?;
-
- let favorite_info = {
- let is_favorited = article.is_favorited_by_user_id(conn, &author.id)?;
- let favorites_count = article.fetch_favorites_count(conn)?;
- FavoriteInfo {
- is_favorited,
- favorites_count,
- }
- };
-
- Ok((article, profile, favorite_info, tags_list))
-}
-
-use crate::schema::follows;
-pub struct FetchFollowedArticlesSerivce {
- pub current_user: User,
- pub offset: i64,
- pub limit: i64,
-}
-pub fn fetch_following_articles(
- conn: &mut PgConnection,
- params: &FetchFollowedArticlesSerivce,
-) -> Result<(ArticlesList, ArticlesCount), AppError> {
- let create_query = {
- let ids = Follow::fetch_folowee_ids_by_follower_id(conn, ¶ms.current_user.id)?;
- articles.filter(articles::author_id.eq_any(ids))
- };
-
- let articles_list = {
- let article_and_user_list = create_query
- .to_owned()
- .inner_join(users::table)
- .limit(params.limit)
- .offset(params.offset)
- .order(articles::created_at.desc())
- .get_results::<(Article, User)>(conn)?;
-
- let tags_list = {
- let articles_list = article_and_user_list
- .clone() // TODO: avoid clone
- .into_iter()
- .map(|(article, _)| article)
- .collect::>();
-
- let tags_list = Tag::belonging_to(&articles_list).load::(conn)?;
- let tags_list: Vec> = tags_list.grouped_by(&articles_list);
- tags_list
- };
-
- let follows_list = {
- let user_ids_list = article_and_user_list
- .clone() // TODO: avoid clone
- .into_iter()
- .map(|(_, user)| user.id)
- .collect::>();
-
- let list = follows::table
- .filter(Follow::with_follower(¶ms.current_user.id))
- .filter(follows::followee_id.eq_any(user_ids_list))
- .get_results::(conn)?;
-
- list.into_iter()
- };
-
- let favorites_count_list = {
- let list: Result, _> = article_and_user_list
- .clone()
- .into_iter()
- .map(|(article, _)| article.fetch_favorites_count(conn))
- .collect();
-
- list?
- };
-
- let favorited_article_ids = params.current_user.fetch_favorited_article_ids(conn)?;
- let is_favorited_by_me = |article: &Article| {
- favorited_article_ids
- .iter()
- .copied()
- .any(|_id| _id == article.id)
- };
-
- article_and_user_list
- .into_iter()
- .zip(favorites_count_list)
- .map(|((article, user), favorites_count)| {
- let following = follows_list.clone().any(|item| item.followee_id == user.id);
- let is_favorited = is_favorited_by_me(&article);
- (
- article,
- Profile {
- username: user.username,
- bio: user.bio,
- image: user.image,
- following: following.to_owned(),
- },
- FavoriteInfo {
- is_favorited,
- favorites_count,
- },
- )
- })
- .zip(tags_list)
- .collect::>()
- };
-
- let articles_count = create_query
- .select(diesel::dsl::count(articles::id))
- .first::(conn)?;
-
- Ok((articles_list, articles_count))
-}
-
-pub struct UpdateArticleService {
- pub current_user: User,
- pub article_title_slug: String,
- pub slug: Option,
- pub title: Option,
- pub description: Option,
- pub body: Option,
-}
-pub fn update_article(
- conn: &mut PgConnection,
- params: &UpdateArticleService,
-) -> Result<(Article, Profile, FavoriteInfo, Vec), AppError> {
- let article = Article::update(
- conn,
- ¶ms.article_title_slug,
- ¶ms.current_user.id,
- &UpdateArticle {
- slug: params.slug.to_owned(),
- title: params.title.to_owned(),
- description: params.description.to_owned(),
- body: params.body.to_owned(),
- },
- )?;
-
- let tag_list = Tag::fetch_by_article_id(conn, &article.id)?;
-
- let profile = params
- .current_user
- .fetch_profile(conn, &article.author_id)?;
-
- let favorite_info = {
- let is_favorited = article.is_favorited_by_user_id(conn, ¶ms.current_user.id)?;
- let favorites_count = article.fetch_favorites_count(conn)?;
- FavoriteInfo {
- is_favorited,
- favorites_count,
- }
- };
-
- Ok((article, profile, favorite_info, tag_list))
-}
diff --git a/src/app/comment/api.rs b/src/app/comment/api.rs
deleted file mode 100644
index 69e320e..0000000
--- a/src/app/comment/api.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-use super::{
- request,
- response::{MultipleCommentsResponse, SingleCommentResponse},
- service,
-};
-use crate::middleware::auth;
-use crate::middleware::state::AppState;
-use crate::utils::api::ApiResponse;
-use crate::utils::uuid;
-use actix_web::{web, HttpRequest, HttpResponse};
-
-type ArticleIdSlug = String;
-type CommentIdSlug = String;
-
-pub async fn index(state: web::Data, req: HttpRequest) -> ApiResponse {
- let conn = &mut state.get_conn()?;
- let current_user = auth::get_current_user(&req).ok();
- let list = service::fetch_comments_list(conn, ¤t_user)?;
- let res = MultipleCommentsResponse::from(list);
- Ok(HttpResponse::Ok().json(res))
-}
-
-pub async fn create(
- state: web::Data,
- req: HttpRequest,
- path: web::Path,
- form: web::Json,
-) -> ApiResponse {
- let conn = &mut state.get_conn()?;
- let current_user = auth::get_current_user(&req)?;
- let article_title_slug = path.into_inner();
- let (comment, profile) = service::create(
- conn,
- &service::CreateCommentService {
- body: form.comment.body.to_owned(),
- article_title_slug,
- author: current_user,
- },
- )?;
- let res = SingleCommentResponse::from((comment, profile));
- Ok(HttpResponse::Ok().json(res))
-}
-
-pub async fn delete(
- state: web::Data,
- req: HttpRequest,
- path: web::Path<(ArticleIdSlug, CommentIdSlug)>,
-) -> ApiResponse {
- let conn = &mut state.get_conn()?;
- let current_user = auth::get_current_user(&req)?;
- let (article_title_slug, comment_id) = path.into_inner();
- let comment_id = uuid::parse(&comment_id)?;
- service::delete_comment(
- conn,
- &service::DeleteCommentService {
- article_title_slug,
- comment_id,
- author_id: current_user.id,
- },
- )?;
- Ok(HttpResponse::Ok().json("Ok"))
-}
diff --git a/src/app/comment/mod.rs b/src/app/comment/mod.rs
deleted file mode 100644
index c4c0061..0000000
--- a/src/app/comment/mod.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-pub mod api;
-pub mod model;
-pub mod request;
-pub mod response;
-pub mod service;
diff --git a/src/app/comment/service.rs b/src/app/comment/service.rs
deleted file mode 100644
index f00b777..0000000
--- a/src/app/comment/service.rs
+++ /dev/null
@@ -1,98 +0,0 @@
-use super::model::{Comment, CreateComment, DeleteComment};
-use crate::app::article::model::{Article, FetchBySlugAndAuthorId};
-use crate::app::profile::model::Profile;
-use crate::app::profile::service::{conver_user_to_profile, ConverUserToProfile};
-use crate::app::user::model::User;
-use crate::error::AppError;
-use diesel::pg::PgConnection;
-use uuid::Uuid;
-
-pub struct CreateCommentService {
- pub body: String,
- pub article_title_slug: String,
- pub author: User,
-}
-pub fn create(
- conn: &mut PgConnection,
- params: &CreateCommentService,
-) -> Result<(Comment, Profile), AppError> {
- let CreateCommentService {
- body,
- article_title_slug,
- author,
- } = params;
- let article = Article::fetch_by_slug_and_author_id(
- conn,
- &FetchBySlugAndAuthorId {
- slug: article_title_slug.to_owned(),
- author_id: author.id,
- },
- )?;
- let comment = Comment::create(
- conn,
- &CreateComment {
- body: body.to_string(),
- author_id: author.id,
- article_id: article.id.to_owned(),
- },
- )?;
- let profile = author.fetch_profile(conn, &author.id)?;
- Ok((comment, profile))
-}
-
-pub fn fetch_comments_list(
- conn: &mut PgConnection,
- current_user: &Option,
-) -> Result, AppError> {
- let comments = {
- use crate::schema::comments;
- // use crate::schema::comments::dsl::*;
- use crate::schema::users;
- use diesel::prelude::*;
- comments::table
- .inner_join(users::table)
- // .filter(comments::article_id.eq(article_id))
- .get_results::<(Comment, User)>(conn)?
- };
-
- let comments = comments
- .iter()
- .map(|(comment, user)| {
- // TODO: avoid N+1. Write one query to fetch all data somehow.
- let profile = conver_user_to_profile(conn, &ConverUserToProfile { user, current_user });
-
- // TODO: avoid copy
- (comment.to_owned(), profile)
- })
- .collect::>();
-
- Ok(comments)
-}
-
-pub struct DeleteCommentService {
- pub article_title_slug: String,
- pub author_id: Uuid,
- pub comment_id: Uuid,
-}
-
-pub fn delete_comment(
- conn: &mut PgConnection,
- params: &DeleteCommentService,
-) -> Result<(), AppError> {
- let article = Article::fetch_by_slug_and_author_id(
- conn,
- &FetchBySlugAndAuthorId {
- slug: params.article_title_slug.to_owned(),
- author_id: params.author_id,
- },
- )?;
- Comment::delete(
- conn,
- &DeleteComment {
- comment_id: params.comment_id,
- article_id: article.id,
- author_id: params.author_id,
- },
- )?;
- Ok(())
-}
diff --git a/src/middleware/auth.rs b/src/app/drivers/middlewares/auth.rs
similarity index 98%
rename from src/middleware/auth.rs
rename to src/app/drivers/middlewares/auth.rs
index acb27d3..502cb39 100644
--- a/src/middleware/auth.rs
+++ b/src/app/drivers/middlewares/auth.rs
@@ -1,7 +1,7 @@
-use crate::app::user::model::User;
+use crate::app::drivers::middlewares::state::AppState;
+use crate::app::features::user::entities::User;
use crate::constants;
use crate::error::AppError;
-use crate::middleware::state::AppState;
use crate::utils::token;
use actix_web::HttpMessage;
use actix_web::{
diff --git a/src/middleware/cors.rs b/src/app/drivers/middlewares/cors.rs
similarity index 100%
rename from src/middleware/cors.rs
rename to src/app/drivers/middlewares/cors.rs
diff --git a/src/middleware/error.rs b/src/app/drivers/middlewares/error.rs
similarity index 100%
rename from src/middleware/error.rs
rename to src/app/drivers/middlewares/error.rs
diff --git a/src/middleware/mod.rs b/src/app/drivers/middlewares/mod.rs
similarity index 100%
rename from src/middleware/mod.rs
rename to src/app/drivers/middlewares/mod.rs
diff --git a/src/middleware/state.rs b/src/app/drivers/middlewares/state.rs
similarity index 56%
rename from src/middleware/state.rs
rename to src/app/drivers/middlewares/state.rs
index fee3577..65ea3ec 100644
--- a/src/middleware/state.rs
+++ b/src/app/drivers/middlewares/state.rs
@@ -1,5 +1,6 @@
use crate::error::AppError;
-use crate::utils;
+use crate::utils::db::DbPool;
+use crate::utils::di::DiContainer;
use diesel::pg::PgConnection;
use diesel::r2d2::{ConnectionManager, PooledConnection};
@@ -7,10 +8,17 @@ type AppConn = PooledConnection>;
#[derive(Clone)]
pub struct AppState {
- pub pool: utils::db::DbPool,
+ #[deprecated]
+ pub pool: DbPool,
+ pub di_container: DiContainer,
}
impl AppState {
+ pub fn new(pool: DbPool) -> Self {
+ let di_container = DiContainer::new(&pool);
+ Self { pool, di_container }
+ }
+
pub fn get_conn(&self) -> Result {
let conn = self.pool.get()?;
Ok(conn)
diff --git a/src/app/drivers/mod.rs b/src/app/drivers/mod.rs
new file mode 100644
index 0000000..c58f3b1
--- /dev/null
+++ b/src/app/drivers/mod.rs
@@ -0,0 +1,2 @@
+pub mod middlewares;
+pub mod routes;
diff --git a/src/app/drivers/routes.rs b/src/app/drivers/routes.rs
new file mode 100644
index 0000000..8951eb3
--- /dev/null
+++ b/src/app/drivers/routes.rs
@@ -0,0 +1,80 @@
+use crate::app;
+use actix_web::web;
+use actix_web::web::{delete, get, post, put};
+
+pub fn api(cfg: &mut web::ServiceConfig) {
+ cfg.service(
+ web::scope("/api")
+ .service(
+ web::scope("/healthcheck")
+ .route("", get().to(app::features::healthcheck::controllers::index)),
+ )
+ .service(
+ web::scope("/tags").route("", get().to(app::features::tag::controllers::index)),
+ )
+ .service(
+ web::scope("/users")
+ .route(
+ "/login",
+ post().to(app::features::user::controllers::signin),
+ )
+ .route("", post().to(app::features::user::controllers::signup)),
+ )
+ .service(
+ web::scope("/user")
+ .route("", get().to(app::features::user::controllers::me))
+ .route("", put().to(app::features::user::controllers::update)),
+ )
+ .service(
+ web::scope("/profiles")
+ .route(
+ "/{username}",
+ get().to(app::features::profile::controllers::show),
+ )
+ .route(
+ "/{username}/follow",
+ post().to(app::features::profile::controllers::follow),
+ )
+ .route(
+ "/{username}/follow",
+ delete().to(app::features::profile::controllers::unfollow),
+ ),
+ )
+ .service(
+ web::scope("/articles")
+ .route("/feed", get().to(app::features::article::controllers::feed))
+ .route("", get().to(app::features::article::controllers::index))
+ .route("", post().to(app::features::article::controllers::create))
+ .service(
+ web::scope("/{article_title_slug}")
+ .route("", get().to(app::features::article::controllers::show))
+ .route("", put().to(app::features::article::controllers::update))
+ .route("", delete().to(app::features::article::controllers::delete))
+ .service(
+ web::scope("/favorite")
+ .route(
+ "",
+ post().to(app::features::favorite::controllers::favorite),
+ )
+ .route(
+ "",
+ delete()
+ .to(app::features::favorite::controllers::unfavorite),
+ ),
+ )
+ .service(
+ web::scope("/comments")
+ .route("", get().to(app::features::comment::controllers::index))
+ .route(
+ "",
+ post().to(app::features::comment::controllers::create),
+ )
+ .route(
+ "/{comment_id}",
+ delete().to(app::features::comment::controllers::delete),
+ ),
+ ),
+ ),
+ ),
+ );
+}
diff --git a/src/app/favorite/api.rs b/src/app/favorite/api.rs
deleted file mode 100644
index 52568b2..0000000
--- a/src/app/favorite/api.rs
+++ /dev/null
@@ -1,48 +0,0 @@
-use super::{
- response::SingleArticleResponse,
- service::{self, UnfavoriteService},
-};
-use crate::middleware::auth;
-use crate::middleware::state::AppState;
-use crate::utils::api::ApiResponse;
-use actix_web::{web, HttpRequest, HttpResponse};
-
-type ArticleIdSlug = String;
-
-pub async fn favorite(
- state: web::Data,
- req: HttpRequest,
- path: web::Path,
-) -> ApiResponse {
- let conn = &mut state.get_conn()?;
- let current_user = auth::get_current_user(&req)?;
- let article_title_slug = path.into_inner();
- let (article, profile, favorite_info, tags_list) = service::favorite(
- conn,
- &service::FavoriteService {
- current_user,
- article_title_slug,
- },
- )?;
- let res = SingleArticleResponse::from((article, profile, favorite_info, tags_list));
- Ok(HttpResponse::Ok().json(res))
-}
-
-pub async fn unfavorite(
- state: web::Data,
- req: HttpRequest,
- path: web::Path,
-) -> ApiResponse {
- let conn = &mut state.get_conn()?;
- let current_user = auth::get_current_user(&req)?;
- let article_title_slug = path.into_inner();
- let (article, profile, favorite_info, tags_list) = service::unfavorite(
- conn,
- &UnfavoriteService {
- current_user,
- article_title_slug,
- },
- )?;
- let res = SingleArticleResponse::from((article, profile, favorite_info, tags_list));
- Ok(HttpResponse::Ok().json(res))
-}
diff --git a/src/app/favorite/mod.rs b/src/app/favorite/mod.rs
deleted file mode 100644
index a451d09..0000000
--- a/src/app/favorite/mod.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-pub mod api;
-pub mod model;
-pub mod response;
-pub mod service;
diff --git a/src/app/favorite/response.rs b/src/app/favorite/response.rs
deleted file mode 100644
index ca699d6..0000000
--- a/src/app/favorite/response.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub use crate::app::article::response::SingleArticleResponse;
diff --git a/src/app/favorite/service.rs b/src/app/favorite/service.rs
deleted file mode 100644
index 1243731..0000000
--- a/src/app/favorite/service.rs
+++ /dev/null
@@ -1,75 +0,0 @@
-use crate::app::article::model::{Article, FetchBySlugAndAuthorId};
-use crate::app::article::service::{fetch_article, FetchArticle};
-use crate::app::favorite::model::{CreateFavorite, DeleteFavorite, Favorite, FavoriteInfo};
-use crate::app::profile::model::Profile;
-use crate::app::tag::model::Tag;
-use crate::app::user::model::User;
-use crate::error::AppError;
-use diesel::pg::PgConnection;
-
-pub struct FavoriteService {
- pub current_user: User,
- pub article_title_slug: String,
-}
-
-// TODO: move to User model
-pub fn favorite(
- conn: &mut PgConnection,
- params: &FavoriteService,
-) -> Result<(Article, Profile, FavoriteInfo, Vec), AppError> {
- let article = Article::fetch_by_slug_and_author_id(
- conn,
- &FetchBySlugAndAuthorId {
- slug: params.article_title_slug.to_owned(),
- author_id: params.current_user.id,
- },
- )?;
- Favorite::create(
- conn,
- &CreateFavorite {
- user_id: params.current_user.id,
- article_id: article.id,
- },
- )?;
- let item = fetch_article(
- conn,
- &FetchArticle {
- article_id: article.id,
- current_user: params.current_user.to_owned(),
- },
- )?;
- Ok(item)
-}
-
-pub struct UnfavoriteService {
- pub current_user: User,
- pub article_title_slug: String,
-}
-
-pub fn unfavorite(
- conn: &mut PgConnection,
- params: &UnfavoriteService,
-) -> Result<(Article, Profile, FavoriteInfo, Vec), AppError> {
- let article = Article::fetch_by_slug_and_author_id(
- conn,
- &FetchBySlugAndAuthorId {
- slug: params.article_title_slug.to_owned(),
- author_id: params.current_user.id,
- },
- )?;
- Favorite::delete(
- conn,
- &DeleteFavorite {
- user_id: params.current_user.id,
- article_id: article.id,
- },
- )?;
- let item = fetch_article(
- conn,
- &FetchArticle {
- article_id: article.id,
- current_user: params.current_user.to_owned(),
- },
- )?;
- Ok(item)
-}
diff --git a/src/app/features/article/controllers.rs b/src/app/features/article/controllers.rs
new file mode 100644
index 0000000..f1d2b69
--- /dev/null
+++ b/src/app/features/article/controllers.rs
@@ -0,0 +1,129 @@
+use super::{
+ entities::{Article, DeleteArticle},
+ presenters::{MultipleArticlesResponse, SingleArticleResponse},
+ repositories::FetchFollowingArticlesRepositoryInput,
+ requests,
+ usecases::{
+ CreateArticleUsecaseInput, DeleteArticleUsecaseInput, FetchArticlesListUsecaseInput,
+ UpdateArticleUsecaseInput,
+ },
+};
+use crate::app::drivers::middlewares::auth;
+use crate::app::drivers::middlewares::state::AppState;
+use crate::utils::api::ApiResponse;
+use actix_web::{web, HttpRequest, HttpResponse};
+use serde::Deserialize;
+
+type ArticleTitleSlug = String;
+
+#[derive(Deserialize)]
+pub struct ArticlesListQueryParameter {
+ tag: Option,
+ author: Option,
+ favorited: Option,
+ limit: Option,
+ offset: Option,
+}
+
+pub async fn index(
+ state: web::Data,
+ params: web::Query,
+) -> ApiResponse {
+ let offset = std::cmp::min(params.offset.to_owned().unwrap_or(0), 100);
+ let limit = params.limit.unwrap_or(20);
+ state
+ .di_container
+ .article_usecase
+ .fetch_articles_list(FetchArticlesListUsecaseInput {
+ tag: params.tag.clone(),
+ author: params.author.clone(),
+ favorited: params.favorited.clone(),
+ offset,
+ limit,
+ })
+}
+
+#[derive(Deserialize)]
+pub struct FeedQueryParameter {
+ limit: Option,
+ offset: Option,
+}
+
+pub async fn feed(
+ state: web::Data,
+ req: HttpRequest,
+ params: web::Query,
+) -> ApiResponse {
+ let current_user = auth::get_current_user(&req)?;
+ let offset = std::cmp::min(params.offset.to_owned().unwrap_or(0), 100);
+ let limit = params.limit.unwrap_or(20);
+ state
+ .di_container
+ .article_usecase
+ .fetch_following_articles(current_user, offset, limit)
+}
+
+pub async fn show(state: web::Data, path: web::Path) -> ApiResponse {
+ let article_title_slug = path.into_inner();
+ state
+ .di_container
+ .article_usecase
+ .fetch_article_by_slug(article_title_slug)
+}
+
+pub async fn create(
+ state: web::Data,
+ req: HttpRequest,
+ form: web::Json,
+) -> ApiResponse {
+ let current_user = auth::get_current_user(&req)?;
+ state
+ .di_container
+ .article_usecase
+ .create(CreateArticleUsecaseInput {
+ title: form.article.title.clone(),
+ description: form.article.description.clone(),
+ body: form.article.body.clone(),
+ tag_name_list: form.article.tag_list.to_owned(),
+ current_user,
+ })
+}
+
+pub async fn update(
+ state: web::Data,
+ req: HttpRequest,
+ path: web::Path,
+ form: web::Json,
+) -> ApiResponse {
+ let current_user = auth::get_current_user(&req)?;
+ let article_title_slug = path.into_inner();
+ let title = form.article.title.clone();
+ let description = form.article.description.clone();
+ let body = form.article.body.clone();
+ state
+ .di_container
+ .article_usecase
+ .update(UpdateArticleUsecaseInput {
+ current_user,
+ article_title_slug,
+ title,
+ description,
+ body,
+ })
+}
+
+pub async fn delete(
+ state: web::Data,
+ req: HttpRequest,
+ path: web::Path,
+) -> ApiResponse {
+ let current_user = auth::get_current_user(&req)?;
+ let article_title_slug = path.into_inner();
+ state
+ .di_container
+ .article_usecase
+ .delete(DeleteArticleUsecaseInput {
+ author_id: current_user.id,
+ slug: article_title_slug,
+ })
+}
diff --git a/src/app/article/model.rs b/src/app/features/article/entities.rs
similarity index 98%
rename from src/app/article/model.rs
rename to src/app/features/article/entities.rs
index 65daa95..7aff2e6 100644
--- a/src/app/article/model.rs
+++ b/src/app/features/article/entities.rs
@@ -1,5 +1,5 @@
-use crate::app::favorite::model::Favorite;
-use crate::app::user::model::User;
+use crate::app::features::favorite::entities::Favorite;
+use crate::app::features::user::entities::User;
use crate::error::AppError;
use crate::schema::articles;
use crate::utils::converter;
diff --git a/src/app/features/article/mod.rs b/src/app/features/article/mod.rs
new file mode 100644
index 0000000..7f85232
--- /dev/null
+++ b/src/app/features/article/mod.rs
@@ -0,0 +1,6 @@
+pub mod controllers;
+pub mod entities;
+pub mod presenters;
+pub mod repositories;
+pub mod requests;
+pub mod usecases;
diff --git a/src/app/article/response.rs b/src/app/features/article/presenters.rs
similarity index 75%
rename from src/app/article/response.rs
rename to src/app/features/article/presenters.rs
index 3d7ea7d..76e171e 100644
--- a/src/app/article/response.rs
+++ b/src/app/features/article/presenters.rs
@@ -1,8 +1,9 @@
-use crate::app::article::model::Article;
-use crate::app::favorite::model::FavoriteInfo;
-use crate::app::profile::model::Profile;
-use crate::app::tag::model::Tag;
+use super::{entities::Article, repositories::ArticlesList};
+use crate::app::features::favorite::entities::FavoriteInfo;
+use crate::app::features::profile::entities::Profile;
+use crate::app::features::tag::entities::Tag;
use crate::utils::date::Iso8601;
+use actix_web::HttpResponse;
use serde::{Deserialize, Serialize};
use std::convert::From;
@@ -51,7 +52,6 @@ pub struct MultipleArticlesResponse {
type ArticlesCount = i64;
type Inner = ((Article, Profile, FavoriteInfo), Vec);
-type ArticlesList = Vec;
type Item = (ArticlesList, ArticlesCount);
impl From- for MultipleArticlesResponse {
fn from((list, articles_count): (Vec, ArticleCount)) -> Self {
@@ -119,3 +119,31 @@ pub struct AuthorContent {
pub image: Option,
pub following: bool,
}
+
+pub trait ArticlePresenter: Send + Sync + 'static {
+ fn from_list_and_count(&self, list: ArticlesList, count: i64) -> HttpResponse;
+ fn from_item(&self, item: (Article, Profile, FavoriteInfo, Vec)) -> HttpResponse;
+ fn toHttpRes(&self) -> HttpResponse;
+}
+
+#[derive(Clone)]
+pub struct ArticlePresenterImpl {}
+impl ArticlePresenterImpl {
+ pub fn new() -> Self {
+ Self {}
+ }
+}
+impl ArticlePresenter for ArticlePresenterImpl {
+ fn from_list_and_count(&self, list: ArticlesList, count: i64) -> HttpResponse {
+ let res = MultipleArticlesResponse::from((list, count));
+ HttpResponse::Ok().json(res)
+ }
+ fn from_item(&self, item: (Article, Profile, FavoriteInfo, Vec)) -> HttpResponse {
+ let res = SingleArticleResponse::from(item);
+ HttpResponse::Ok().json(res)
+ }
+ fn toHttpRes(&self) -> HttpResponse {
+ let res = ();
+ HttpResponse::Ok().json(res)
+ }
+}
diff --git a/src/app/features/article/repositories.rs b/src/app/features/article/repositories.rs
new file mode 100644
index 0000000..129ebdc
--- /dev/null
+++ b/src/app/features/article/repositories.rs
@@ -0,0 +1,489 @@
+use super::entities::{Article, CreateArticle, DeleteArticle, UpdateArticle};
+use crate::app::features::favorite::entities::FavoriteInfo;
+use crate::app::features::profile::entities::Profile;
+use crate::app::features::tag::entities::{CreateTag, Tag};
+use crate::app::features::user::entities::User;
+use crate::error::AppError;
+use crate::utils::db::DbPool;
+use diesel::PgConnection;
+use uuid::Uuid;
+
+pub trait ArticleRepository: Send + Sync + 'static {
+ fn fetch_articles_list(
+ &self,
+ params: FetchArticlesListRepositoryInput,
+ ) -> Result<(ArticlesList, ArticlesCount), AppError>;
+
+ fn fetch_article_by_slug(
+ &self,
+ article_title_slug: String,
+ ) -> Result;
+
+ fn create(
+ &self,
+ params: CreateArticleRepositoryInput,
+ ) -> Result<(Article, Profile, FavoriteInfo, Vec), AppError>;
+
+ fn delete(&self, input: DeleteArticleRepositoryInput) -> Result<(), AppError>;
+
+ fn update(
+ &self,
+ input: UpdateArticleRepositoryInput,
+ ) -> Result<(Article, Profile, FavoriteInfo, Vec), AppError>;
+
+ fn fetch_article_item(
+ &self,
+ input: &FetchArticleRepositoryInput,
+ ) -> Result<(Article, Profile, FavoriteInfo, Vec), AppError>;
+
+ fn fetch_following_articles(
+ &self,
+ params: &FetchFollowingArticlesRepositoryInput,
+ ) -> Result<(ArticlesList, ArticlesCount), AppError>;
+}
+#[derive(Clone)]
+pub struct ArticleRepositoryImpl {
+ pool: DbPool,
+}
+
+impl ArticleRepositoryImpl {
+ pub fn new(pool: DbPool) -> Self {
+ Self { pool }
+ }
+
+ fn create_tag_list(
+ conn: &mut PgConnection,
+ tag_name_list: &Option>,
+ article_id: &Uuid,
+ ) -> Result, AppError> {
+ let list = tag_name_list
+ .as_ref()
+ .map(|tag_name_list| {
+ let records = tag_name_list
+ .iter()
+ .map(|name| CreateTag { name, article_id })
+ .collect();
+ Tag::create_list(conn, records)
+ })
+ .unwrap_or_else(|| Ok(vec![]));
+ list
+ }
+}
+
+impl ArticleRepository for ArticleRepositoryImpl {
+ fn fetch_articles_list(
+ &self,
+ params: FetchArticlesListRepositoryInput,
+ ) -> Result<(ArticlesList, ArticlesCount), AppError> {
+ use crate::app::features::article::entities::Article;
+ use crate::app::features::favorite::entities::{Favorite, FavoriteInfo};
+ use crate::app::features::follow::entities::Follow;
+ use crate::app::features::profile::entities::Profile;
+ use crate::app::features::tag::entities::Tag;
+ use crate::app::features::user::entities::User;
+ use crate::error::AppError;
+ use crate::schema::articles::dsl::*;
+ use crate::schema::{articles, tags, users};
+ use diesel::pg::PgConnection;
+ use diesel::prelude::*;
+ use diesel::prelude::*;
+ // ====
+ let conn = &mut self.pool.get()?;
+
+ let query = {
+ let mut query = articles::table.inner_join(users::table).into_boxed();
+
+ if let Some(tag_name) = ¶ms.tag {
+ let ids = Tag::fetch_article_ids_by_name(conn, tag_name)
+ .expect("could not fetch tagged article ids."); // TODO: use ? or error handling
+ query = query.filter(articles::id.eq_any(ids));
+ }
+
+ if let Some(author_name) = ¶ms.author {
+ let ids = Article::fetch_ids_by_author_name(conn, author_name)
+ .expect("could not fetch authors id."); // TODO: use ? or error handling
+ query = query.filter(articles::id.eq_any(ids));
+ }
+
+ if let Some(username) = ¶ms.favorited {
+ let ids = Favorite::fetch_favorited_article_ids_by_username(conn, username)
+ .expect("could not fetch favorited articles id."); // TODO: use ? or error handling
+
+ query = query.filter(articles::id.eq_any(ids));
+ }
+
+ query
+ };
+ let articles_count = query
+ .select(diesel::dsl::count(articles::id))
+ .first::(conn)?;
+
+ let result = {
+ let query = {
+ let mut query = articles::table.inner_join(users::table).into_boxed();
+
+ if let Some(tag_name) = ¶ms.tag {
+ let ids = Tag::fetch_article_ids_by_name(conn, tag_name)
+ .expect("could not fetch tagged article ids."); // TODO: use ? or error handling
+ query = query.filter(articles::id.eq_any(ids));
+ }
+
+ if let Some(author_name) = ¶ms.author {
+ let ids = Article::fetch_ids_by_author_name(conn, author_name)
+ .expect("could not fetch authors id."); // TODO: use ? or error handling
+ query = query.filter(articles::id.eq_any(ids));
+ }
+
+ if let Some(username) = ¶ms.favorited {
+ let ids = Favorite::fetch_favorited_article_ids_by_username(conn, username)
+ .expect("could not fetch favorited articles id."); // TODO: use ? or error handling
+
+ query = query.filter(articles::id.eq_any(ids));
+ }
+
+ query
+ };
+ let article_and_user_list =
+ query
+ .offset(params.offset)
+ .limit(params.limit)
+ .load::<(Article, User)>(conn)?;
+
+ let tags_list = {
+ let articles_list = article_and_user_list
+ .clone()
+ .into_iter()
+ .map(|(article, _)| article)
+ .collect::>();
+ let tags_list = Tag::belonging_to(&articles_list)
+ .order(tags::name.asc())
+ .load::(conn)?;
+ let tags_list: Vec> = tags_list.grouped_by(&articles_list);
+ tags_list
+ };
+
+ let favorites_count_list = {
+ let list: Result, _> = article_and_user_list
+ .clone()
+ .into_iter()
+ .map(|(article, _)| article.fetch_favorites_count(conn))
+ .collect();
+
+ list?
+ };
+
+ article_and_user_list
+ .into_iter()
+ .zip(favorites_count_list)
+ .map(|((article, user), favorites_count)| {
+ (
+ article,
+ Profile {
+ username: user.username,
+ bio: user.bio,
+ image: user.image,
+ following: false, // NOTE: because not authz
+ },
+ FavoriteInfo {
+ is_favorited: false, // NOTE: because not authz
+ favorites_count,
+ },
+ )
+ })
+ .zip(tags_list)
+ .collect::>()
+ };
+
+ Ok((result, articles_count))
+ }
+
+ fn fetch_article_by_slug(
+ &self,
+ article_title_slug: String,
+ ) -> Result {
+ let conn = &mut self.pool.get()?;
+
+ let (article, author) = Article::fetch_by_slug_with_author(conn, &article_title_slug)?;
+
+ let profile = author.fetch_profile(conn, &author.id)?;
+
+ let tags_list = {
+ use diesel::prelude::*;
+ Tag::belonging_to(&article).load::(conn)?
+ };
+
+ let favorite_info = {
+ let is_favorited = article.is_favorited_by_user_id(conn, &author.id)?;
+ let favorites_count = article.fetch_favorites_count(conn)?;
+ FavoriteInfo {
+ is_favorited,
+ favorites_count,
+ }
+ };
+
+ Ok((article, profile, favorite_info, tags_list))
+ }
+
+ fn create(
+ &self,
+ params: CreateArticleRepositoryInput,
+ ) -> Result<(Article, Profile, FavoriteInfo, Vec), AppError> {
+ let conn = &mut self.pool.get()?;
+
+ let article = Article::create(
+ conn,
+ &CreateArticle {
+ author_id: params.current_user.id,
+ slug: params.slug.clone(),
+ title: params.title.clone(),
+ description: params.description.clone(),
+ body: params.body.clone(),
+ },
+ )?;
+
+ let tag_list = Self::create_tag_list(conn, ¶ms.tag_name_list, &article.id)?;
+
+ let profile = params
+ .current_user
+ .fetch_profile(conn, &article.author_id)?;
+
+ let favorite_info = {
+ let is_favorited = article.is_favorited_by_user_id(conn, ¶ms.current_user.id)?;
+ let favorites_count = article.fetch_favorites_count(conn)?;
+ FavoriteInfo {
+ is_favorited,
+ favorites_count,
+ }
+ };
+
+ Ok((article, profile, favorite_info, tag_list))
+ }
+
+ fn delete(&self, input: DeleteArticleRepositoryInput) -> Result<(), AppError> {
+ let conn = &mut self.pool.get()?;
+ Article::delete(
+ conn,
+ &DeleteArticle {
+ slug: input.slug,
+ author_id: input.author_id,
+ },
+ )
+ }
+
+ fn update(
+ &self,
+ input: UpdateArticleRepositoryInput,
+ ) -> Result<(Article, Profile, FavoriteInfo, Vec), AppError> {
+ let conn = &mut self.pool.get()?;
+
+ let article = Article::update(
+ conn,
+ &input.article_title_slug,
+ &input.current_user.id,
+ &UpdateArticle {
+ slug: input.slug.to_owned(),
+ title: input.title.to_owned(),
+ description: input.description.to_owned(),
+ body: input.body.to_owned(),
+ },
+ )?;
+
+ let tag_list = Tag::fetch_by_article_id(conn, &article.id)?;
+
+ let profile = input.current_user.fetch_profile(conn, &article.author_id)?;
+
+ let favorite_info = {
+ let is_favorited = article.is_favorited_by_user_id(conn, &input.current_user.id)?;
+ let favorites_count = article.fetch_favorites_count(conn)?;
+ FavoriteInfo {
+ is_favorited,
+ favorites_count,
+ }
+ };
+
+ Ok((article, profile, favorite_info, tag_list))
+ }
+
+ fn fetch_article_item(
+ &self,
+ input: &FetchArticleRepositoryInput,
+ ) -> Result<(Article, Profile, FavoriteInfo, Vec), AppError> {
+ let conn = &mut self.pool.get()?;
+ let (article, author) = Article::find_with_author(conn, &input.article_id)?;
+
+ let profile = input.current_user.fetch_profile(conn, &author.id)?;
+
+ let favorite_info = {
+ let is_favorited = article.is_favorited_by_user_id(conn, &input.current_user.id)?;
+ let favorites_count = article.fetch_favorites_count(conn)?;
+ FavoriteInfo {
+ is_favorited,
+ favorites_count,
+ }
+ };
+
+ let tags_list = {
+ use diesel::prelude::*;
+ Tag::belonging_to(&article).load::(conn)?
+ };
+
+ Ok((article, profile, favorite_info, tags_list))
+ }
+
+ fn fetch_following_articles(
+ &self,
+ params: &FetchFollowingArticlesRepositoryInput,
+ ) -> Result<(ArticlesList, ArticlesCount), AppError> {
+ use crate::app::features::article::entities::Article;
+ use crate::app::features::favorite::entities::{Favorite, FavoriteInfo};
+ use crate::app::features::follow::entities::Follow;
+ use crate::app::features::profile::entities::Profile;
+ use crate::app::features::tag::entities::Tag;
+ use crate::app::features::user::entities::User;
+ use crate::error::AppError;
+ use crate::schema::articles::dsl::*;
+ use crate::schema::follows;
+ use crate::schema::{articles, tags, users};
+ use diesel::pg::PgConnection;
+ use diesel::prelude::*;
+ // ==
+ let conn = &mut self.pool.get()?;
+
+ let create_query = {
+ let ids = Follow::fetch_folowee_ids_by_follower_id(conn, ¶ms.current_user.id)?;
+ articles.filter(articles::author_id.eq_any(ids))
+ };
+
+ let articles_list = {
+ let article_and_user_list = create_query
+ .to_owned()
+ .inner_join(users::table)
+ .limit(params.limit)
+ .offset(params.offset)
+ .order(articles::created_at.desc())
+ .get_results::<(Article, User)>(conn)?;
+
+ let tags_list = {
+ let articles_list = article_and_user_list
+ .clone() // TODO: avoid clone
+ .into_iter()
+ .map(|(article, _)| article)
+ .collect::>();
+
+ let tags_list = Tag::belonging_to(&articles_list).load::(conn)?;
+ let tags_list: Vec> = tags_list.grouped_by(&articles_list);
+ tags_list
+ };
+
+ let follows_list = {
+ let user_ids_list = article_and_user_list
+ .clone() // TODO: avoid clone
+ .into_iter()
+ .map(|(_, user)| user.id)
+ .collect::>();
+
+ let list = follows::table
+ .filter(Follow::with_follower(¶ms.current_user.id))
+ .filter(follows::followee_id.eq_any(user_ids_list))
+ .get_results::(conn)?;
+
+ list.into_iter()
+ };
+
+ let favorites_count_list = {
+ let list: Result, _> = article_and_user_list
+ .clone()
+ .into_iter()
+ .map(|(article, _)| article.fetch_favorites_count(conn))
+ .collect();
+
+ list?
+ };
+
+ let favorited_article_ids = params.current_user.fetch_favorited_article_ids(conn)?;
+ let is_favorited_by_me = |article: &Article| {
+ favorited_article_ids
+ .iter()
+ .copied()
+ .any(|_id| _id == article.id)
+ };
+
+ article_and_user_list
+ .into_iter()
+ .zip(favorites_count_list)
+ .map(|((article, user), favorites_count)| {
+ let following = follows_list.clone().any(|item| item.followee_id == user.id);
+ let is_favorited = is_favorited_by_me(&article);
+ (
+ article,
+ Profile {
+ username: user.username,
+ bio: user.bio,
+ image: user.image,
+ following: following.to_owned(),
+ },
+ FavoriteInfo {
+ is_favorited,
+ favorites_count,
+ },
+ )
+ })
+ .zip(tags_list)
+ .collect::>()
+ };
+
+ let articles_count = create_query
+ .select(diesel::dsl::count(articles::id))
+ .first::(conn)?;
+
+ Ok((articles_list, articles_count))
+ }
+}
+
+pub struct CreateArticleRepositoryInput {
+ pub slug: String,
+ pub title: String,
+ pub description: String,
+ pub body: String,
+ pub tag_name_list: Option>,
+ pub current_user: User,
+}
+
+pub struct DeleteArticleRepositoryInput {
+ pub slug: String,
+ pub author_id: Uuid,
+}
+
+pub struct UpdateArticleRepositoryInput {
+ pub current_user: User,
+ pub article_title_slug: String,
+ pub slug: Option,
+ pub title: Option,
+ pub description: Option,
+ pub body: Option,
+}
+
+pub type FetchArticleBySlugOutput = (Article, Profile, FavoriteInfo, Vec);
+
+pub struct FetchArticlesListRepositoryInput {
+ pub tag: Option,
+ pub author: Option,
+ pub favorited: Option,
+ pub offset: i64,
+ pub limit: i64,
+}
+
+pub struct FetchArticleRepositoryInput {
+ pub article_id: Uuid,
+ pub current_user: User,
+}
+
+pub struct FetchFollowingArticlesRepositoryInput {
+ pub current_user: User,
+ pub offset: i64,
+ pub limit: i64,
+}
+
+type ArticlesCount = i64;
+type ArticlesListInner = (Article, Profile, FavoriteInfo);
+pub type ArticlesList = Vec<(ArticlesListInner, Vec)>;
diff --git a/src/app/article/request.rs b/src/app/features/article/requests.rs
similarity index 100%
rename from src/app/article/request.rs
rename to src/app/features/article/requests.rs
diff --git a/src/app/features/article/usecases.rs b/src/app/features/article/usecases.rs
new file mode 100644
index 0000000..6a97312
--- /dev/null
+++ b/src/app/features/article/usecases.rs
@@ -0,0 +1,151 @@
+use super::entities::Article;
+use super::presenters::ArticlePresenter;
+use super::repositories::{
+ ArticleRepository, CreateArticleRepositoryInput, DeleteArticleRepositoryInput,
+ FetchArticlesListRepositoryInput, FetchFollowingArticlesRepositoryInput,
+ UpdateArticleRepositoryInput,
+};
+use crate::app::features::user::entities::User;
+use crate::error::AppError;
+use actix_web::HttpResponse;
+use std::sync::Arc;
+use uuid::Uuid;
+
+#[derive(Clone)]
+pub struct ArticleUsecase {
+ article_repository: Arc,
+ article_presenter: Arc,
+}
+
+impl ArticleUsecase {
+ pub fn new(
+ article_repository: Arc,
+ article_presenter: Arc,
+ ) -> Self {
+ Self {
+ article_repository,
+ article_presenter,
+ }
+ }
+
+ pub fn fetch_articles_list(
+ &self,
+ params: FetchArticlesListUsecaseInput,
+ ) -> Result {
+ let (list, count) =
+ self.article_repository
+ .fetch_articles_list(FetchArticlesListRepositoryInput {
+ tag: params.tag.clone(),
+ author: params.author.clone(),
+ favorited: params.favorited.clone(),
+ offset: params.offset,
+ limit: params.limit,
+ })?;
+ let res = self.article_presenter.from_list_and_count(list, count);
+ Ok(res)
+ }
+
+ pub fn fetch_article_by_slug(
+ &self,
+ article_title_slug: String,
+ ) -> Result {
+ let article_title_slug = article_title_slug.clone();
+ let result = self
+ .article_repository
+ .fetch_article_by_slug(article_title_slug)?;
+ let res = self.article_presenter.from_item(result);
+ Ok(res)
+ }
+
+ pub fn fetch_following_articles(
+ &self,
+ user: User,
+ offset: i64,
+ limit: i64,
+ ) -> Result {
+ let (list, count) = self.article_repository.fetch_following_articles(
+ &FetchFollowingArticlesRepositoryInput {
+ current_user: user,
+ offset,
+ limit,
+ },
+ )?;
+ let res = self.article_presenter.from_list_and_count(list, count);
+ Ok(res)
+ }
+
+ pub fn create(&self, params: CreateArticleUsecaseInput) -> Result {
+ let slug = Article::convert_title_to_slug(¶ms.title);
+ let result = self
+ .article_repository
+ .create(CreateArticleRepositoryInput {
+ body: params.body,
+ current_user: params.current_user,
+ description: params.description,
+ tag_name_list: params.tag_name_list,
+ title: params.title,
+ slug,
+ })?;
+ let res = self.article_presenter.from_item(result);
+ Ok(res)
+ }
+
+ pub fn delete(&self, input: DeleteArticleUsecaseInput) -> Result {
+ self.article_repository
+ .delete(DeleteArticleRepositoryInput {
+ slug: input.slug,
+ author_id: input.author_id,
+ })?;
+ let res = self.article_presenter.toHttpRes();
+ Ok(res)
+ }
+
+ pub fn update(&self, input: UpdateArticleUsecaseInput) -> Result {
+ let article_slug = &input
+ .title
+ .as_ref()
+ .map(|_title| Article::convert_title_to_slug(_title));
+ let slug = article_slug.to_owned();
+ let result = self
+ .article_repository
+ .update(UpdateArticleRepositoryInput {
+ current_user: input.current_user,
+ article_title_slug: input.article_title_slug,
+ slug,
+ title: input.title,
+ description: input.description,
+ body: input.body,
+ })?;
+ let res = self.article_presenter.from_item(result);
+ Ok(res)
+ }
+}
+
+pub struct CreateArticleUsecaseInput {
+ pub title: String,
+ pub description: String,
+ pub body: String,
+ pub tag_name_list: Option>,
+ pub current_user: User,
+}
+
+pub struct DeleteArticleUsecaseInput {
+ pub slug: String,
+ pub author_id: Uuid,
+}
+
+pub struct UpdateArticleUsecaseInput {
+ pub current_user: User,
+ pub article_title_slug: String,
+ pub title: Option,
+ pub description: Option,
+ pub body: Option,
+}
+
+pub struct FetchArticlesListUsecaseInput {
+ pub tag: Option,
+ pub author: Option,
+ pub favorited: Option,
+ pub offset: i64,
+ pub limit: i64,
+}
diff --git a/src/app/features/comment/controllers.rs b/src/app/features/comment/controllers.rs
new file mode 100644
index 0000000..b731c73
--- /dev/null
+++ b/src/app/features/comment/controllers.rs
@@ -0,0 +1,46 @@
+use super::request;
+use crate::app::drivers::middlewares::auth;
+use crate::app::drivers::middlewares::state::AppState;
+use crate::utils::api::ApiResponse;
+use crate::utils::uuid;
+use actix_web::{web, HttpRequest};
+
+type ArticleIdSlug = String;
+type CommentIdSlug = String;
+
+pub async fn index(state: web::Data, req: HttpRequest) -> ApiResponse {
+ let current_user = auth::get_current_user(&req).ok();
+ state
+ .di_container
+ .comment_usecase
+ .fetch_comments_list(¤t_user)
+}
+
+pub async fn create(
+ state: web::Data,
+ req: HttpRequest,
+ path: web::Path,
+ form: web::Json,
+) -> ApiResponse {
+ let current_user = auth::get_current_user(&req)?;
+ let article_title_slug = path.into_inner();
+ let body = form.comment.body.to_owned();
+ state
+ .di_container
+ .comment_usecase
+ .create(body, article_title_slug, current_user)
+}
+
+pub async fn delete(
+ state: web::Data,
+ req: HttpRequest,
+ path: web::Path<(ArticleIdSlug, CommentIdSlug)>,
+) -> ApiResponse {
+ let current_user = auth::get_current_user(&req)?;
+ let (article_title_slug, comment_id) = path.into_inner();
+ let comment_id = uuid::parse(&comment_id)?;
+ state
+ .di_container
+ .comment_usecase
+ .delete(&article_title_slug, comment_id, current_user.id)
+}
diff --git a/src/app/comment/model.rs b/src/app/features/comment/entities.rs
similarity index 95%
rename from src/app/comment/model.rs
rename to src/app/features/comment/entities.rs
index 949288c..1f83934 100644
--- a/src/app/comment/model.rs
+++ b/src/app/features/comment/entities.rs
@@ -1,5 +1,5 @@
-use crate::app::article::model::Article;
-use crate::app::user::model::User;
+use crate::app::features::article::entities::Article;
+use crate::app::features::user::entities::User;
use crate::error::AppError;
use crate::schema::comments;
use chrono::NaiveDateTime;
diff --git a/src/app/features/comment/mod.rs b/src/app/features/comment/mod.rs
new file mode 100644
index 0000000..8b19374
--- /dev/null
+++ b/src/app/features/comment/mod.rs
@@ -0,0 +1,6 @@
+pub mod controllers;
+pub mod entities;
+pub mod presenters;
+pub mod repositories;
+pub mod request;
+pub mod usecases;
diff --git a/src/app/comment/response.rs b/src/app/features/comment/presenters.rs
similarity index 68%
rename from src/app/comment/response.rs
rename to src/app/features/comment/presenters.rs
index 379f2cb..3be869f 100644
--- a/src/app/comment/response.rs
+++ b/src/app/features/comment/presenters.rs
@@ -1,6 +1,7 @@
-use crate::app::comment::model::Comment;
-use crate::app::profile::model::Profile;
+use crate::app::features::comment::entities::Comment;
+use crate::app::features::profile::entities::Profile;
use crate::utils::date::Iso8601;
+use actix_web::HttpResponse;
use serde::{Deserialize, Serialize};
use std::convert::From;
use uuid::Uuid;
@@ -76,3 +77,32 @@ pub struct InnerAuthor {
pub image: Option,
pub following: bool,
}
+
+pub trait CommentPresenter: Send + Sync + 'static {
+ fn toHttpRes(&self) -> HttpResponse;
+ fn from_comment_and_profile(&self, item: (Comment, Profile)) -> HttpResponse;
+ fn from_comment_and_profile_list(&self, list: Vec<(Comment, Profile)>) -> HttpResponse;
+}
+
+#[derive(Clone)]
+pub struct CommentPresenterImpl {}
+impl CommentPresenterImpl {
+ pub fn new() -> Self {
+ Self {}
+ }
+}
+impl CommentPresenter for CommentPresenterImpl {
+ fn toHttpRes(&self) -> HttpResponse {
+ HttpResponse::Ok().json("OK")
+ }
+
+ fn from_comment_and_profile_list(&self, list: Vec<(Comment, Profile)>) -> HttpResponse {
+ let res = MultipleCommentsResponse::from(list);
+ HttpResponse::Ok().json(res)
+ }
+
+ fn from_comment_and_profile(&self, item: (Comment, Profile)) -> HttpResponse {
+ let res = SingleCommentResponse::from(item);
+ HttpResponse::Ok().json(res)
+ }
+}
diff --git a/src/app/features/comment/repositories.rs b/src/app/features/comment/repositories.rs
new file mode 100644
index 0000000..d2cce2a
--- /dev/null
+++ b/src/app/features/comment/repositories.rs
@@ -0,0 +1,128 @@
+use super::entities::{Comment, CreateComment, DeleteComment};
+use crate::{
+ app::features::{
+ article::entities::{Article, FetchBySlugAndAuthorId},
+ profile::entities::Profile,
+ user::entities::User,
+ },
+ error::AppError,
+ utils::db::DbPool,
+};
+use uuid::Uuid;
+
+pub trait CommentRepository: Send + Sync + 'static {
+ fn fetch_comments_list(
+ &self,
+ current_user: &Option,
+ ) -> Result, AppError>;
+
+ fn create(
+ &self,
+ body: String,
+ article_title_slug: String,
+ author: User,
+ ) -> Result<(Comment, Profile), AppError>;
+
+ fn delete(
+ &self,
+ article_title_slug: &str,
+ comment_id: Uuid,
+ author_id: Uuid,
+ ) -> Result<(), AppError>;
+}
+
+#[derive(Clone)]
+pub struct CommentRepositoryImpl {
+ pool: DbPool,
+}
+
+impl CommentRepositoryImpl {
+ pub fn new(pool: DbPool) -> Self {
+ Self { pool }
+ }
+}
+impl CommentRepository for CommentRepositoryImpl {
+ fn fetch_comments_list(
+ &self,
+ current_user: &Option,
+ ) -> Result, AppError> {
+ let conn = &mut self.pool.get()?;
+
+ let comments = {
+ use crate::schema::comments;
+ // use crate::schema::comments::dsl::*;
+ use crate::schema::users;
+ use diesel::prelude::*;
+ comments::table
+ .inner_join(users::table)
+ // .filter(comments::article_id.eq(article_id))
+ .get_results::<(Comment, User)>(conn)?
+ };
+
+ let comments = comments
+ .iter()
+ .map(|(comment, user)| {
+ // TODO: avoid N+1. Write one query to fetch all data somehow.
+ let profile = user.to_profile(conn, current_user);
+ // Self::conver_user_to_profile(&ConverUserToProfile { user, current_user });
+
+ // TODO: avoid copy
+ (comment.to_owned(), profile)
+ })
+ .collect::>();
+
+ Ok(comments)
+ }
+
+ fn create(
+ &self,
+ body: String,
+ article_title_slug: String,
+ author: User,
+ ) -> Result<(Comment, Profile), AppError> {
+ let conn = &mut self.pool.get()?;
+
+ let article = Article::fetch_by_slug_and_author_id(
+ conn,
+ &FetchBySlugAndAuthorId {
+ slug: article_title_slug.to_owned(),
+ author_id: author.id,
+ },
+ )?;
+ let comment = Comment::create(
+ conn,
+ &CreateComment {
+ body: body.to_string(),
+ author_id: author.id,
+ article_id: article.id.to_owned(),
+ },
+ )?;
+ let profile = author.fetch_profile(conn, &author.id)?;
+ Ok((comment, profile))
+ }
+
+ fn delete(
+ &self,
+ article_title_slug: &str,
+ comment_id: Uuid,
+ author_id: Uuid,
+ ) -> Result<(), AppError> {
+ let conn = &mut self.pool.get()?;
+ let article = Article::fetch_by_slug_and_author_id(
+ conn,
+ &FetchBySlugAndAuthorId {
+ slug: article_title_slug.to_owned(),
+ author_id,
+ },
+ )?;
+ Comment::delete(
+ conn,
+ &DeleteComment {
+ comment_id,
+ article_id: article.id,
+ author_id,
+ },
+ )?;
+ Ok(())
+ }
+}
diff --git a/src/app/comment/request.rs b/src/app/features/comment/request.rs
similarity index 100%
rename from src/app/comment/request.rs
rename to src/app/features/comment/request.rs
diff --git a/src/app/features/comment/usecases.rs b/src/app/features/comment/usecases.rs
new file mode 100644
index 0000000..d74d1dd
--- /dev/null
+++ b/src/app/features/comment/usecases.rs
@@ -0,0 +1,56 @@
+use super::presenters::CommentPresenter;
+use super::repositories::CommentRepository;
+use crate::app::features::user::entities::User;
+use crate::error::AppError;
+use actix_web::HttpResponse;
+use std::sync::Arc;
+use uuid::Uuid;
+
+#[derive(Clone)]
+pub struct CommentUsecase {
+ comment_repository: Arc,
+ comment_presenter: Arc,
+}
+
+impl CommentUsecase {
+ pub fn new(
+ comment_repository: Arc,
+ comment_presenter: Arc,
+ ) -> Self {
+ Self {
+ comment_repository,
+ comment_presenter,
+ }
+ }
+
+ pub fn fetch_comments_list(&self, user: &Option) -> Result {
+ let result = self.comment_repository.fetch_comments_list(user)?;
+ let res = self.comment_presenter.from_comment_and_profile_list(result);
+ Ok(res)
+ }
+
+ pub fn create(
+ &self,
+ body: String,
+ article_title_slug: String,
+ author: User,
+ ) -> Result {
+ let result = self
+ .comment_repository
+ .create(body, article_title_slug, author)?;
+ let res = self.comment_presenter.from_comment_and_profile(result);
+ Ok(res)
+ }
+
+ pub fn delete(
+ &self,
+ article_title_slug: &str,
+ comment_id: Uuid,
+ author_id: Uuid,
+ ) -> Result {
+ self.comment_repository
+ .delete(&article_title_slug, comment_id, author_id);
+ let res = self.comment_presenter.toHttpRes();
+ Ok(res)
+ }
+}
diff --git a/src/app/features/favorite/controllers.rs b/src/app/features/favorite/controllers.rs
new file mode 100644
index 0000000..925f2b6
--- /dev/null
+++ b/src/app/features/favorite/controllers.rs
@@ -0,0 +1,32 @@
+use crate::app::drivers::middlewares::auth;
+use crate::app::drivers::middlewares::state::AppState;
+use crate::utils::api::ApiResponse;
+use actix_web::{web, HttpRequest};
+
+type ArticleIdSlug = String;
+
+pub async fn favorite(
+ state: web::Data,
+ req: HttpRequest,
+ path: web::Path,
+) -> ApiResponse {
+ let current_user = auth::get_current_user(&req)?;
+ let article_title_slug = path.into_inner();
+ state
+ .di_container
+ .favorite_usecase
+ .favorite(current_user, article_title_slug)
+}
+
+pub async fn unfavorite(
+ state: web::Data,
+ req: HttpRequest,
+ path: web::Path,
+) -> ApiResponse {
+ let current_user = auth::get_current_user(&req)?;
+ let article_title_slug = path.into_inner();
+ state
+ .di_container
+ .favorite_usecase
+ .unfavorite(current_user, article_title_slug)
+}
diff --git a/src/app/favorite/model.rs b/src/app/features/favorite/entities.rs
similarity index 95%
rename from src/app/favorite/model.rs
rename to src/app/features/favorite/entities.rs
index 01009b1..582f35a 100644
--- a/src/app/favorite/model.rs
+++ b/src/app/features/favorite/entities.rs
@@ -1,5 +1,5 @@
-use crate::app::article::model::Article;
-use crate::app::user::model::User;
+use crate::app::features::article::entities::Article;
+use crate::app::features::user::entities::User;
use crate::error::AppError;
use crate::schema::favorites;
use chrono::NaiveDateTime;
diff --git a/src/app/features/favorite/mod.rs b/src/app/features/favorite/mod.rs
new file mode 100644
index 0000000..9c2709f
--- /dev/null
+++ b/src/app/features/favorite/mod.rs
@@ -0,0 +1,5 @@
+pub mod controllers;
+pub mod entities;
+pub mod presenters;
+pub mod repositories;
+pub mod usecases;
diff --git a/src/app/features/favorite/presenters.rs b/src/app/features/favorite/presenters.rs
new file mode 100644
index 0000000..86fe1a2
--- /dev/null
+++ b/src/app/features/favorite/presenters.rs
@@ -0,0 +1,27 @@
+use super::entities::FavoriteInfo;
+use crate::app::features::article::entities::Article;
+pub use crate::app::features::article::presenters::SingleArticleResponse;
+use crate::app::features::profile::entities::Profile;
+use crate::app::features::tag::entities::Tag;
+use actix_web::HttpResponse;
+
+pub trait FavoritePresenter: Send + Sync + 'static {
+ fn complete(&self, item: (Article, Profile, FavoriteInfo, Vec)) -> HttpResponse;
+}
+
+#[derive(Clone)]
+pub struct FavoritePresenterImpl {}
+impl FavoritePresenterImpl {
+ pub fn new() -> Self {
+ Self {}
+ }
+}
+impl FavoritePresenter for FavoritePresenterImpl {
+ fn complete(
+ &self,
+ (article, profile, favorite_info, tags_list): (Article, Profile, FavoriteInfo, Vec),
+ ) -> HttpResponse {
+ let res_model = SingleArticleResponse::from((article, profile, favorite_info, tags_list));
+ HttpResponse::Ok().json(res_model)
+ }
+}
diff --git a/src/app/features/favorite/repositories.rs b/src/app/features/favorite/repositories.rs
new file mode 100644
index 0000000..2553c89
--- /dev/null
+++ b/src/app/features/favorite/repositories.rs
@@ -0,0 +1,62 @@
+use super::entities::{CreateFavorite, DeleteFavorite, Favorite};
+use crate::app::features::article::entities::{Article, FetchBySlugAndAuthorId};
+use crate::app::features::user::entities::User;
+use crate::error::AppError;
+use crate::utils::db::DbPool;
+
+pub trait FavoriteRepository: Send + Sync + 'static {
+ fn favorite(&self, user: User, article_title_slug: String) -> Result;
+ fn unfavorite(&self, user: User, article_title_slug: String) -> Result;
+}
+
+#[derive(Clone)]
+pub struct FavoriteRepositoryImpl {
+ pool: DbPool,
+}
+
+impl FavoriteRepositoryImpl {
+ pub fn new(pool: DbPool) -> Self {
+ Self { pool }
+ }
+}
+impl FavoriteRepository for FavoriteRepositoryImpl {
+ fn favorite(&self, user: User, article_title_slug: String) -> Result {
+ let conn = &mut self.pool.get()?;
+
+ let article = Article::fetch_by_slug_and_author_id(
+ conn,
+ &FetchBySlugAndAuthorId {
+ slug: article_title_slug.to_owned(),
+ author_id: user.id,
+ },
+ )?;
+ Favorite::create(
+ conn,
+ &CreateFavorite {
+ user_id: user.id,
+ article_id: article.id,
+ },
+ )?;
+
+ Ok(article)
+ }
+
+ fn unfavorite(&self, user: User, article_title_slug: String) -> Result {
+ let conn = &mut self.pool.get()?;
+ let article = Article::fetch_by_slug_and_author_id(
+ conn,
+ &FetchBySlugAndAuthorId {
+ slug: article_title_slug.to_owned(),
+ author_id: user.id,
+ },
+ )?;
+ Favorite::delete(
+ conn,
+ &DeleteFavorite {
+ user_id: user.id,
+ article_id: article.id,
+ },
+ )?;
+ Ok(article)
+ }
+}
diff --git a/src/app/features/favorite/usecases.rs b/src/app/features/favorite/usecases.rs
new file mode 100644
index 0000000..bb431ce
--- /dev/null
+++ b/src/app/features/favorite/usecases.rs
@@ -0,0 +1,67 @@
+use super::presenters::FavoritePresenter;
+use super::repositories::FavoriteRepository;
+use crate::app::features::article::repositories::{ArticleRepository, FetchArticleRepositoryInput};
+use crate::app::features::user::entities::User;
+use crate::error::AppError;
+use actix_web::HttpResponse;
+use std::sync::Arc;
+
+#[derive(Clone)]
+pub struct FavoriteUsecase {
+ favorite_repository: Arc,
+ favorite_presenter: Arc,
+ article_repository: Arc,
+}
+
+impl FavoriteUsecase {
+ pub fn new(
+ favorite_repository: Arc,
+ favorite_presenter: Arc,
+ article_repository: Arc,
+ ) -> Self {
+ Self {
+ favorite_repository,
+ favorite_presenter,
+ article_repository,
+ }
+ }
+
+ pub fn favorite(
+ &self,
+ user: User,
+ article_title_slug: String,
+ ) -> Result {
+ let article = self
+ .favorite_repository
+ .favorite(user.clone(), article_title_slug)?;
+
+ let result = self
+ .article_repository
+ .fetch_article_item(&FetchArticleRepositoryInput {
+ article_id: article.id,
+ current_user: user,
+ })?;
+ let res = self.favorite_presenter.complete(result);
+ Ok(res)
+ }
+
+ pub fn unfavorite(
+ &self,
+ user: User,
+ article_title_slug: String,
+ ) -> Result {
+ let article = self
+ .favorite_repository
+ .unfavorite(user.clone(), article_title_slug)?;
+
+ let result = self
+ .article_repository
+ .fetch_article_item(&FetchArticleRepositoryInput {
+ article_id: article.id,
+ current_user: user,
+ })?;
+
+ let res = self.favorite_presenter.complete(result);
+ Ok(res)
+ }
+}
diff --git a/src/app/follow/model.rs b/src/app/features/follow/entities.rs
similarity index 97%
rename from src/app/follow/model.rs
rename to src/app/features/follow/entities.rs
index f31e992..89ddb1e 100644
--- a/src/app/follow/model.rs
+++ b/src/app/features/follow/entities.rs
@@ -1,4 +1,4 @@
-use crate::app::user::model::User;
+use crate::app::features::user::entities::User;
use crate::error::AppError;
use crate::schema::follows;
use chrono::NaiveDateTime;
diff --git a/src/app/features/follow/mod.rs b/src/app/features/follow/mod.rs
new file mode 100644
index 0000000..0b8f0b5
--- /dev/null
+++ b/src/app/features/follow/mod.rs
@@ -0,0 +1 @@
+pub mod entities;
diff --git a/src/app/healthcheck/api.rs b/src/app/features/healthcheck/controllers.rs
similarity index 100%
rename from src/app/healthcheck/api.rs
rename to src/app/features/healthcheck/controllers.rs
diff --git a/src/app/features/healthcheck/mod.rs b/src/app/features/healthcheck/mod.rs
new file mode 100644
index 0000000..f916674
--- /dev/null
+++ b/src/app/features/healthcheck/mod.rs
@@ -0,0 +1 @@
+pub mod controllers;
diff --git a/src/app/features/mod.rs b/src/app/features/mod.rs
new file mode 100644
index 0000000..4ef9b41
--- /dev/null
+++ b/src/app/features/mod.rs
@@ -0,0 +1,8 @@
+pub mod article;
+pub mod comment;
+pub mod favorite;
+pub mod follow;
+pub mod healthcheck;
+pub mod profile;
+pub mod tag;
+pub mod user;
diff --git a/src/app/features/profile/controllers.rs b/src/app/features/profile/controllers.rs
new file mode 100644
index 0000000..628cee8
--- /dev/null
+++ b/src/app/features/profile/controllers.rs
@@ -0,0 +1,44 @@
+use crate::app::drivers::middlewares::{auth, state::AppState};
+use crate::utils::api::ApiResponse;
+use actix_web::{web, HttpRequest};
+
+type UsernameSlug = String;
+
+pub async fn show(
+ state: web::Data,
+ req: HttpRequest,
+ path: web::Path,
+) -> ApiResponse {
+ let current_user = auth::get_current_user(&req)?;
+ let username = path.into_inner();
+ state
+ .di_container
+ .profile_usecase
+ .show(¤t_user, &username)
+}
+
+pub async fn follow(
+ state: web::Data,
+ req: HttpRequest,
+ path: web::Path,
+) -> ApiResponse {
+ let current_user = auth::get_current_user(&req)?;
+ let target_username = path.into_inner();
+ state
+ .di_container
+ .profile_usecase
+ .follow(¤t_user, &target_username)
+}
+
+pub async fn unfollow(
+ state: web::Data,
+ req: HttpRequest,
+ path: web::Path,
+) -> ApiResponse {
+ let current_user = auth::get_current_user(&req)?;
+ let target_username = path.into_inner();
+ state
+ .di_container
+ .profile_usecase
+ .unfollow(¤t_user, &target_username)
+}
diff --git a/src/app/profile/model.rs b/src/app/features/profile/entities.rs
similarity index 100%
rename from src/app/profile/model.rs
rename to src/app/features/profile/entities.rs
diff --git a/src/app/features/profile/mod.rs b/src/app/features/profile/mod.rs
new file mode 100644
index 0000000..9c2709f
--- /dev/null
+++ b/src/app/features/profile/mod.rs
@@ -0,0 +1,5 @@
+pub mod controllers;
+pub mod entities;
+pub mod presenters;
+pub mod repositories;
+pub mod usecases;
diff --git a/src/app/profile/response.rs b/src/app/features/profile/presenters.rs
similarity index 56%
rename from src/app/profile/response.rs
rename to src/app/features/profile/presenters.rs
index c7e3320..ebefafa 100644
--- a/src/app/profile/response.rs
+++ b/src/app/features/profile/presenters.rs
@@ -1,6 +1,8 @@
-use crate::app::profile::model::Profile as ProfileModel;
+use super::entities::Profile as ProfileModel;
+use actix_web::HttpResponse;
use serde::{Deserialize, Serialize};
use std::convert::From;
+
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ProfileResponse {
pub profile: ProfileContent,
@@ -25,3 +27,21 @@ impl From for ProfileResponse {
ProfileResponse { profile }
}
}
+
+pub trait ProfilePresenter: Send + Sync + 'static {
+ fn from_profile(&self, model: ProfileModel) -> HttpResponse;
+}
+
+#[derive(Clone)]
+pub struct ProfilePresenterImpl {}
+impl ProfilePresenterImpl {
+ pub fn new() -> Self {
+ Self {}
+ }
+}
+impl ProfilePresenter for ProfilePresenterImpl {
+ fn from_profile(&self, model: ProfileModel) -> HttpResponse {
+ let res_model = ProfileResponse::from(model);
+ HttpResponse::Ok().json(res_model)
+ }
+}
diff --git a/src/app/features/profile/repositories.rs b/src/app/features/profile/repositories.rs
new file mode 100644
index 0000000..a1ee8a7
--- /dev/null
+++ b/src/app/features/profile/repositories.rs
@@ -0,0 +1,30 @@
+use super::entities::Profile;
+use crate::app::features::user::entities::User;
+use crate::error::AppError;
+use crate::utils::db::DbPool;
+
+pub trait ProfileRepository: Send + Sync + 'static {
+ fn fetch_by_name(&self, current_user: &User, username: &str) -> Result;
+}
+
+#[derive(Clone)]
+pub struct ProfileRepositoryImpl {
+ pool: DbPool,
+}
+
+impl ProfileRepositoryImpl {
+ pub fn new(pool: DbPool) -> Self {
+ Self { pool }
+ }
+}
+
+impl ProfileRepository for ProfileRepositoryImpl {
+ fn fetch_by_name(&self, current_user: &User, username: &str) -> Result {
+ let conn = &mut self.pool.get()?;
+ let profile = {
+ let followee = User::find_by_username(conn, username)?;
+ current_user.fetch_profile(conn, &followee.id)?
+ };
+ Ok(profile)
+ }
+}
diff --git a/src/app/features/profile/usecases.rs b/src/app/features/profile/usecases.rs
new file mode 100644
index 0000000..db6e262
--- /dev/null
+++ b/src/app/features/profile/usecases.rs
@@ -0,0 +1,55 @@
+use super::presenters::ProfilePresenter;
+use super::repositories::ProfileRepository;
+use crate::app::features::user::entities::User;
+use crate::app::features::user::repositories::UserRepository;
+use crate::error::AppError;
+use actix_web::HttpResponse;
+use std::sync::Arc;
+
+#[derive(Clone)]
+pub struct ProfileUsecase {
+ user_repository: Arc,
+ profile_repository: Arc,
+ presenter: Arc,
+}
+
+impl ProfileUsecase {
+ pub fn new(
+ profile_repository: Arc,
+ user_repository: Arc,
+ presenter: Arc,
+ ) -> Self {
+ Self {
+ profile_repository,
+ user_repository,
+ presenter,
+ }
+ }
+
+ pub fn show(&self, current_user: &User, username: &str) -> Result {
+ let profile = self
+ .profile_repository
+ .fetch_by_name(current_user, username)?;
+ Ok(self.presenter.from_profile(profile))
+ }
+
+ pub fn follow(
+ &self,
+ current_user: &User,
+ target_username: &str,
+ ) -> Result {
+ let profile = self.user_repository.follow(current_user, target_username)?;
+ Ok(self.presenter.from_profile(profile))
+ }
+
+ pub fn unfollow(
+ &self,
+ current_user: &User,
+ target_username: &str,
+ ) -> Result {
+ let profile = self
+ .user_repository
+ .unfollow(current_user, target_username)?;
+ Ok(self.presenter.from_profile(profile))
+ }
+}
diff --git a/src/app/features/tag/controllers.rs b/src/app/features/tag/controllers.rs
new file mode 100644
index 0000000..edc4e69
--- /dev/null
+++ b/src/app/features/tag/controllers.rs
@@ -0,0 +1,8 @@
+extern crate serde_json;
+use crate::app::drivers::middlewares::state::AppState;
+use crate::utils::api::ApiResponse;
+use actix_web::web;
+
+pub async fn index(state: web::Data) -> ApiResponse {
+ state.di_container.tag_usecase.list()
+}
diff --git a/src/app/tag/model.rs b/src/app/features/tag/entities.rs
similarity index 98%
rename from src/app/tag/model.rs
rename to src/app/features/tag/entities.rs
index c7dcd13..d1755a0 100644
--- a/src/app/tag/model.rs
+++ b/src/app/features/tag/entities.rs
@@ -1,4 +1,4 @@
-use crate::app::article::model::Article;
+use crate::app::features::article::entities::Article;
use crate::error::AppError;
use crate::schema::tags;
use chrono::NaiveDateTime;
diff --git a/src/app/features/tag/mod.rs b/src/app/features/tag/mod.rs
new file mode 100644
index 0000000..9c2709f
--- /dev/null
+++ b/src/app/features/tag/mod.rs
@@ -0,0 +1,5 @@
+pub mod controllers;
+pub mod entities;
+pub mod presenters;
+pub mod repositories;
+pub mod usecases;
diff --git a/src/app/features/tag/presenters.rs b/src/app/features/tag/presenters.rs
new file mode 100644
index 0000000..9c70b31
--- /dev/null
+++ b/src/app/features/tag/presenters.rs
@@ -0,0 +1,34 @@
+use super::entities::Tag;
+use actix_web::HttpResponse;
+use serde::{Deserialize, Serialize};
+
+#[derive(Deserialize, Serialize, Debug, Clone)]
+pub struct TagsResponse {
+ // SPEC: https://gothinkster.github.io/realworld/docs/specs/backend-specs/endpoints#registration
+ pub tags: Vec,
+}
+
+impl std::convert::From> for TagsResponse {
+ fn from(tags: Vec) -> Self {
+ let list = tags.iter().map(move |tag| tag.name.clone()).collect();
+ TagsResponse { tags: list }
+ }
+}
+
+pub trait TagPresenter: Send + Sync + 'static {
+ fn from_list(&self, list: Vec) -> HttpResponse;
+}
+
+#[derive(Clone)]
+pub struct TagPresenterImpl {}
+impl TagPresenterImpl {
+ pub fn new() -> Self {
+ Self {}
+ }
+}
+impl TagPresenter for TagPresenterImpl {
+ fn from_list(&self, list: Vec) -> HttpResponse {
+ let res = TagsResponse::from(list);
+ HttpResponse::Ok().json(res)
+ }
+}
diff --git a/src/app/features/tag/repositories.rs b/src/app/features/tag/repositories.rs
new file mode 100644
index 0000000..2b8b025
--- /dev/null
+++ b/src/app/features/tag/repositories.rs
@@ -0,0 +1,25 @@
+use super::entities::Tag;
+use crate::error::AppError;
+use crate::utils::db::DbPool;
+
+pub trait TagRepository: Send + Sync + 'static {
+ fn list(&self) -> Result, AppError>;
+}
+
+#[derive(Clone)]
+pub struct TagRepositoryImpl {
+ pool: DbPool,
+}
+
+impl TagRepositoryImpl {
+ pub fn new(pool: DbPool) -> Self {
+ Self { pool }
+ }
+}
+
+impl TagRepository for TagRepositoryImpl {
+ fn list(&self) -> Result, AppError> {
+ let conn = &mut self.pool.get()?;
+ Tag::fetch(conn)
+ }
+}
diff --git a/src/app/features/tag/usecases.rs b/src/app/features/tag/usecases.rs
new file mode 100644
index 0000000..72f02c4
--- /dev/null
+++ b/src/app/features/tag/usecases.rs
@@ -0,0 +1,29 @@
+use super::presenters::TagPresenter;
+use super::repositories::TagRepository;
+use crate::error::AppError;
+use actix_web::HttpResponse;
+use std::sync::Arc;
+
+#[derive(Clone)]
+pub struct TagUsecase {
+ tag_repository: Arc,
+ tag_presenter: Arc,
+}
+
+impl TagUsecase {
+ pub fn new(
+ tag_repository: Arc,
+ tag_presenter: Arc,
+ ) -> Self {
+ Self {
+ tag_repository,
+ tag_presenter,
+ }
+ }
+
+ pub fn list(&self) -> Result {
+ let list = self.tag_repository.list()?;
+ let res = self.tag_presenter.from_list(list);
+ Ok(res)
+ }
+}
diff --git a/src/app/features/user/controllers.rs b/src/app/features/user/controllers.rs
new file mode 100644
index 0000000..b8e7a1c
--- /dev/null
+++ b/src/app/features/user/controllers.rs
@@ -0,0 +1,44 @@
+use super::entities::UpdateUser;
+use super::requests;
+use crate::app::drivers::middlewares::auth;
+use crate::app::drivers::middlewares::state::AppState;
+use crate::utils::api::ApiResponse;
+use actix_web::{web, HttpRequest};
+
+pub async fn signin(state: web::Data, form: web::Json) -> ApiResponse {
+ state
+ .di_container
+ .user_usecase
+ .signin(&form.user.email, &form.user.password)
+}
+
+pub async fn signup(state: web::Data, form: web::Json) -> ApiResponse {
+ state.di_container.user_usecase.signup(
+ &form.user.email,
+ &form.user.username,
+ &form.user.password,
+ )
+}
+
+pub async fn me(state: web::Data, req: HttpRequest) -> ApiResponse {
+ let current_user = auth::get_current_user(&req)?;
+ state.di_container.user_usecase.me(¤t_user)
+}
+
+pub async fn update(
+ state: web::Data,
+ req: HttpRequest,
+ form: web::Json,
+) -> ApiResponse {
+ let current_user = auth::get_current_user(&req)?;
+ state.di_container.user_usecase.update(
+ current_user.id,
+ UpdateUser {
+ email: form.user.email.clone(),
+ username: form.user.username.clone(),
+ password: form.user.password.clone(),
+ image: form.user.image.clone(),
+ bio: form.user.bio.clone(),
+ },
+ )
+}
diff --git a/src/app/user/model.rs b/src/app/features/user/entities.rs
similarity index 79%
rename from src/app/user/model.rs
rename to src/app/features/user/entities.rs
index 82f911a..5b7aa8f 100644
--- a/src/app/user/model.rs
+++ b/src/app/features/user/entities.rs
@@ -1,6 +1,6 @@
-use crate::app::favorite::model::Favorite;
-use crate::app::follow::model::{CreateFollow, DeleteFollow, Follow};
-use crate::app::profile::model::Profile;
+use crate::app::features::favorite::entities::Favorite;
+use crate::app::features::follow::entities::Follow;
+use crate::app::features::profile::entities::Profile;
use crate::error::AppError;
use crate::schema::users;
use crate::utils::{hasher, token};
@@ -46,7 +46,7 @@ impl User {
users::username.eq(username)
}
- fn by_username(username: &str) -> ByUsername
+ pub fn by_username(username: &str) -> ByUsername
where
DB: Backend,
{
@@ -125,46 +125,6 @@ impl User {
Ok(user)
}
- pub fn follow(&self, conn: &mut PgConnection, username: &str) -> Result {
- let t = Self::by_username(username);
- let followee = t.first::(conn)?;
-
- Follow::create(
- conn,
- &CreateFollow {
- follower_id: self.id,
- followee_id: followee.id,
- },
- )?;
-
- Ok(Profile {
- username: self.username.clone(),
- bio: self.bio.clone(),
- image: self.image.clone(),
- following: true,
- })
- }
-
- pub fn unfollow(&self, conn: &mut PgConnection, username: &str) -> Result {
- let t = Self::by_username(username);
- let followee = t.first::(conn)?;
-
- Follow::delete(
- conn,
- &DeleteFollow {
- followee_id: followee.id,
- follower_id: self.id,
- },
- )?;
-
- Ok(Profile {
- username: self.username.clone(),
- bio: self.bio.clone(),
- image: self.image.clone(),
- following: false,
- })
- }
-
pub fn is_following(&self, conn: &mut PgConnection, followee_id: &Uuid) -> bool {
use crate::schema::follows;
let t = follows::table
@@ -208,6 +168,21 @@ impl User {
};
Ok(profile)
}
+
+ pub fn to_profile(&self, conn: &mut PgConnection, current_user: &Option) -> Profile {
+ let user = self;
+ let following = match current_user {
+ Some(current_user) => current_user.is_following(conn, &user.id),
+ None => false,
+ };
+
+ Profile {
+ username: user.username.to_owned(),
+ bio: user.bio.to_owned(),
+ image: user.image.to_owned(),
+ following,
+ }
+ }
}
#[derive(Insertable, Debug, Deserialize)]
diff --git a/src/app/features/user/mod.rs b/src/app/features/user/mod.rs
new file mode 100644
index 0000000..7f85232
--- /dev/null
+++ b/src/app/features/user/mod.rs
@@ -0,0 +1,6 @@
+pub mod controllers;
+pub mod entities;
+pub mod presenters;
+pub mod repositories;
+pub mod requests;
+pub mod usecases;
diff --git a/src/app/user/response.rs b/src/app/features/user/presenters.rs
similarity index 59%
rename from src/app/user/response.rs
rename to src/app/features/user/presenters.rs
index afeac3d..5967144 100644
--- a/src/app/user/response.rs
+++ b/src/app/features/user/presenters.rs
@@ -1,4 +1,5 @@
-use crate::app::user::model::User;
+use crate::app::features::user::entities::User;
+use actix_web::HttpResponse;
use serde::{Deserialize, Serialize};
use std::convert::From;
@@ -30,3 +31,20 @@ pub struct AuthUser {
pub bio: Option,
pub image: Option,
}
+
+pub trait UserPresenter: Send + Sync + 'static {
+ fn from_user_and_token(&self, user: User, token: String) -> HttpResponse;
+}
+#[derive(Clone)]
+pub struct UserPresenterImpl {}
+impl UserPresenterImpl {
+ pub fn new() -> Self {
+ Self {}
+ }
+}
+impl UserPresenter for UserPresenterImpl {
+ fn from_user_and_token(&self, user: User, token: String) -> HttpResponse {
+ let res_model = UserResponse::from((user, token));
+ HttpResponse::Ok().json(res_model)
+ }
+}
diff --git a/src/app/features/user/repositories.rs b/src/app/features/user/repositories.rs
new file mode 100644
index 0000000..c45db55
--- /dev/null
+++ b/src/app/features/user/repositories.rs
@@ -0,0 +1,112 @@
+use super::entities::UpdateUser;
+use crate::app::features::follow::entities::{CreateFollow, DeleteFollow, Follow};
+use crate::app::features::profile::entities::Profile;
+use crate::app::features::user::entities::User;
+use crate::error::AppError;
+use crate::utils::db::DbPool;
+use uuid::Uuid;
+
+type Token = String;
+
+pub trait UserRepository: Send + Sync + 'static {
+ fn me<'a>(&self, user: &'a User) -> Result<(&'a User, Token), AppError>;
+ fn signin(&self, email: &str, naive_password: &str) -> Result<(User, Token), AppError>;
+ fn signup(
+ &self,
+ email: &str,
+ username: &str,
+ naive_password: &str,
+ ) -> Result<(User, Token), AppError>;
+ fn follow(&self, current_user: &User, target_username: &str) -> Result;
+ fn unfollow(&self, current_user: &User, target_username: &str) -> Result;
+ fn update(&self, user_id: Uuid, changeset: UpdateUser) -> Result<(User, Token), AppError>;
+}
+
+#[derive(Clone)]
+pub struct UserRepositoryImpl {
+ pool: DbPool,
+}
+
+impl UserRepositoryImpl {
+ pub fn new(pool: DbPool) -> Self {
+ Self { pool }
+ }
+}
+
+impl UserRepository for UserRepositoryImpl {
+ fn me<'a>(&self, current_user: &'a User) -> Result<(&'a User, Token), AppError> {
+ let token = current_user.generate_token()?;
+ Ok((current_user, token))
+ }
+
+ fn signin(&self, email: &str, naive_password: &str) -> Result<(User, Token), AppError> {
+ let conn = &mut self.pool.get()?;
+ User::signin(conn, email, naive_password)
+ }
+
+ fn signup(
+ &self,
+ email: &str,
+ username: &str,
+ naive_password: &str,
+ ) -> Result<(User, Token), AppError> {
+ let conn = &mut self.pool.get()?;
+ User::signup(conn, email, username, naive_password)
+ }
+
+ fn follow(&self, current_user: &User, target_username: &str) -> Result {
+ let conn = &mut self.pool.get()?;
+ let t = User::by_username(target_username);
+
+ let followee = {
+ use diesel::prelude::*;
+ t.first::(conn)?
+ };
+
+ Follow::create(
+ conn,
+ &CreateFollow {
+ follower_id: current_user.id,
+ followee_id: followee.id,
+ },
+ )?;
+
+ Ok(Profile {
+ username: current_user.username.clone(),
+ bio: current_user.bio.clone(),
+ image: current_user.image.clone(),
+ following: true,
+ })
+ }
+
+ fn unfollow(&self, current_user: &User, target_username: &str) -> Result {
+ let conn = &mut self.pool.get()?;
+ let t = User::by_username(target_username);
+ let followee = {
+ use diesel::prelude::*;
+ t.first::(conn)?
+ };
+
+ Follow::delete(
+ conn,
+ &DeleteFollow {
+ followee_id: followee.id,
+ follower_id: current_user.id,
+ },
+ )?;
+
+ Ok(Profile {
+ username: current_user.username.clone(),
+ bio: current_user.bio.clone(),
+ image: current_user.image.clone(),
+ following: false,
+ })
+ }
+
+ fn update(&self, user_id: Uuid, changeset: UpdateUser) -> Result<(User, Token), AppError> {
+ let conn = &mut self.pool.get()?;
+ let new_user = User::update(conn, user_id, changeset)?;
+ let token = &new_user.generate_token()?;
+ Ok((new_user, token.clone()))
+ }
+}
diff --git a/src/app/user/request.rs b/src/app/features/user/requests.rs
similarity index 100%
rename from src/app/user/request.rs
rename to src/app/features/user/requests.rs
diff --git a/src/app/features/user/usecases.rs b/src/app/features/user/usecases.rs
new file mode 100644
index 0000000..9f5c795
--- /dev/null
+++ b/src/app/features/user/usecases.rs
@@ -0,0 +1,54 @@
+use super::entities::{UpdateUser, User};
+use super::presenters::UserPresenter;
+use super::repositories::UserRepository;
+use crate::error::AppError;
+use actix_web::HttpResponse;
+use std::sync::Arc;
+use uuid::Uuid;
+
+#[derive(Clone)]
+pub struct UserUsecase {
+ user_repository: Arc,
+ user_presenter: Arc,
+}
+
+impl UserUsecase {
+ pub fn new(
+ user_repository: Arc,
+ user_presenter: Arc,
+ ) -> Self {
+ Self {
+ user_repository,
+ user_presenter,
+ }
+ }
+
+ pub fn signin(&self, email: &str, password: &str) -> Result {
+ let (user, token) = self.user_repository.signin(email, password)?;
+ let res = self.user_presenter.from_user_and_token(user, token);
+ Ok(res)
+ }
+
+ pub fn signup(
+ &self,
+ email: &str,
+ username: &str,
+ password: &str,
+ ) -> Result {
+ let (user, token) = self.user_repository.signup(email, username, password)?;
+ let res = self.user_presenter.from_user_and_token(user, token);
+ Ok(res)
+ }
+
+ pub fn me(&self, current_user: &User) -> Result {
+ let (user, token) = self.user_repository.me(current_user)?;
+ let res = self.user_presenter.from_user_and_token(user.clone(), token);
+ Ok(res)
+ }
+
+ pub fn update(&self, user_id: Uuid, changeset: UpdateUser) -> Result {
+ let (new_user, token) = self.user_repository.update(user_id, changeset)?;
+ let res = self.user_presenter.from_user_and_token(new_user, token);
+ Ok(res)
+ }
+}
diff --git a/src/app/follow/mod.rs b/src/app/follow/mod.rs
deleted file mode 100644
index 65880be..0000000
--- a/src/app/follow/mod.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub mod model;
diff --git a/src/app/healthcheck/mod.rs b/src/app/healthcheck/mod.rs
deleted file mode 100644
index e5fdf85..0000000
--- a/src/app/healthcheck/mod.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub mod api;
diff --git a/src/app/mod.rs b/src/app/mod.rs
index d8e1fa0..710b127 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -1,8 +1,2 @@
-pub mod article;
-pub mod comment;
-pub mod favorite;
-pub mod follow;
-pub mod profile;
-pub mod tag;
-pub mod user;
-pub mod healthcheck;
\ No newline at end of file
+pub mod drivers;
+pub mod features;
diff --git a/src/app/profile/api.rs b/src/app/profile/api.rs
deleted file mode 100644
index 073ec35..0000000
--- a/src/app/profile/api.rs
+++ /dev/null
@@ -1,52 +0,0 @@
-use super::response::ProfileResponse;
-use super::service;
-use crate::middleware::{auth, state::AppState};
-use crate::utils::api::ApiResponse;
-use actix_web::{web, HttpRequest, HttpResponse};
-
-type UsernameSlug = String;
-
-pub async fn show(
- state: web::Data,
- req: HttpRequest,
- path: web::Path,
-) -> ApiResponse {
- let conn = &mut state.get_conn()?;
- let current_user = auth::get_current_user(&req)?;
- let _username = path.into_inner();
- let profile = service::fetch_by_name(
- conn,
- &service::FetchProfileByName {
- current_user,
- username: _username,
- },
- )?;
- let res = ProfileResponse::from(profile);
- Ok(HttpResponse::Ok().json(res))
-}
-
-pub async fn follow(
- state: web::Data,
- req: HttpRequest,
- path: web::Path,
-) -> ApiResponse {
- let conn = &mut state.get_conn()?;
- let current_user = auth::get_current_user(&req)?;
- let username = path.into_inner();
- let profile = current_user.follow(conn, &username)?;
- let res = ProfileResponse::from(profile);
- Ok(HttpResponse::Ok().json(res))
-}
-
-pub async fn unfollow(
- state: web::Data,
- req: HttpRequest,
- path: web::Path,
-) -> ApiResponse {
- let conn = &mut state.get_conn()?;
- let current_user = auth::get_current_user(&req)?;
- let username = path.into_inner();
- let profile = current_user.unfollow(conn, &username)?;
- let res = ProfileResponse::from(profile);
- Ok(HttpResponse::Ok().json(res))
-}
diff --git a/src/app/profile/mod.rs b/src/app/profile/mod.rs
deleted file mode 100644
index a451d09..0000000
--- a/src/app/profile/mod.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-pub mod api;
-pub mod model;
-pub mod response;
-pub mod service;
diff --git a/src/app/profile/service.rs b/src/app/profile/service.rs
deleted file mode 100644
index fcfbb81..0000000
--- a/src/app/profile/service.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use super::model::Profile;
-use crate::app::user::model::User;
-use crate::error::AppError;
-use diesel::pg::PgConnection;
-
-pub struct FetchProfileByName {
- pub current_user: User,
- pub username: String,
-}
-
-pub fn fetch_by_name(
- conn: &mut PgConnection,
- FetchProfileByName {
- current_user,
- username,
- }: &FetchProfileByName,
-) -> Result {
- let profile = {
- let followee = User::find_by_username(conn, username)?;
- current_user.fetch_profile(conn, &followee.id)?
- };
- Ok(profile)
-}
-
-pub struct ConverUserToProfile<'a> {
- pub user: &'a User,
- pub current_user: &'a Option