articles_rs/articles/
service.rs1use chrono::{DateTime, Utc};
7use sqlx::postgres::PgPool;
8use std::collections::HashMap;
9use std::error;
10use std::io::ErrorKind;
11
12use uuid::Uuid;
13
14use crate::{
15 article_repository::{ArticleRepository, DbArticle},
16 config::DbConfig,
17 images_repository::{DbArticleImage, ImageRepository},
18 postgres_repository::PostgresRepository,
19};
20
21use super::{Article, ArticleKind};
22
23pub struct ArticleService {
28 article_repo: ArticleRepository,
29 image_repo: ImageRepository,
30}
31
32impl std::fmt::Debug for ArticleService {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.debug_struct("ArticleService")
35 .field("article_repo", &"ArticleRepository")
36 .field("image_repo", &"ImageRepository")
37 .finish()
38 }
39}
40
41impl ArticleService {
42 pub async fn new(config: DbConfig) -> Self {
50 let url = &config.get_connection_url();
51 let pool = PgPool::connect(url)
52 .await
53 .expect("unable to connect to the database");
54 let article_repo = ArticleRepository::new(pool.clone());
55 let image_repo = ImageRepository::new(pool);
56
57 ArticleService {
58 article_repo,
59 image_repo,
60 }
61 }
62
63 async fn get_article_with_images(
71 &self,
72 article: DbArticle,
73 ) -> Result<Article, Box<dyn error::Error>> {
74 let mut filter = HashMap::new();
75 filter.insert(
76 "article_id".to_string(),
77 article.id.expect("could not get id").to_string(),
78 );
79 let images = self.image_repo.list(Some(filter)).await?;
80 let image_paths: Vec<String> = images.into_iter().map(|img| img.image_path).collect();
81
82 let mut found = Article::from(article);
83 found.add_images(image_paths);
84 Ok(found)
85 }
86
87 pub async fn get_article_by_slug(&self, slug: &str) -> Result<Article, Box<dyn error::Error>> {
95 match self.article_repo.find_by_name(slug.to_string()).await {
96 Ok(Some(article)) => self.get_article_with_images(article).await,
97 Ok(None) => Err(Box::new(std::io::Error::new(
98 ErrorKind::NotFound,
99 "Article not found",
100 ))),
101 Err(e) => Err(Box::new(e)),
102 }
103 }
104
105 pub async fn get_article_by_id(&self, id: Uuid) -> Result<Article, Box<dyn error::Error>> {
113 match self.article_repo.find_by_id(id).await {
114 Ok(Some(article)) => self.get_article_with_images(article).await,
115 Ok(None) => Err(Box::new(std::io::Error::new(
116 ErrorKind::NotFound,
117 "Article not found",
118 ))),
119 Err(e) => Err(Box::new(e)),
120 }
121 }
122
123 pub async fn delete_article(&self, id: Uuid) -> Result<(), Box<dyn error::Error>> {
131 let mut filter = HashMap::new();
132 filter.insert("article_id".to_string(), id.to_string());
133 self.image_repo.delete_all(Some(filter)).await?;
134
135 match self.article_repo.delete(id).await {
136 Ok(_) => Ok(()),
137 Err(e) => Err(Box::new(e)),
138 }
139 }
140
141 pub async fn list_articles(
149 &self,
150 filter: Option<HashMap<String, String>>,
151 ) -> Result<Vec<Article>, Box<dyn error::Error>> {
152 match self.article_repo.list(filter).await {
153 Ok(v) => {
154 let articles = v.into_iter().map(Article::from).collect();
155 Ok(articles)
156 }
157 Err(e) => Err(Box::new(e)),
158 }
159 }
160
161 pub async fn create_article(
173 &self,
174 title: &str,
175 slug: &str,
176 description: &str,
177 author: &str,
178 kind: Option<ArticleKind>,
179 ) -> Result<Uuid, Box<dyn error::Error>> {
180 let authors = if author.contains('[') && author.contains(']') {
181 author
182 .trim_matches(|c| c == '[' || c == ']')
183 .split(',')
184 .map(|s| {
185 s.trim_matches('\'')
186 .trim()
187 .trim_matches('(')
188 .trim_matches(')')
189 })
190 .collect::<Vec<&str>>()
191 } else {
192 author
193 .split(',')
194 .map(|s| s.trim().trim_matches('(').trim_matches(')'))
195 .collect::<Vec<&str>>()
196 };
197
198 let mut article = Article::new(title, slug, description, authors);
199 if let Some(k) = kind {
200 article.kind = k;
201 }
202 match self.article_repo.create(&DbArticle::from(article)).await {
203 Ok(id) => Ok(id),
204 Err(e) => Err(Box::new(e)),
205 }
206 }
207
208 pub async fn update_article(
227 &self,
228 id: Uuid,
229 title: &str,
230 slug: &str,
231 description: &str,
232 author: Vec<String>,
233 status: Option<&str>,
234 created: Option<DateTime<Utc>>,
235 contents: &str,
236 images: Vec<String>,
237 source: &str,
238 publish: Option<DateTime<Utc>>,
239 kind: Option<ArticleKind>,
240 ) -> Result<(), Box<dyn error::Error>> {
241 let mut article = self
242 .article_repo
243 .find_by_id(id)
244 .await?
245 .ok_or_else(|| std::io::Error::new(ErrorKind::NotFound, "Article not found"))?;
246
247 article.title = title.to_string();
248 article.slug = slug.to_string();
249 article.description = description.to_string();
250 article.author = author.join(",");
251 article.content = contents.to_string();
252 article.source = source.to_string();
253
254 if let Some(s) = status {
255 article.status = s.to_string();
256 }
257 if let Some(d) = created {
258 article.created = d;
259 }
260 if let Some(p) = publish {
261 article.published = Some(p);
262 }
263 if let Some(k) = kind {
264 article.kind = match k {
265 ArticleKind::POST => String::from("POST"),
266 ArticleKind::LINK => String::from("LINK"),
267 };
268 }
269
270 self.article_repo.update(&article).await?;
271 let mut filter = HashMap::new();
272 filter.insert("article_id".to_string(), id.to_string());
273 let current_images = self.image_repo.list(Some(filter)).await?;
274 let current_paths: Vec<String> = current_images
275 .iter()
276 .map(|img| img.image_path.clone())
277 .collect();
278
279 for image_path in &images {
281 if !image_path.is_empty() && !current_paths.contains(image_path) {
282 let image = DbArticleImage {
283 id: None,
284 article_id: id,
285 image_path: image_path.clone(),
286 visible: true,
287 created_at: Utc::now(),
288 };
289 self.image_repo.create(&image).await?;
290 }
291 }
292
293 for current_image in current_images {
295 if !images.contains(¤t_image.image_path) {
296 self.image_repo.delete(current_image.id.unwrap()).await?;
297 }
298 }
299
300 Ok(())
301 }
302}