Skip to content

Commit 2d7b29f

Browse files
committed
Add "ReadNewsCase".
1 parent 82ab80b commit 2d7b29f

File tree

9 files changed

+190
-11
lines changed

9 files changed

+190
-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"

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 read_news;
23

34
use anyhow::Result;
45
use async_trait::async_trait;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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::{
12+
ports::repository::{Repository, SelectNewsInput},
13+
workshop::Workshop,
14+
},
15+
Case,
16+
};
17+
18+
pub(crate) struct ReadNewsCaseInput {
19+
pub(crate) date: NaiveDate,
20+
}
21+
22+
pub(crate) struct ReadNewsCaseOutput {
23+
pub(crate) articles_stream: ReaderStream<Cursor<Vec<u8>>>,
24+
}
25+
26+
struct ReadNewsCase {
27+
repository: Arc<dyn Repository>,
28+
input: ReadNewsCaseInput,
29+
}
30+
31+
impl Workshop {
32+
pub(crate) async fn execute_read_news_case(&self, input: ReadNewsCaseInput) -> Result<ReadNewsCaseOutput> {
33+
let case = ReadNewsCase {
34+
repository: Arc::clone(&self.repository),
35+
input,
36+
};
37+
self.run_case(case).await
38+
}
39+
}
40+
41+
#[async_trait]
42+
impl Case for ReadNewsCase {
43+
type Output = ReadNewsCaseOutput;
44+
45+
async fn execute(self) -> Result<Self::Output> {
46+
let mut writer = Writer::from_writer(vec![]);
47+
for select_news_output in self
48+
.repository
49+
.select_news(SelectNewsInput { date: self.input.date })
50+
.await?
51+
{
52+
if let Err(error) = writer.serialize(select_news_output) {
53+
error!("error={}", error);
54+
}
55+
}
56+
let output = ReadNewsCaseOutput {
57+
articles_stream: ReaderStream::new(Cursor::new(writer.into_inner()?)),
58+
};
59+
Ok(output)
60+
}
61+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
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) source_name: String,
14+
pub(crate) article_id: String,
15+
pub(crate) title: Option<String>,
16+
pub(crate) text: Option<String>,
17+
pub(crate) image_path: Option<String>,
18+
}
419

520
#[async_trait]
621
#[automock] // See: https://github.com/asomers/mockall/issues/189#issuecomment-689145249
722
pub(crate) trait Repository: Send + Sync {
823
async fn select_client_api_secret(&self, api_key_input: &str) -> Result<Option<String>>;
24+
async fn select_news(&self, input: SelectNewsInput) -> Result<Vec<SelectNewsOutput>>;
925
}

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

Lines changed: 28 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,24 @@ 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 (source_name_value, article_id_value, title_value, long_text_value, image_path_value) in news
47+
.filter(news::created_at.ge(date).and(news::created_at.lt(next_date)))
48+
.select((source_name, article_id, title, long_text, image_path))
49+
.get_results(&mut self.pool.get()?)?
50+
{
51+
outputs.push(SelectNewsOutput {
52+
source_name: source_name_value,
53+
article_id: article_id_value,
54+
title: title_value,
55+
text: long_text_value,
56+
image_path: image_path_value,
57+
});
58+
}
59+
Ok(outputs)
60+
}
3461
}

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 read_news;
23

34
use serde::Serialize;
45
use serde_json::Value;
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::read_news::ReadNewsCaseInput;
13+
14+
#[derive(Deserialize)]
15+
pub(in super::super) struct ReadNewsRequest {
16+
date: String,
17+
}
18+
19+
pub(in super::super) async fn read_news(
20+
State(state): State<RouterState>,
21+
Query(request): Query<ReadNewsRequest>,
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_read_news_case(ReadNewsCaseInput { date })
28+
.await
29+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e.to_string().into())))?;
30+
let body = Body::from_stream(output.articles_stream);
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/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+
read_news::read_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("/news", get(read_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 read_news() {}

0 commit comments

Comments
 (0)