From 1fdd82ebdcb406fd617366d3813193d83fe324a2 Mon Sep 17 00:00:00 2001 From: H1rono Date: Fri, 6 Dec 2024 07:19:41 +0900 Subject: [PATCH 1/6] :wrench: Add optional-deps in `http` feature --- Cargo.lock | 1 + Cargo.toml | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb87cf0..1f3bb39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1637,6 +1637,7 @@ dependencies = [ name = "traq-bot-http" version = "0.10.1" dependencies = [ + "bytes", "chrono", "futures", "http 1.1.0", diff --git a/Cargo.toml b/Cargo.toml index 2303151..5fbce34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,11 @@ version = "0.4" optional = true features = ["serde"] +[dependencies.bytes] +version = "1" +features = [] +optional = true + [dependencies.http] version = "1" features = [] @@ -85,5 +90,5 @@ futures = { version = "0.3", features = ["executor"] } uuid = ["dep:uuid"] time = ["dep:time"] chrono = ["dep:chrono"] -http = ["dep:http", "dep:http-body", "dep:http-body-util"] -tower = ["http", "dep:tower", "dep:tower-http", "dep:futures", "dep:pin-project-lite"] +http = ["dep:bytes", "dep:http", "dep:http-body", "dep:http-body-util", "dep:pin-project-lite", "dep:futures"] +tower = ["http", "dep:tower", "dep:tower-http"] From 3af02f3d554f09da11542ad2cacd7905cf190dea Mon Sep 17 00:00:00 2001 From: H1rono Date: Fri, 6 Dec 2024 07:31:53 +0900 Subject: [PATCH 2/6] :recycle: Fix `parse_body` --- src/parser.rs | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 58ba0f8..07bc4b7 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,21 +2,25 @@ use std::str::from_utf8; -use serde::Deserialize; - use crate::error::{Error, ErrorKind, Result}; use crate::macros::all_events; use crate::{Event, EventKind, RequestParser}; /// ボディをDeserializeして`Event`に渡す -fn parse_body<'a, T, F>(f: F, body: &'a str) -> Result -where - T: Deserialize<'a>, - F: Fn(T) -> Event, -{ - serde_json::from_str(body) - .map(f) - .map_err(Error::parse_body_failed) +pub(crate) fn parse_body(kind: EventKind, body: &str) -> Result { + macro_rules! match_kind_parse_body { + ($( $k:ident ),*) => { + match kind { + $( + EventKind::$k => { + ::serde_json::from_str(body).map(Event::$k) + }, + )* + } + }; + } + + all_events!(match_kind_parse_body).map_err(Error::parse_body_failed) } // https://datatracker.ietf.org/doc/html/rfc9110#section-5.5 @@ -186,18 +190,7 @@ impl RequestParser { { let kind = self.parse_headers(headers)?; let body = from_utf8(body).map_err(Error::read_body_failed)?; - - macro_rules! match_kind_parse_body { - ($( $k:ident ),*) => { - match kind { - $( - EventKind::$k => parse_body(Event::$k, body), - )* - } - }; - } - - all_events!(match_kind_parse_body) + parse_body(kind, body) } } From d06eb994ce4708237e97bfeaa011dcc2eb55109a Mon Sep 17 00:00:00 2001 From: H1rono Date: Fri, 6 Dec 2024 07:33:17 +0900 Subject: [PATCH 3/6] :boom: Add `ParseRequest`, change `parse_request` signature --- src/parser.rs | 68 ++--------------- src/parser/http.rs | 182 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 60 deletions(-) create mode 100644 src/parser/http.rs diff --git a/src/parser.rs b/src/parser.rs index 07bc4b7..9744dcd 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -6,6 +6,12 @@ use crate::error::{Error, ErrorKind, Result}; use crate::macros::all_events; use crate::{Event, EventKind, RequestParser}; +#[cfg(feature = "http")] +mod http; + +#[cfg(feature = "http")] +pub use self::http::ParseRequest; + /// ボディをDeserializeして`Event`に渡す pub(crate) fn parse_body(kind: EventKind, body: &str) -> Result { macro_rules! match_kind_parse_body { @@ -194,71 +200,13 @@ impl RequestParser { } } -#[cfg(feature = "http")] -impl RequestParser { - /// [`http::Request`]をパースします。 - /// - /// **Note**: この関数は`http`featureが有効になっている時のみ有効です。 - /// - /// ## Arguments - /// * `request`: リクエスト全体 - /// - /// ## Example - /// ``` - /// # fn main() -> Result<(), Box> { - /// # let res: Result<(), Box> = futures::executor::block_on(async { - /// use traq_bot_http::{EventKind, RequestParser}; - /// - /// let verification_token = "verification_token"; - /// let body = r#"{"eventTime": "2019-05-07T04:50:48.582586882Z"}"#.to_string(); - /// let request = http::Request::builder() - /// .method(http::Method::POST) - /// .header(http::header::CONTENT_TYPE, "application/json") - /// .header("X-TRAQ-BOT-TOKEN", verification_token) - /// .header("X-TRAQ-BOT-EVENT", "PING") - /// .body(body)?; - /// let parser = RequestParser::new(verification_token); - /// let event = parser.parse_request(request).await?; - /// assert_eq!(event.kind(), EventKind::Ping); - /// # Ok(()) - /// # }); - /// # res - /// # } - /// ``` - /// - /// ## Errors - /// [`Error`]のうち、[`Error::kind`]が以下のものを返す可能性があります。 - /// - /// - [`parse`]で返されるもの - /// - [`ErrorKind::ReadBodyFailed`] : - /// リクエストボディの読み込みに失敗した - /// - /// [`Error::kind`]: crate::Error::kind - /// [`parse`]: RequestParser::parse - pub async fn parse_request(&self, request: http::Request) -> Result - where - B: http_body::Body, - B::Error: Into>, - { - use http_body_util::BodyExt; - - let (parts, body) = request.into_parts(); - let body = body - .collect() - .await - .map_err(Error::read_body_failed)? - .to_bytes(); - self.parse(&parts.headers, &body) - } -} - #[cfg(test)] mod tests { use super::*; use crate::macros::test_parse_payload; - use http::header::HeaderMap; - use http::header::CONTENT_TYPE; + use ::http::header::HeaderMap; + use ::http::header::CONTENT_TYPE; #[test] fn request_parser_new() { diff --git a/src/parser/http.rs b/src/parser/http.rs new file mode 100644 index 0000000..d80b054 --- /dev/null +++ b/src/parser/http.rs @@ -0,0 +1,182 @@ +// #![cfg(feature = "http")] + +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use bytes::Bytes; +use futures::future::Ready; +use futures::ready; +use http_body::Body; +use http_body_util::{combinators::Collect, Collected}; + +use crate::error::{Error, Result}; +use crate::events::{Event, EventKind}; +use crate::parser::RequestParser; + +pin_project_lite::pin_project! { + #[must_use] + #[project = CollectBodyProject] + struct CollectBody + where + B: Body, + B: ?Sized, + { + #[pin] + collect: Collect, + } +} + +impl Future for CollectBody +where + B: Body + ?Sized, + B::Error: Into>, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let s = self.project(); + let collected = ready!(s.collect.poll(cx)); + let res = collected + .map(Collected::to_bytes) + .map_err(Error::read_body_failed); + Poll::Ready(res) + } +} + +pin_project_lite::pin_project! { + #[must_use] + #[project = ParseRequestInnerProject] + struct ParseRequestInner { + #[pin] + kind: K, + #[pin] + body: B, + } +} + +impl Future for ParseRequestInner +where + K: Future>, + B: Future>, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let s = self.project(); + let kind = match ready!(s.kind.poll(cx)) { + Ok(k) => k, + Err(e) => return Poll::Ready(Err(e)), + }; + let body = ready!(s.body.poll(cx)); + let res: Result = { + let body = body?; + let body = std::str::from_utf8(&body).map_err(Error::read_body_failed)?; + super::parse_body(kind, body) + }; + Poll::Ready(res) + } +} + +pin_project_lite::pin_project! { + /// impl [Future]> + /// + /// [Future]: std::future::Future + /// [Event]: crate::Event + /// [Error]: crate::Error + #[must_use] + #[project = ParseRequestProject] + pub struct ParseRequest + where + B: Body, + { + #[pin] + inner: ParseRequestInner>, CollectBody> + } +} + +impl ParseRequest +where + B: Body, +{ + fn new(kind: Result, body: B) -> Self { + use http_body_util::BodyExt; + + let kind = futures::future::ready(kind); + let inner = ParseRequestInner { + kind, + body: CollectBody { + collect: body.collect(), + }, + }; + Self { inner } + } +} + +impl Future for ParseRequest +where + B: Body, + B::Error: Into>, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let s = self.project(); + s.inner.poll(cx) + } +} + +impl RequestParser { + /// [`http::Request`]をパースします。 + /// + /// **Note**: この関数は`http`featureが有効になっている時のみ有効です。 + /// + /// # Arguments + /// + /// * `request`: リクエスト全体 + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), Box> { + /// # let res: Result<(), Box> = futures::executor::block_on(async { + /// use traq_bot_http::{EventKind, RequestParser}; + /// + /// let verification_token = "verification_token"; + /// let body = r#"{"eventTime": "2019-05-07T04:50:48.582586882Z"}"#.to_string(); + /// let request = http::Request::builder() + /// .method(http::Method::POST) + /// .header(http::header::CONTENT_TYPE, "application/json") + /// .header("X-TRAQ-BOT-TOKEN", verification_token) + /// .header("X-TRAQ-BOT-EVENT", "PING") + /// .body(body)?; + /// let parser = RequestParser::new(verification_token); + /// let event = parser.parse_request(request).await?; + /// assert_eq!(event.kind(), EventKind::Ping); + /// # Ok(()) + /// # }); + /// # res + /// # } + /// ``` + /// + /// # Errors + /// + /// [`Error`]のうち、[`Error::kind`]が以下のものを返す可能性があります。 + /// + /// - [`parse`]で返されるもの + /// - [`ErrorKind::ReadBodyFailed`] : + /// リクエストボディの読み込みに失敗した + /// + /// [`Error::kind`]: crate::Error::kind + /// [`parse`]: crate::RequestParser::parse + /// [`ErrorKind::ReadBodyFailed`]: crate::ErrorKind::ReadBodyFailed + pub fn parse_request(&self, request: http::Request) -> ParseRequest + where + B: Body, + B::Error: Into>, + { + let (parts, body) = request.into_parts(); + let kind = self.parse_headers(&parts.headers); + ParseRequest::new(kind, body) + } +} From b9ac6bc91495b213b4dba132749ee690eb99ade3 Mon Sep 17 00:00:00 2001 From: H1rono Date: Fri, 6 Dec 2024 07:34:09 +0900 Subject: [PATCH 4/6] :sparkles: Make mod `parser` into `pub` --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 8fb1975..97df66e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ mod error; mod events; pub(crate) mod macros; -mod parser; +pub mod parser; pub mod payloads; #[cfg(feature = "tower")] From 882d16843fb8d54b50f74276a9356262857a9210 Mon Sep 17 00:00:00 2001 From: H1rono Date: Fri, 6 Dec 2024 07:41:10 +0900 Subject: [PATCH 5/6] :adhesive_bandage: Edit cSpell dict --- .cspell-dict/lib-words.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.cspell-dict/lib-words.txt b/.cspell-dict/lib-words.txt index 0c9d972..dda56a5 100644 --- a/.cspell-dict/lib-words.txt +++ b/.cspell-dict/lib-words.txt @@ -5,6 +5,7 @@ TRAQ chrono eprintln serde +combinators HTAB VCHAR From 57af6dd40d104e45b7a7ad6859a01882944c0413 Mon Sep 17 00:00:00 2001 From: H1rono Date: Fri, 6 Dec 2024 08:06:18 +0900 Subject: [PATCH 6/6] :white_check_mark: Add tests --- src/parser/http.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/parser/http.rs b/src/parser/http.rs index d80b054..7ff3499 100644 --- a/src/parser/http.rs +++ b/src/parser/http.rs @@ -180,3 +180,32 @@ impl RequestParser { ParseRequest::new(kind, body) } } + +#[cfg(test)] +mod tests { + use futures::executor::block_on; + use http_body_util::BodyExt; + + use super::{CollectBody, ParseRequest}; + use crate::{Event, EventKind}; + + #[test] + fn collect_body() { + let body_content = "some content"; + let fut = CollectBody { + collect: body_content.to_string().collect(), + }; + let collected = block_on(fut).unwrap(); + assert_eq!(collected, body_content.as_bytes()); + } + + #[test] + fn parse_request_future() { + let kind = EventKind::Ping; + let payload = r#"{"eventTime": "2019-05-07T04:50:48.582586882Z"}"#; + let body = payload.to_string(); + let fut = ParseRequest::new(Ok(kind), body); + let event = block_on(fut).unwrap(); + assert!(matches!(event, Event::Ping(_))); + } +}