Skip to content

Commit c35b1cc

Browse files
committed
Restructure to Clean Architecture in the backend
1 parent a25b118 commit c35b1cc

14 files changed

Lines changed: 361 additions & 185 deletions

File tree

backend/src/api/handlers.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use axum::{
2+
extract::{Path, State},
3+
http::StatusCode,
4+
response::Json,
5+
routing::get,
6+
Router,
7+
};
8+
use std::sync::Arc;
9+
10+
use crate::api::AppState;
11+
use crate::application::ApplicationError;
12+
13+
fn map_error(e: ApplicationError) -> (StatusCode, Json<serde_json::Value>) {
14+
let (status, message) = if e.not_found() {
15+
(StatusCode::NOT_FOUND, e.to_string())
16+
} else {
17+
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
18+
};
19+
(status, Json(serde_json::json!({ "error": message })))
20+
}
21+
22+
pub async fn get_course(
23+
State(state): State<Arc<AppState>>,
24+
) -> Result<Json<crate::domain::Course>, (StatusCode, Json<serde_json::Value>)> {
25+
state.get_course.execute().map(Json).map_err(map_error)
26+
}
27+
28+
pub async fn get_lesson(
29+
State(state): State<Arc<AppState>>,
30+
Path(lesson_id): Path<String>,
31+
) -> Result<Json<crate::domain::Lesson>, (StatusCode, Json<serde_json::Value>)> {
32+
state
33+
.get_lesson
34+
.execute(&lesson_id)
35+
.map(Json)
36+
.map_err(map_error)
37+
}
38+
39+
pub async fn get_all_lessons(
40+
State(state): State<Arc<AppState>>,
41+
) -> Result<Json<Vec<crate::domain::Lesson>>, (StatusCode, Json<serde_json::Value>)> {
42+
state
43+
.get_all_lessons
44+
.execute()
45+
.map(Json)
46+
.map_err(map_error)
47+
}
48+
49+
pub async fn get_chapter(
50+
State(state): State<Arc<AppState>>,
51+
Path(chapter_id): Path<String>,
52+
) -> Result<Json<crate::domain::Chapter>, (StatusCode, Json<serde_json::Value>)> {
53+
state
54+
.get_chapter
55+
.execute(&chapter_id)
56+
.map(Json)
57+
.map_err(map_error)
58+
}
59+
60+
pub fn router(state: Arc<AppState>) -> Router {
61+
Router::new()
62+
.route("/api/course", get(get_course))
63+
.route("/api/lessons", get(get_all_lessons))
64+
.route("/api/lessons/:lesson_id", get(get_lesson))
65+
.route("/api/chapters/:chapter_id", get(get_chapter))
66+
.with_state(state)
67+
}

backend/src/api/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod handlers;
2+
mod state;
3+
4+
pub use handlers::router;
5+
pub use state::AppState;

backend/src/api/state.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use crate::application::{GetAllLessonsUseCase, GetChapterUseCase, GetCourseUseCase, GetLessonUseCase};
2+
3+
pub struct AppState {
4+
pub get_course: GetCourseUseCase,
5+
pub get_lesson: GetLessonUseCase,
6+
pub get_all_lessons: GetAllLessonsUseCase,
7+
pub get_chapter: GetChapterUseCase,
8+
}
9+
10+
impl AppState {
11+
pub fn new(
12+
get_course: GetCourseUseCase,
13+
get_lesson: GetLessonUseCase,
14+
get_all_lessons: GetAllLessonsUseCase,
15+
get_chapter: GetChapterUseCase,
16+
) -> Self {
17+
Self {
18+
get_course,
19+
get_lesson,
20+
get_all_lessons,
21+
get_chapter,
22+
}
23+
}
24+
}

backend/src/application/error.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use crate::domain::RepositoryError;
2+
use thiserror::Error;
3+
4+
#[derive(Error, Debug)]
5+
pub enum ApplicationError {
6+
#[error("Course data not found")]
7+
CourseNotFound,
8+
9+
#[error("Lesson not found: {0}")]
10+
LessonNotFound(String),
11+
12+
#[error("Chapter not found: {0}")]
13+
ChapterNotFound(String),
14+
15+
#[error("Failed to parse course data")]
16+
ParseCourseError,
17+
18+
#[error("Failed to parse lesson: {0}")]
19+
ParseLessonError(String),
20+
21+
#[error("Failed to read lessons directory")]
22+
ReadLessonsDirError,
23+
}
24+
25+
impl From<RepositoryError> for ApplicationError {
26+
fn from(e: RepositoryError) -> Self {
27+
match e {
28+
RepositoryError::CourseNotFound => ApplicationError::CourseNotFound,
29+
RepositoryError::LessonNotFound(s) => ApplicationError::LessonNotFound(s),
30+
RepositoryError::ChapterNotFound(s) => ApplicationError::ChapterNotFound(s),
31+
RepositoryError::ParseCourseError => ApplicationError::ParseCourseError,
32+
RepositoryError::ParseLessonError(s) => ApplicationError::ParseLessonError(s),
33+
RepositoryError::ReadLessonsDirError => ApplicationError::ReadLessonsDirError,
34+
}
35+
}
36+
}
37+
38+
impl ApplicationError {
39+
pub fn not_found(&self) -> bool {
40+
matches!(
41+
self,
42+
ApplicationError::CourseNotFound
43+
| ApplicationError::LessonNotFound(_)
44+
| ApplicationError::ChapterNotFound(_)
45+
)
46+
}
47+
}
48+
49+
pub type ApplicationResult<T> = Result<T, ApplicationError>;

backend/src/application/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod error;
2+
mod use_cases;
3+
4+
pub use error::ApplicationError;
5+
pub use use_cases::*;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use crate::application::{ApplicationResult, ApplicationError};
2+
use crate::domain::{Chapter, Course, CourseRepository, Lesson, LessonRepository};
3+
use std::sync::Arc;
4+
5+
pub struct GetCourseUseCase {
6+
repo: Arc<dyn CourseRepository>,
7+
}
8+
9+
impl GetCourseUseCase {
10+
pub fn new(repo: Arc<dyn CourseRepository>) -> Self {
11+
Self { repo }
12+
}
13+
14+
pub fn execute(&self) -> ApplicationResult<Course> {
15+
self.repo.get_course().map_err(ApplicationError::from)
16+
}
17+
}
18+
19+
pub struct GetLessonUseCase {
20+
repo: Arc<dyn LessonRepository>,
21+
}
22+
23+
impl GetLessonUseCase {
24+
pub fn new(repo: Arc<dyn LessonRepository>) -> Self {
25+
Self { repo }
26+
}
27+
28+
pub fn execute(&self, lesson_id: &str) -> ApplicationResult<Lesson> {
29+
self.repo.get_lesson(lesson_id).map_err(ApplicationError::from)
30+
}
31+
}
32+
33+
pub struct GetAllLessonsUseCase {
34+
repo: Arc<dyn LessonRepository>,
35+
}
36+
37+
impl GetAllLessonsUseCase {
38+
pub fn new(repo: Arc<dyn LessonRepository>) -> Self {
39+
Self { repo }
40+
}
41+
42+
pub fn execute(&self) -> ApplicationResult<Vec<Lesson>> {
43+
self.repo.get_all_lessons().map_err(ApplicationError::from)
44+
}
45+
}
46+
47+
pub struct GetChapterUseCase {
48+
repo: Arc<dyn CourseRepository>,
49+
}
50+
51+
impl GetChapterUseCase {
52+
pub fn new(repo: Arc<dyn CourseRepository>) -> Self {
53+
Self { repo }
54+
}
55+
56+
pub fn execute(&self, chapter_id: &str) -> ApplicationResult<Chapter> {
57+
self.repo.get_chapter(chapter_id).map_err(ApplicationError::from)
58+
}
59+
}
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ pub struct Exercise {
6767
}
6868

6969
#[derive(Debug, Serialize, Deserialize, Clone)]
70-
w#[serde(rename_all = "camelCase")]
70+
#[serde(rename_all = "camelCase")]
7171
pub struct ProjectIdea {
7272
pub id: String,
7373
pub title: String,
@@ -82,4 +82,3 @@ pub struct ProjectIdea {
8282
#[serde(default, rename = "learningOutcomes")]
8383
pub learning_outcomes: Option<Vec<String>>,
8484
}
85-

backend/src/domain/error.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use thiserror::Error;
2+
3+
#[derive(Error, Debug)]
4+
pub enum RepositoryError {
5+
#[error("Course data not found")]
6+
CourseNotFound,
7+
8+
#[error("Lesson not found: {0}")]
9+
LessonNotFound(String),
10+
11+
#[error("Chapter not found: {0}")]
12+
ChapterNotFound(String),
13+
14+
#[error("Failed to parse course data")]
15+
ParseCourseError,
16+
17+
#[error("Failed to parse lesson: {0}")]
18+
ParseLessonError(String),
19+
20+
#[error("Failed to read lessons directory")]
21+
ReadLessonsDirError,
22+
}

backend/src/domain/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
mod entities;
2+
mod error;
3+
mod ports;
4+
5+
pub use entities::*;
6+
pub use error::RepositoryError;
7+
pub use ports::{CourseRepository, LessonRepository};

backend/src/domain/ports.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use super::entities::{Chapter, Course, Lesson};
2+
use super::error::RepositoryError;
3+
4+
pub trait CourseRepository: Send + Sync {
5+
fn get_course(&self) -> Result<Course, RepositoryError>;
6+
fn get_chapter(&self, chapter_id: &str) -> Result<Chapter, RepositoryError>;
7+
}
8+
9+
pub trait LessonRepository: Send + Sync {
10+
fn get_lesson(&self, lesson_id: &str) -> Result<Lesson, RepositoryError>;
11+
fn get_all_lessons(&self) -> Result<Vec<Lesson>, RepositoryError>;
12+
}

0 commit comments

Comments
 (0)