Skip to content

Commit 35d60de

Browse files
committed
Add route for getting news.
1 parent 60d775a commit 35d60de

File tree

9 files changed

+182
-11
lines changed

9 files changed

+182
-11
lines changed

chloria-backend/Cargo.lock

Lines changed: 36 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

chloria-backend/chloria-api/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@ argon2 = { version = "0.5.3", features = ["std"] }
99
async-trait = "0.1.86"
1010
axum = "0.8.1"
1111
axum-extra = { version = "0.10.0", features = ["typed-header"] }
12-
diesel = { version = "2.2.7", features = ["postgres", "r2d2"] }
12+
chrono = "0.4.40"
13+
csv = "1.3.1"
14+
diesel = { version = "2.2.7", features = ["chrono", "postgres", "r2d2"] }
1315
jsonwebtoken = "9.3.1"
16+
log = "0.4.26"
1417
mockall = "0.13.1"
1518
serde = "1.0.218"
1619
serde_json = "1.0.140"
1720
serde_with = "3.12.0"
1821
strum = { version = "0.27.1", features = ["derive"] }
1922
tokio = { version = "1.43.0", features = ["rt-multi-thread"] }
23+
tokio-util = { version = "0.7.13", features = ["io"] }
2024
tower = "0.5.2"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use std::{io::Cursor, sync::Arc};
2+
3+
use anyhow::Result;
4+
use async_trait::async_trait;
5+
use chrono::NaiveDate;
6+
use csv::Writer;
7+
use log::error;
8+
use tokio_util::io::ReaderStream;
9+
10+
use super::{
11+
super::{ports::repository::Repository, workshop::Workshop},
12+
Case,
13+
};
14+
use crate::execution::ports::repository::SelectNewsInput;
15+
16+
pub(crate) struct GetNewsCaseInput {
17+
pub(crate) date: NaiveDate,
18+
}
19+
20+
pub(crate) struct GetNewsCaseOutput {
21+
pub(crate) articles_file: ReaderStream<Cursor<Vec<u8>>>,
22+
}
23+
24+
struct GetNewsCase {
25+
repository: Arc<dyn Repository>,
26+
input: GetNewsCaseInput,
27+
}
28+
29+
impl Workshop {
30+
pub(crate) async fn execute_get_news_case(&self, input: GetNewsCaseInput) -> Result<GetNewsCaseOutput> {
31+
let case = GetNewsCase {
32+
repository: Arc::clone(&self.repository),
33+
input,
34+
};
35+
self.run_case(case).await
36+
}
37+
}
38+
39+
#[async_trait]
40+
impl Case for GetNewsCase {
41+
type Output = GetNewsCaseOutput;
42+
43+
async fn execute(self) -> Result<Self::Output> {
44+
let mut writer = Writer::from_writer(vec![]);
45+
for select_news_output in self
46+
.repository
47+
.select_news(SelectNewsInput { date: self.input.date })
48+
.await?
49+
{
50+
if let Err(error) = writer.serialize(select_news_output) {
51+
error!("error={}", error);
52+
}
53+
}
54+
let output = GetNewsCaseOutput {
55+
articles_file: ReaderStream::new(Cursor::new(writer.into_inner()?)),
56+
};
57+
Ok(output)
58+
}
59+
}

chloria-backend/chloria-api/src/execution/cases/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub(crate) mod authenticate;
2+
pub(crate) mod get_news;
23

34
use anyhow::Result;
45
use async_trait::async_trait;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
use anyhow::Result;
22
use async_trait::async_trait;
3+
use chrono::NaiveDate;
34
use mockall::automock;
5+
use serde::Serialize;
6+
7+
pub(crate) struct SelectNewsInput {
8+
pub(crate) date: NaiveDate,
9+
}
10+
11+
#[derive(Serialize)]
12+
pub(crate) struct SelectNewsOutput {
13+
pub(crate) text: Option<String>,
14+
pub(crate) image_path: Option<String>,
15+
}
416

517
#[automock]
618
#[async_trait]
719
pub(crate) trait Repository: Send + Sync {
820
async fn select_client_api_secret(&self, api_key_input: &str) -> Result<Option<String>>;
21+
async fn select_news(&self, input: SelectNewsInput) -> Result<Vec<SelectNewsOutput>>;
922
}

chloria-backend/chloria-api/src/infrastructure/repository/postgresql.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
use anyhow::Result;
22
use async_trait::async_trait;
3+
use chrono::{DateTime, Duration, Local, NaiveTime};
34
use diesel::{
45
prelude::*,
56
r2d2::{ConnectionManager, Pool},
67
PgConnection,
78
};
89

9-
use crate::{execution::ports::repository::Repository, schema::client_credentials::dsl::*};
10+
use crate::{
11+
execution::ports::repository::{Repository, SelectNewsInput, SelectNewsOutput},
12+
schema::{
13+
client_credentials::dsl::*,
14+
news::{self, dsl::*},
15+
},
16+
};
1017

1118
pub(crate) struct PostgresqlClient {
1219
pool: Pool<ConnectionManager<PgConnection>>,
@@ -31,4 +38,21 @@ impl Repository for PostgresqlClient {
3138
.optional()?;
3239
Ok(api_secret_value)
3340
}
41+
42+
async fn select_news(&self, input: SelectNewsInput) -> Result<Vec<SelectNewsOutput>> {
43+
let date: DateTime<Local> = DateTime::from(input.date.and_time(NaiveTime::default()).and_utc());
44+
let next_date = date + Duration::days(1);
45+
let mut outputs = vec![];
46+
for (short_text_value, image_path_value) in news
47+
.filter(news::created_at.ge(date).and(news::created_at.lt(next_date)))
48+
.select((short_text, image_path))
49+
.get_results(&mut self.pool.get()?)?
50+
{
51+
outputs.push(SelectNewsOutput {
52+
text: short_text_value,
53+
image_path: image_path_value,
54+
});
55+
}
56+
Ok(outputs)
57+
}
3458
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use anyhow::Result;
2+
use axum::{
3+
body::Body,
4+
extract::{Query, State},
5+
http::{header, HeaderName, StatusCode},
6+
Json,
7+
};
8+
use chrono::NaiveDate;
9+
use serde::Deserialize;
10+
11+
use super::{super::state::RouterState, ErrorResponse};
12+
use crate::execution::cases::get_news::GetNewsCaseInput;
13+
14+
#[derive(Deserialize)]
15+
pub(in super::super) struct GetNewsRequest {
16+
date: String,
17+
}
18+
19+
pub(in super::super) async fn get_news(
20+
State(state): State<RouterState>,
21+
Query(request): Query<GetNewsRequest>,
22+
) -> Result<([(HeaderName, &'static str); 2], Body), (StatusCode, Json<ErrorResponse>)> {
23+
let date = NaiveDate::parse_from_str(&request.date, "%Y-%m-%d")
24+
.map_err(|e| (StatusCode::BAD_REQUEST, Json(e.to_string().into())))?;
25+
let output = state
26+
.workshop
27+
.execute_get_news_case(GetNewsCaseInput { date })
28+
.await
29+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e.to_string().into())))?;
30+
let body = Body::from_stream(output.articles_file);
31+
let headers = [
32+
(header::CONTENT_TYPE, "text/csv; charset=utf-8"),
33+
(header::CONTENT_DISPOSITION, "attachment; filename=\"articles.csv\""),
34+
];
35+
Ok((headers, body))
36+
}

chloria-backend/chloria-api/src/interface/adapters/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub(super) mod auth;
2+
pub(super) mod get_news;
23

34
use serde::Serialize;
45
use serde_json::Value;

chloria-backend/chloria-api/src/interface/router.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ use jsonwebtoken::{DecodingKey, EncodingKey};
77
use tower::ServiceBuilder;
88

99
use super::{
10-
adapters::auth::{authenticate, authorize},
10+
adapters::{
11+
auth::{authenticate, authorize},
12+
get_news::get_news,
13+
},
1114
state::{RouterState, RouterStateJwt},
1215
};
1316
use crate::execution::workshop::Workshop;
@@ -31,9 +34,8 @@ pub(crate) fn new(config: RouterConfig, workshop: Workshop) -> Router {
3134
.with_state(state.clone());
3235
let authorized_router = Router::new()
3336
.route("/get-news", get(get_news))
34-
.route_layer(ServiceBuilder::new().layer(middleware::from_fn_with_state(state, authorize)));
37+
.route_layer(ServiceBuilder::new().layer(middleware::from_fn_with_state(state.clone(), authorize)))
38+
.with_state(state.clone());
3539
let router = Router::new().merge(public_router).merge(authorized_router);
3640
router
3741
}
38-
39-
async fn get_news() {}

0 commit comments

Comments
 (0)