Skip to content

Commit

Permalink
feat(spansy): HTTP body content + parsing (#21)
Browse files Browse the repository at this point in the history
* feat(spansy): add AsRef<Span<[u8]>> impl for Span<str>

* feat(spansy): HTTP body content + parsing

* mark BodyContent enum non_exhaustive

* Apply suggestions from code review

Co-authored-by: dan <[email protected]>

* update Body doc comment

---------

Co-authored-by: dan <[email protected]>
  • Loading branch information
sinui0 and themighty1 authored Feb 8, 2024
1 parent b1a3b01 commit 554b7be
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 15 deletions.
4 changes: 2 additions & 2 deletions spansy/src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use bytes::Bytes;

pub use span::{parse_request, parse_response};
pub use types::{
Body, Code, Header, HeaderName, HeaderValue, Method, Reason, Request, RequestLine, Response,
Status, Target,
Body, BodyContent, Code, Header, HeaderName, HeaderValue, Method, Reason, Request, RequestLine,
Response, Status, Target,
};

use crate::ParseError;
Expand Down
86 changes: 75 additions & 11 deletions spansy/src/http/span.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::ops::Range;

use bytes::Bytes;

use crate::{
helpers::get_span_range,
http::{
Body, Code, Header, HeaderName, HeaderValue, Method, Reason, Request, RequestLine,
Response, Status, Target,
Body, BodyContent, Code, Header, HeaderName, HeaderValue, Method, Reason, Request,
RequestLine, Response, Status, Target,
},
ParseError, Span,
json, ParseError, Span,
};

const MAX_HEADERS: usize = 128;
Expand Down Expand Up @@ -87,11 +89,14 @@ pub(crate) fn parse_request_from_bytes(src: &Bytes, offset: usize) -> Result<Req
)));
}

request.span = Span::new_bytes(src.clone(), offset..range.end);
let content_type = request
.headers_with_name("Content-Type")
.next()
.map(|header| header.value.as_bytes())
.unwrap_or_default();

request.body = Some(Body {
span: Span::new_bytes(src.clone(), range),
});
request.body = Some(parse_body(src, range.clone(), content_type)?);
request.span = Span::new_bytes(src.clone(), offset..range.end);
}

Ok(request)
Expand Down Expand Up @@ -175,11 +180,14 @@ pub(crate) fn parse_response_from_bytes(
)));
}

response.span = Span::new_bytes(src.clone(), offset..range.end);
let content_type = response
.headers_with_name("Content-Type")
.next()
.map(|header| header.value.as_bytes())
.unwrap_or_default();

response.body = Some(Body {
span: Span::new_bytes(src.clone(), range),
});
response.body = Some(parse_body(src, range.clone(), content_type)?);
response.span = Span::new_bytes(src.clone(), offset..range.end);
}

Ok(response)
Expand Down Expand Up @@ -273,6 +281,27 @@ fn response_body_len(response: &Response) -> Result<usize, ParseError> {
}
}

/// Parses a request or response message body.
///
/// # Arguments
///
/// * `src` - The source bytes.
/// * `range` - The range of the message body in the source bytes.
/// * `content_type` - The value of the Content-Type header.
fn parse_body(src: &Bytes, range: Range<usize>, content_type: &[u8]) -> Result<Body, ParseError> {
let span = Span::new_bytes(src.clone(), range.clone());
let content = if content_type.get(..16) == Some(b"application/json".as_slice()) {
let mut value = json::parse(span.data.clone())?;
value.offset(range.start);

BodyContent::Json(value)
} else {
BodyContent::Unknown(span.clone())
};

Ok(Body { span, content })
}

#[cfg(test)]
mod tests {
use crate::Spanned;
Expand Down Expand Up @@ -321,6 +350,19 @@ mod tests {
Connection: keep-alive\r\n\r\n\
pong";

const TEST_REQUEST_JSON: &[u8] = b"\
POST / HTTP/1.1\r\n\
Host: localhost\r\n\
Content-Type: application/json\r\n\
Content-Length: 14\r\n\r\n\
{\"foo\": \"bar\"}";

const TEST_RESPONSE_JSON: &[u8] = b"\
HTTP/1.1 200 OK\r\n\
Content-Type: application/json\r\n\
Content-Length: 14\r\n\r\n\
{\"foo\": \"bar\"}";

#[test]
fn test_parse_request() {
let req = parse_request(TEST_REQUEST).unwrap();
Expand Down Expand Up @@ -432,4 +474,26 @@ mod tests {
b"<html>\n<body>\n<h1>Hello, World!</h1>\n</body>\n</html>".as_slice()
);
}

#[test]
fn test_parse_request_json() {
let req = parse_request(TEST_REQUEST_JSON).unwrap();

let BodyContent::Json(value) = req.body.unwrap().content else {
panic!("body is not json");
};

assert_eq!(value.span(), "{\"foo\": \"bar\"}");
}

#[test]
fn test_parse_response_json() {
let res = parse_response(TEST_RESPONSE_JSON).unwrap();

let BodyContent::Json(value) = res.body.unwrap().content else {
panic!("body is not json");
};

assert_eq!(value.span(), "{\"foo\": \"bar\"}");
}
}
27 changes: 25 additions & 2 deletions spansy/src/http/types.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use utils::range::{RangeDifference, RangeSet};

use crate::{Span, Spanned};
use crate::{json::JsonValue, Span, Spanned};

/// An HTTP header name.
#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -342,11 +342,14 @@ impl Spanned for Response {
}
}

/// An HTTP request or response body.
/// An HTTP request or response payload body.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Body {
pub(crate) span: Span,

/// The body content.
pub content: BodyContent,
}

impl Body {
Expand All @@ -366,3 +369,23 @@ impl Spanned for Body {
&self.span
}
}

/// An HTTP request or response payload body content.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum BodyContent {
/// Body with an `application/json` content type.
Json(JsonValue),
/// Body with an unknown content type.
Unknown(Span),
}

impl Spanned for BodyContent {
fn span(&self) -> &Span {
match self {
BodyContent::Json(json) => json.span().as_ref(),
BodyContent::Unknown(span) => span,
}
}
}

0 comments on commit 554b7be

Please sign in to comment.