diff --git a/packages/toy-blog-endpoint-model/src/lib.rs b/packages/toy-blog-endpoint-model/src/lib.rs index f46a238..adf4553 100644 --- a/packages/toy-blog-endpoint-model/src/lib.rs +++ b/packages/toy-blog-endpoint-model/src/lib.rs @@ -173,13 +173,20 @@ pub enum Visibility { } #[derive(Serialize, Clone, Eq, PartialEq, Debug)] -pub struct ArticleIdSet(pub HashSet); +pub struct ArticleListingResponseRepresentation(pub Vec); -pub struct ArticleIdSetMetadata { +pub struct ArticleListingResponseMetadata { pub oldest_created_at: Option>, pub newest_updated_at: Option>, } +#[derive(Serialize, Clone, Eq, PartialEq, Debug)] +pub struct ArticleListResponseEntry { + pub id: ArticleId, + pub created_at: DateTime, + pub updated_at: DateTime, +} + #[derive(Deserialize, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] pub struct AnnoDominiYear(NonZeroU32); diff --git a/packages/toy-blog/src/service/rest/api/list.rs b/packages/toy-blog/src/service/rest/api/list.rs index 5244cfa..d22a090 100644 --- a/packages/toy-blog/src/service/rest/api/list.rs +++ b/packages/toy-blog/src/service/rest/api/list.rs @@ -4,7 +4,7 @@ use actix_web::{get, Responder}; use actix_web::web::Path; use chrono::Datelike; -use toy_blog_endpoint_model::{AnnoDominiYear, Article, ArticleId, ArticleIdSet, ArticleIdSetMetadata, OneOriginTwoDigitsMonth, OwnedMetadata, Visibility}; +use toy_blog_endpoint_model::{AnnoDominiYear, Article, ArticleId, ArticleListingResponseRepresentation, ArticleListingResponseMetadata, ArticleListResponseEntry, OneOriginTwoDigitsMonth, OwnedMetadata, Visibility}; use crate::service::persistence::ArticleRepository; use crate::service::rest::exposed_representation_format::{ArticleIdCollectionResponseRepr, EndpointRepresentationCompiler, MaybeNotModified, ReportLastModofied}; @@ -32,8 +32,12 @@ fn compute_and_filter_out( ret_304 = false; } - let id = ArticleIdSet(x.iter().filter(only_public).filter(additional_filter.clone()) - .map(|x| &x.0).cloned().collect()); + let entries = ArticleListingResponseRepresentation(x.iter().filter(only_public).filter(additional_filter.clone()) + .map(|(id, a)| ArticleListResponseEntry { + id: id.clone(), + created_at: a.created_at, + updated_at: a.updated_at, + }).collect()); let old_cre = x.iter().filter(only_public).filter(additional_filter.clone()) .min_by_key(|x| x.1.created_at).map(|x| x.1.created_at); let new_upd = x.iter().filter(only_public).filter(additional_filter) @@ -43,11 +47,11 @@ fn compute_and_filter_out( MaybeNotModified { inner: ReportLastModofied { inner: OwnedMetadata { - metadata: ArticleIdSetMetadata { + metadata: ArticleListingResponseMetadata { oldest_created_at: old_cre, newest_updated_at: new_upd, }, - data: id + data: entries }, latest_updated: latest_updated.map(|x| x.try_into().unwrap()) }, @@ -124,7 +128,7 @@ mod tests { use crate::service::rest::api::list::{article_id_list0, article_id_list_by_year0, article_id_list_by_year_and_month0}; #[test] - fn do_not_leak() { + fn do_not_include_non_public_article() { tokio::runtime::Builder::new_current_thread() .enable_all() .build() @@ -134,24 +138,24 @@ mod tests { ArticleRepository::init(m.path()); let a = ArticleRepository::new(m.path()).await; { - let aa = ArticleId::new("123".to_string()); + let aa = ArticleId::new("12345".to_string()); a.create_entry(&aa, "12345".to_string(), Visibility::Private).unwrap(); let ac = article_id_list0(&a, None); - let m = ac.0.inner.inner.data.0.get(&aa); + let m = ac.0.inner.inner.data.0.iter().find(|x| x.id == aa); assert!(m.is_none()); } { - let aa = ArticleId::new("1234".to_string()); + let aa = ArticleId::new("123456".to_string()); a.create_entry(&aa, "123456".to_string(), Visibility::Restricted).unwrap(); let ac = article_id_list0(&a, None); - let m = ac.0.inner.inner.data.0.get(&aa); + let m = ac.0.inner.inner.data.0.iter().find(|x| x.id == aa); assert!(m.is_none()); } }); } #[test] - fn do_not_leak_by_year() { + fn do_not_include_non_public_article_by_year_filter() { tokio::runtime::Builder::new_current_thread() .enable_all() .build() @@ -161,24 +165,24 @@ mod tests { ArticleRepository::init(m.path()); let a = ArticleRepository::new(m.path()).await; { - let aa = ArticleId::new("123".to_string()); + let aa = ArticleId::new("12345".to_string()); a.create_entry(&aa, "12345".to_string(), Visibility::Private).unwrap(); let ac = article_id_list_by_year0(&a, AnnoDominiYear::try_from(Local::now().year() as u32).unwrap(), None); - let m = ac.0.inner.inner.data.0.get(&aa); + let m = ac.0.inner.inner.data.0.iter().find(|x| x.id == aa); assert!(m.is_none()); } { - let aa = ArticleId::new("1234".to_string()); + let aa = ArticleId::new("123456".to_string()); a.create_entry(&aa, "123456".to_string(), Visibility::Restricted).unwrap(); let ac = article_id_list_by_year0(&a, AnnoDominiYear::try_from(Local::now().year() as u32).unwrap(), None); - let m = ac.0.inner.inner.data.0.get(&aa); + let m = ac.0.inner.inner.data.0.iter().find(|x| x.id == aa); assert!(m.is_none()); } }); } #[test] - fn do_not_leak_by_year_and_month() { + fn do_not_include_non_public_article_by_year_and_month_filter() { tokio::runtime::Builder::new_current_thread() .enable_all() .build() @@ -188,7 +192,7 @@ mod tests { ArticleRepository::init(m.path()); let a = ArticleRepository::new(m.path()).await; { - let aa = ArticleId::new("123".to_string()); + let aa = ArticleId::new("12345".to_string()); a.create_entry(&aa, "12345".to_string(), Visibility::Private).unwrap(); let now = Local::now(); let ac = article_id_list_by_year_and_month0( @@ -197,11 +201,11 @@ mod tests { OneOriginTwoDigitsMonth::try_from(now.month() as u8).unwrap() ), None ); - let a = ac.0.inner.inner.data.0.get(&aa); + let a = ac.0.inner.inner.data.0.iter().find(|x| x.id == aa); assert!(a.is_none()); } { - let aa = ArticleId::new("1235".to_string()); + let aa = ArticleId::new("123456".to_string()); a.create_entry(&aa, "123456".to_string(), Visibility::Restricted).unwrap(); let now = Local::now(); let ac = article_id_list_by_year_and_month0( @@ -210,9 +214,9 @@ mod tests { OneOriginTwoDigitsMonth::try_from(now.month() as u8).unwrap() ), None ); - let a = ac.0.inner.inner.data.0.get(&aa); + let a = ac.0.inner.inner.data.0.iter().find(|x| x.id == aa); assert!(a.is_none()); } }); } -} \ No newline at end of file +} diff --git a/packages/toy-blog/src/service/rest/exposed_representation_format.rs b/packages/toy-blog/src/service/rest/exposed_representation_format.rs index 19b30ea..19daa8e 100644 --- a/packages/toy-blog/src/service/rest/exposed_representation_format.rs +++ b/packages/toy-blog/src/service/rest/exposed_representation_format.rs @@ -7,7 +7,7 @@ use actix_web::HttpResponse; use chrono::{FixedOffset, Utc}; use serde::{Serialize, Serializer}; -use toy_blog_endpoint_model::{ArticleCreatedNotice, ArticleIdSet, ArticleIdSetMetadata, ChangeArticleIdError, ChangeArticleIdRequestResult, CreateArticleError, CreateArticleResult, DeleteArticleError, DeleteArticleResult, GetArticleError, GetArticleResult, ListArticleResponse, ListArticleResult, OwnedMetadata, UpdateArticleError, UpdateArticleResult}; +use toy_blog_endpoint_model::{ArticleCreatedNotice, ArticleListingResponseRepresentation, ArticleListingResponseMetadata, ChangeArticleIdError, ChangeArticleIdRequestResult, CreateArticleError, CreateArticleResult, DeleteArticleError, DeleteArticleResult, GetArticleError, GetArticleResult, ListArticleResponse, ListArticleResult, OwnedMetadata, UpdateArticleError, UpdateArticleResult}; use crate::service::rest::header::HttpDate; use crate::service::rest::inner_no_leak::{ComposeInternalError, UnhandledError}; @@ -515,7 +515,9 @@ impl Serialize for ReportLastModofied { } -pub(super) struct ArticleIdCollectionResponseRepr(pub(super) MaybeNotModified>>); +pub(super) struct ArticleIdCollectionResponseRepr( + pub(super) MaybeNotModified>> +); impl HttpStatusCode for ArticleIdCollectionResponseRepr { fn call_status_code(&self) -> StatusCode { diff --git a/packages/toy-blog/src/service/rest/header.rs b/packages/toy-blog/src/service/rest/header.rs index 9bafcba..a84337f 100644 --- a/packages/toy-blog/src/service/rest/header.rs +++ b/packages/toy-blog/src/service/rest/header.rs @@ -67,20 +67,14 @@ impl FromRequest for LastModified { type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - let w = req.headers().get("Last-Modified") - .ok_or(HttpDateExtractionError::NotFound); - let w = match w { - Ok(t) => t, - Err(e) => return std::future::ready(Err(e)), + let inner = || { + let w = req.headers().get("Last-Modified") + .ok_or(HttpDateExtractionError::NotFound)?; + let r = Self::try_from(w)?; + Ok(r) }; - let r = Self::try_from(w); - let r = match r { - Ok(t) => t, - Err(e) => return std::future::ready(Err(HttpDateExtractionError::from(e))), - }; - - std::future::ready(Ok(r)) + std::future::ready(inner()) } }