Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ where
S: Serializer + Default,
C: Send + 'static,
{
/// Creates a new `WireframeApp` instance with default configuration.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (review_instructions): Use imperative mood in doc comments for constructors (e.g., 'Create' instead of 'Creates').

The doc comment for WireframeApp::default uses 'Creates a new...' instead of 'Create a new...'. Please use the imperative mood.

Review instructions:

Path patterns: **/*

Instructions:
Use the imperative mood when writing pull request review feedback.

///
/// Initialises empty routes, services, middleware, and application data. Sets the
/// default frame processor and serializer, with no connection lifecycle hooks.
fn default() -> Self {
Self {
routes: HashMap::new(),
Expand Down
71 changes: 65 additions & 6 deletions src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,47 @@ pub struct LengthFormat {
}

impl LengthFormat {
/// Create a new [`LengthFormat`].
/// Creates a new `LengthFormat` with the specified number of bytes and
/// endianness for the length prefix.
///
/// # Parameters
/// - `bytes`: The number of bytes used for the length prefix.
/// - `endianness`: The byte order for encoding and decoding the length prefix.
///
/// # Returns
/// A `LengthFormat` configured with the given size and endianness.
#[must_use]
pub const fn new(bytes: usize, endianness: Endianness) -> Self { Self { bytes, endianness } }

/// Two byte big-endian prefix.
/// Creates a `LengthFormat` for a 2-byte big-endian length prefix.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (review_instructions): Function attribute #[must_use] is placed before the doc comment; it should be after the doc comment.

Please move the #[must_use] attribute below the doc comment for this function as required.

Review instructions:

Path patterns: **/*.rs

Instructions:
Ensure that function attributes are placed AFTER the function doc comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (review_instructions): Use imperative mood in doc comments for constructors (e.g., 'Create' instead of 'Creates').

The doc comment for u32_be uses 'Creates a...' instead of 'Create a...'. Please use the imperative mood.

Review instructions:

Path patterns: **/*

Instructions:
Use the imperative mood when writing pull request review feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (review_instructions): Function attribute #[must_use] is placed before the doc comment; it should be after the doc comment.

Please move the #[must_use] attribute below the doc comment for this function.

Review instructions:

Path patterns: **/*.rs

Instructions:
Ensure that function attributes are placed AFTER the function doc comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (review_instructions): Function attribute #[must_use] is placed before the doc comment; it should be after the doc comment.

Please move the #[must_use] attribute below the doc comment for this function as well.

Review instructions:

Path patterns: **/*.rs

Instructions:
Ensure that function attributes are placed AFTER the function doc comment

#[must_use]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (review_instructions): Function attribute #[must_use] is placed before the doc comment; it should be after the doc comment.

Please move the #[must_use] attribute below the doc comment for this function as per the style guide.

Review instructions:

Path patterns: **/*.rs

Instructions:
Ensure that function attributes are placed AFTER the function doc comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (review_instructions): Use imperative mood in doc comments for constructors (e.g., 'Create' instead of 'Creates').

The doc comment for u32_le uses 'Creates a...' instead of 'Create a...'. Please use the imperative mood.

Review instructions:

Path patterns: **/*

Instructions:
Use the imperative mood when writing pull request review feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (review_instructions): Function attribute #[must_use] is placed before the doc comment; it should be after the doc comment.

Please move the #[must_use] attribute below the doc comment for this function as well.

Review instructions:

Path patterns: **/*.rs

Instructions:
Ensure that function attributes are placed AFTER the function doc comment

pub const fn u16_be() -> Self { Self::new(2, Endianness::Big) }

/// Two byte little-endian prefix.
/// Creates a `LengthFormat` for a 2-byte little-endian length prefix.
#[must_use]
pub const fn u16_le() -> Self { Self::new(2, Endianness::Little) }

/// Four byte big-endian prefix.
/// Creates a `LengthFormat` for a 4-byte big-endian length prefix.
#[must_use]
pub const fn u32_be() -> Self { Self::new(4, Endianness::Big) }

/// Four byte little-endian prefix.
/// Creates a `LengthFormat` for a 4-byte little-endian length prefix.
#[must_use]
pub const fn u32_le() -> Self { Self::new(4, Endianness::Little) }

/// Reads a length prefix from a byte slice according to the configured prefix size and
/// endianness.
///
/// # Parameters
/// - `bytes`: The byte slice containing the length prefix. Must be at least as long as the
/// configured prefix size.
///
/// # Returns
/// The decoded length as a `usize` if successful.
///
/// # Errors
/// Returns an error if the prefix size is unsupported or if the decoded length does not fit in
/// a `usize`.
fn read_len(&self, bytes: &[u8]) -> io::Result<usize> {
let len = match (self.bytes, self.endianness) {
(1, _) => u64::from(u8::from_ne_bytes([bytes[0]])),
Expand Down Expand Up @@ -72,6 +93,18 @@ impl LengthFormat {
usize::try_from(len).map_err(|_| io::Error::other("frame too large"))
}

/// Writes a length prefix to the destination buffer using the configured size and endianness.
///
/// Returns an error if the length is too large to fit in the configured prefix size or if the
/// prefix size is unsupported.
///
/// # Parameters
/// - `len`: The length value to encode and write.
/// - `dst`: The buffer to which the encoded length prefix will be appended.
///
/// # Errors
/// Returns an error if `len` exceeds the maximum value for the configured prefix size or if the
/// prefix size is not supported.
fn write_len(&self, len: usize, dst: &mut BytesMut) -> io::Result<()> {
match (self.bytes, self.endianness) {
(1, _) => dst.put_u8(
Expand Down Expand Up @@ -120,6 +153,9 @@ impl LengthFormat {
}

impl Default for LengthFormat {
/// Returns a `LengthFormat` using a 4-byte big-endian length prefix.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (review_instructions): Use imperative mood in doc comments for default implementations (e.g., 'Return' instead of 'Returns').

The doc comment for default uses 'Returns a...' instead of 'Return a...'. Please use the imperative mood.

Review instructions:

Path patterns: **/*

Instructions:
Use the imperative mood when writing pull request review feedback.

///
/// This is the default format for length-prefixed framing.
fn default() -> Self { Self::u32_be() }
}

Expand Down Expand Up @@ -159,19 +195,38 @@ pub struct LengthPrefixedProcessor {
}

impl LengthPrefixedProcessor {
/// Construct a processor with the provided [`LengthFormat`].
/// Creates a new `LengthPrefixedProcessor` with the specified length prefix
/// format.
///
/// # Parameters
/// - `format`: The length prefix format to use for framing.
///
/// # Returns
/// A `LengthPrefixedProcessor` configured with the given length format.
#[must_use]
pub const fn new(format: LengthFormat) -> Self { Self { format } }
}

impl Default for LengthPrefixedProcessor {
/// Creates a `LengthPrefixedProcessor` using the default length format (4-byte big-endian
/// prefix).
///
/// # Returns
/// A processor configured for 4-byte big-endian length-prefixed framing.
fn default() -> Self { Self::new(LengthFormat::default()) }
}

impl FrameProcessor for LengthPrefixedProcessor {
type Frame = Vec<u8>;
type Error = std::io::Error;

/// Attempts to decode a single length-prefixed frame from the source buffer.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (review_instructions): Use imperative mood in doc comments for method actions (e.g., 'Attempt' instead of 'Attempts').

The doc comment for decode uses 'Attempts to decode...' instead of 'Attempt to decode...'. Please use the imperative mood.

Review instructions:

Path patterns: **/*

Instructions:
Use the imperative mood when writing pull request review feedback.

///
/// Returns `Ok(Some(frame))` if a complete frame is available, `Ok(None)` if
/// more data is needed, or an error if the length prefix is invalid or cannot
/// be read according to the configured format.
///
/// The source buffer is advanced past the decoded frame and its length prefix.
fn decode(&self, src: &mut BytesMut) -> Result<Option<Self::Frame>, Self::Error> {
if src.len() < self.format.bytes {
return Ok(None);
Expand All @@ -184,6 +239,10 @@ impl FrameProcessor for LengthPrefixedProcessor {
Ok(Some(src.split_to(len).to_vec()))
}

/// Encodes a frame by prefixing it with its length and appending it to the destination buffer.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (review_instructions): Use imperative mood in doc comments for method actions (e.g., 'Encode' instead of 'Encodes').

The doc comment for encode uses 'Encodes a frame...' instead of 'Encode a frame...'. Please use the imperative mood.

Review instructions:

Path patterns: **/*

Instructions:
Use the imperative mood when writing pull request review feedback.

///
/// The length prefix format is determined by the processor's configuration. Returns an error
/// if the frame length cannot be represented in the configured format.
fn encode(&self, frame: &Self::Frame, dst: &mut BytesMut) -> Result<(), Self::Error> {
dst.reserve(self.format.bytes + frame.len());
self.format.write_len(frame.len(), dst)?;
Expand Down
25 changes: 25 additions & 0 deletions tests/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ impl<'de> bincode::BorrowDecode<'de, ()> for FailingResp {
}

#[tokio::test]
/// Tests that sending a response serialises and frames the data correctly,
/// and that the response can be decoded and deserialised back to its original value asynchronously.
async fn send_response_encodes_and_frames() {
let app = WireframeApp::new()
.unwrap()
Expand All @@ -52,6 +54,9 @@ async fn send_response_encodes_and_frames() {
}

#[tokio::test]
/// Tests that decoding with an incomplete length prefix header returns `None` and does not consume any bytes from the buffer.
///
/// This ensures that the decoder waits for the full header before attempting to decode a frame.
async fn length_prefixed_decode_requires_complete_header() {
let processor = LengthPrefixedProcessor::default();
let mut buf = BytesMut::from(&[0x00, 0x00, 0x00][..]); // only 3 bytes
Expand All @@ -60,6 +65,10 @@ async fn length_prefixed_decode_requires_complete_header() {
}

#[tokio::test]
/// Tests that decoding with a complete length prefix but incomplete frame data returns `None`
/// and retains all bytes in the buffer.
///
/// Ensures that the decoder does not consume any bytes when the full frame is not yet available.
async fn length_prefixed_decode_requires_full_frame() {
let processor = LengthPrefixedProcessor::default();
let mut buf = BytesMut::from(&[0x00, 0x00, 0x00, 0x05, 0x01, 0x02][..]);
Expand Down Expand Up @@ -95,6 +104,10 @@ impl tokio::io::AsyncWrite for FailingWriter {
}

#[tokio::test]
/// Tests that `send_response` correctly propagates I/O errors encountered during writing.
///
/// This test uses a writer that always fails on write operations and asserts that
/// the resulting error is of the `Io` variant.
async fn send_response_propagates_write_error() {
let app = WireframeApp::new()
.unwrap()
Expand All @@ -109,6 +122,10 @@ async fn send_response_propagates_write_error() {
}

#[tokio::test]
/// Tests that `send_response` returns a serialization error when encoding fails.
///
/// This test sends a `FailingResp` using `send_response` and asserts that the resulting
/// error is of the `Serialize` variant, indicating a failure during response encoding.
async fn send_response_returns_encode_error() {
let app = WireframeApp::new().unwrap();
let err = app
Expand All @@ -119,6 +136,10 @@ async fn send_response_returns_encode_error() {
}

#[test]
/// Tests roundtrip encoding and decoding of a frame using a two-byte big-endian length prefix.
///
/// Verifies that a frame encoded with a `LengthPrefixedProcessor` configured for a 2-byte
/// big-endian length format can be correctly decoded back to its original contents.
fn custom_two_byte_big_endian_roundtrip() {
let fmt = LengthFormat::u16_be();
let processor = LengthPrefixedProcessor::new(fmt);
Expand All @@ -130,6 +151,10 @@ fn custom_two_byte_big_endian_roundtrip() {
}

#[test]
/// Tests roundtrip encoding and decoding of a frame using a four-byte little-endian length prefix.
///
/// Verifies that the encoded buffer contains the correct little-endian length prefix and that
/// decoding restores the original frame.
fn custom_four_byte_little_endian_roundtrip() {
let fmt = LengthFormat::u32_le();
let processor = LengthPrefixedProcessor::new(fmt);
Expand Down