Skip to content

Commit

Permalink
Merge pull request #239 from H1rono/parse-request-future
Browse files Browse the repository at this point in the history
  • Loading branch information
H1rono authored Dec 6, 2024
2 parents 6a5d602 + 57af6dd commit b83191d
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 85 deletions.
1 change: 1 addition & 0 deletions .cspell-dict/lib-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ TRAQ
chrono
eprintln
serde
combinators

HTAB
VCHAR
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ version = "0.4"
optional = true
features = ["serde"]

[dependencies.bytes]
version = "1"
features = []
optional = true

[dependencies.http]
version = "1"
features = []
Expand Down Expand Up @@ -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"]
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
mod error;
mod events;
pub(crate) mod macros;
mod parser;
pub mod parser;
pub mod payloads;

#[cfg(feature = "tower")]
Expand Down
105 changes: 23 additions & 82 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,31 @@
use std::str::from_utf8;

use serde::Deserialize;

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`に渡す
fn parse_body<'a, T, F>(f: F, body: &'a str) -> Result<Event>
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<Event> {
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
Expand Down Expand Up @@ -186,76 +196,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)
}
}

#[cfg(feature = "http")]
impl RequestParser {
/// [`http::Request`]をパースします。
///
/// **Note**: この関数は`http`featureが有効になっている時のみ有効です。
///
/// ## Arguments
/// * `request`: リクエスト全体
///
/// ## Example
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # let res: Result<(), Box<dyn std::error::Error>> = 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<B>(&self, request: http::Request<B>) -> Result<Event>
where
B: http_body::Body,
B::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
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)
parse_body(kind, body)
}
}

Expand All @@ -264,8 +205,8 @@ 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() {
Expand Down
211 changes: 211 additions & 0 deletions src/parser/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// #![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<B>
where
B: Body,
B: ?Sized,
{
#[pin]
collect: Collect<B>,
}
}

impl<B> Future for CollectBody<B>
where
B: Body + ?Sized,
B::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
type Output = Result<Bytes>;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
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<K, B> {
#[pin]
kind: K,
#[pin]
body: B,
}
}

impl<K, B> Future for ParseRequestInner<K, B>
where
K: Future<Output = Result<EventKind>>,
B: Future<Output = Result<Bytes>>,
{
type Output = Result<Event>;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
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<Event> = {
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! {
/// <code>impl [Future]<Output = Result<[Event], [Error]>></code>
///
/// [Future]: std::future::Future
/// [Event]: crate::Event
/// [Error]: crate::Error
#[must_use]
#[project = ParseRequestProject]
pub struct ParseRequest<B>
where
B: Body,
{
#[pin]
inner: ParseRequestInner<Ready<Result<EventKind>>, CollectBody<B>>
}
}

impl<B> ParseRequest<B>
where
B: Body,
{
fn new(kind: Result<EventKind>, 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<B> Future for ParseRequest<B>
where
B: Body,
B::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
type Output = Result<Event>;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let s = self.project();
s.inner.poll(cx)
}
}

impl RequestParser {
/// [`http::Request`]をパースします。
///
/// **Note**: この関数は`http`featureが有効になっている時のみ有効です。
///
/// # Arguments
///
/// * `request`: リクエスト全体
///
/// # Example
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # let res: Result<(), Box<dyn std::error::Error>> = 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<B>(&self, request: http::Request<B>) -> ParseRequest<B>
where
B: Body,
B::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
let (parts, body) = request.into_parts();
let kind = self.parse_headers(&parts.headers);
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(_)));
}
}

0 comments on commit b83191d

Please sign in to comment.