Refactor WebSocketContext::read_message_frame#515
Refactor WebSocketContext::read_message_frame#515akonradi-signal wants to merge 3 commits intosnapview:masterfrom
Conversation
| /// Try to decode one message frame. May return None. | ||
| fn read_message_frame(&mut self, stream: &mut impl Read) -> Result<Option<Message>> { | ||
| if let Some(frame) = self | ||
| let frame = match self |
There was a problem hiding this comment.
It would be nice if this could be written as let Some(frame) = ... else { ... }; but that's not available until Rust 1.65 and the MSRV here is 1.63.
e0bd90e to
49f024a
Compare
There was a problem hiding this comment.
I agree with these ones:
- ✅ Early return from the first
match. It definitely looks better! - ✅ Replacing
if let Some(ref mut msg) = self.incomplete { .. } else { return .. }withself.incomplete.as_mut().ok_or(..). Definitely looks more elegant!
As for excessive branching: while I generally agree that it may make code less readable and that read_message_frame() would benefit from some refactoring, I'm not sure that the rest of the changes bring the desired simplification for the following reasons:
- The current handling of the frame's opcode is just under 70 LOC. I may be biased, but I find 70 LOC that contain a "flat representation" of all possible cases (including errors) quite useful. With the new changes, I get a bit more confused, because, e.g., the handling of the reserved code for the
Ctlopcode is done within thematch, but the handling of the reserved code for theDataopcode is done in a different place (inside theDataMessageType::try_from()conversion function) for no apparent reason. - I'm generally confused about the new
DataMessageType. It feels like it was only introduced to remove a singlematchcase inside theread_message_frame()and that it has no utility otherwise. TheTryFrom<Data> for DataMessageTypeessentially states that we can createDataMessageTypefromData, by e.g. doingData::Binary => Self::Initial(IncompleteMessageType::Binary), which feels logically incorrect, because we cannot state that the message is incomplete before checkingfinfirst (frame.header().is_final). Another problem that it introduces is that it changes the order of operations: sincetry_from()transforms reserved code into an error,DataMessageType::try_from(data)?would early return an error when the reserved code is used. However, the previous implementation would not do so if theself.incompleteisSome(..)(it would return a different error), because the match arm for_ if self.incomplete.is_some()would be checked before theOpData::Reserved(i).
49f024a to
20a8c8f
Compare
|
I've pulled out the uncontroversial changes into separate commits while leaving the total change the same. As for the rest of the feedback:
|
|
Funnily enough I tried writing a test against the current #[test]
fn reserved_data_frame_type_in_incomplete_message() {
let mut incoming = WriteMoc(Cursor::new(&[
0x01, 0x06, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, // "Hello"
0x06, // reserved frame code
0x00, 0x06, 0x20, 0x74, 0x68, 0x65, 0x72, 0x65, // (continuation) " there"
]));
let mut context = WebSocketContext::new(Role::Client, None);
let err = context.read(&mut incoming).unwrap_err();
assert!(
matches!(err, Error::Protocol(ProtocolError::UnknownDataFrameType(0x06))),
"err was {err:?}"
);
}An error is produced by tungstenite-rs/src/protocol/frame/frame.rs Lines 193 to 196 in 6520d8f |
Thanks! If you prefer, you could create a separate PR with those commits, so that I can merge them right away.
I agree that having |
Eagerly check conditions for a data frame and encode the result explicitly in a custom enum type. This lets us combine code from two match arms and eliminate a panic! that was impossible to hit but required by the compiler.
20a8c8f to
5da8286
Compare
|
I've pushed a new final commit on the branch that takes a more focused approach to the refactoring. I've preserved the top-level match on the frame opcode and the precedence of checks for error conditions. As noted in the commit description, this does let us get rid of a |
|
Thanks! I've pondered over the changes in order to understand what exactly I do not like about the current ( I think the main thing I find suboptimal about the implementation in The only way to get rid of this complication (without some major re-thinking / refactoring) is to split the processing of the data frame into 2 stages. I believe that your latest change tries to achieve it: judging from the code, the first I attempted to formulate a suggestion based on your changes and what I believe might address the points you dislike, while considering my concerns and maintaining code readability.
What do you think? |
Closing the loop: https://github.com/signalapp/tungstenite-rs supports |
Reduce the amount of nested code by returning earlier on errors and from self-contained logic instead of branching with if/else and match expressions. Use an enum type to consolidate common code and avoid a panic! call.