From 554b7bedb599362ef85f68b4d147216d490d935a Mon Sep 17 00:00:00 2001 From: "sinu.eth" <65924192+sinui0@users.noreply.github.com> Date: Thu, 8 Feb 2024 11:40:07 -0800 Subject: [PATCH] feat(spansy): HTTP body content + parsing (#21) * feat(spansy): add AsRef> impl for Span * feat(spansy): HTTP body content + parsing * mark BodyContent enum non_exhaustive * Apply suggestions from code review Co-authored-by: dan * update Body doc comment --------- Co-authored-by: dan --- spansy/src/http/mod.rs | 4 +- spansy/src/http/span.rs | 86 +++++++++++++++++++++++++++++++++++----- spansy/src/http/types.rs | 27 ++++++++++++- 3 files changed, 102 insertions(+), 15 deletions(-) diff --git a/spansy/src/http/mod.rs b/spansy/src/http/mod.rs index 9080a20..9723712 100644 --- a/spansy/src/http/mod.rs +++ b/spansy/src/http/mod.rs @@ -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; diff --git a/spansy/src/http/span.rs b/spansy/src/http/span.rs index 9bf7ef7..a93b40e 100644 --- a/spansy/src/http/span.rs +++ b/spansy/src/http/span.rs @@ -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; @@ -87,11 +89,14 @@ pub(crate) fn parse_request_from_bytes(src: &Bytes, offset: usize) -> Result Result { } } +/// 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, content_type: &[u8]) -> Result { + 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; @@ -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(); @@ -432,4 +474,26 @@ mod tests { b"\n\n

Hello, World!

\n\n".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\"}"); + } } diff --git a/spansy/src/http/types.rs b/spansy/src/http/types.rs index 4b3ddcb..512ba0c 100644 --- a/spansy/src/http/types.rs +++ b/spansy/src/http/types.rs @@ -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)] @@ -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 { @@ -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, + } + } +}