stomp-parser 0.8.0

A crate for parsing Stomp frames, implemented using nom.
Documentation
pub mod headers;

use nom::bytes::complete::is_not;
use nom::character::complete::{char, line_ending};
use nom::combinator::eof;
use nom::error::context;
use nom::sequence::terminated;
use nom::{IResult, Needed};

use crate::error::FullError;
use crate::error::StompParseError;

pub fn null<'a, E: 'a + FullError<&'a [u8], StompParseError>>(
    input: &'a [u8],
) -> IResult<&'a [u8], &'a [u8], E> {
    terminated(
        context("Null Octet", char('\x00')),
        context("Data after null", eof),
    )(input)
    .map(|(rem, _)| (rem, &input[input.len() - 1..]))
}

pub fn command_line<'a, E: FullError<&'a [u8], E2>, E2>(
    input: &'a [u8],
) -> IResult<&'a [u8], &'a [u8], E> {
    terminated(is_not("\r\n"), line_ending)(input)
}

pub fn remaining_without_null<'a, E: FullError<&'a [u8], StompParseError>>(
    input: &'a [u8],
) -> IResult<&'a [u8], &'a [u8], E> {
    match input.split_last() {
        Some((&0u8, bytes)) => Ok((&bytes[0..0], bytes)),
        _ => Err(nom::Err::Incomplete(Needed::Unknown)),
    }
}

#[cfg(test)]
mod tests {
    use crate::client::ClientFrame;
    use crate::headers::{AckType, HeartBeatIntervals, StompVersion, StompVersions};
    use std::convert::TryFrom;

    #[test]
    fn it_recognises_connect_frames() {
        let frame = ClientFrame::try_from(
            "CONNECT\naccept-version:1.1,1.2,funk\nhost:b\r\n\n\u{00}"
                .as_bytes()
                .to_owned(),
        );

        match frame.unwrap() {
            ClientFrame::Connect(frame) => {
                assert_eq!(
                    StompVersions(vec![
                        StompVersion::V1_1,
                        StompVersion::V1_2,
                        StompVersion::Unknown("funk".to_string())
                    ]),
                    *frame.accept_version().value()
                );
                assert_eq!("b", frame.host().value());
                assert_eq!(None, frame.login());
                assert_eq!(None, frame.passcode());
            }
            _ => panic!("Not a Connect Frame!"),
        }
    }

    #[test]
    fn it_accepts_optional_connect_frame_headers() {
        let frame = ClientFrame::try_from(
            "CONNECT\naccept-version:1.1,1.2,\
            funk\nhost:b\r\nlogin:slarti\npasscode:bartfast\n\n\u{00}"
                .as_bytes()
                .to_owned(),
        );
        match frame.unwrap() {
            ClientFrame::Connect(frame) => {
                assert_eq!("slarti", frame.login().unwrap().value());
                assert_eq!("bartfast", frame.passcode().unwrap().value());
            }
            _ => panic!("Not a Connect Frame!"),
        }
    }

    #[test]
    fn it_fails_if_host_missing() {
        let frame = ClientFrame::try_from(
            "CONNECT\naccept-version:a\nlogin:foo\r\nheart-beat:10,20\r\n\
            \nasakldkf\u{00}"
                .as_bytes()
                .to_owned(),
        );

        assert!(matches!(frame, Err(_)));
    }

    #[test]
    fn it_accepts_heartbeat() {
        let frame = ClientFrame::try_from(
            "CONNECT\naccept-version:a\nhost:b\r\nlogin:foo\r\nheart-beat:10,20\r\n\
            \n\u{00}"
                .as_bytes()
                .to_owned(),
        )
        .unwrap();
        if let ClientFrame::Connect(frame) = frame {
            assert_eq!(
                HeartBeatIntervals {
                    supplied: 10,
                    expected: 20,
                },
                *frame.heartbeat().value()
            );
        } else {
            panic!("Not a connect frame!")
        }
    }

    #[test]
    #[allow(non_snake_case)]
    fn CONNECT_frame_rejects_body() {
        let frame =
            ClientFrame::try_from(b"CONNECT\naccept-version:a\nhost:b\r\n\nfoobar\x00".to_vec());
        assert!(frame.is_err());
    }

    #[test]
    #[allow(non_snake_case)]
    fn CONNECT_frame_rejects_data_after_null() {
        let frame =
            ClientFrame::try_from(b"CONNECT\naccept-version:a\nhost:b\r\n\n\x00foobar".to_vec());
        assert!(frame.is_err());
    }

    #[test]
    fn it_is_case_sensitive() {
        let frame = ClientFrame::try_from(
            b"coNNect\naccept-version:a\nhost:b\r\n\n
        \x00"
                .to_vec(),
        );
        assert!(frame.is_err());
    }

    #[test]
    fn it_doesnt_match_rubbish() {
        let frame =
            ClientFrame::try_from(b"coNct\naccept-version:a\nhost:b\r\n\nasakldkf\x00".to_vec());
        assert!(frame.is_err());
    }

    #[test]
    #[allow(non_snake_case)]
    fn STOMP_frame_works() {
        let frame =
            ClientFrame::try_from(b"STOMP\naccept-version:a\nhost:b\r\n\n\x00".to_vec()).unwrap();
        assert!(matches!(frame, ClientFrame::Connect(_)));
    }

    #[test]
    #[allow(non_snake_case)]
    fn SUBSCRIBE_frame_accepted() {
        let frame = ClientFrame::try_from(
            b"SUBSCRIBE\r\ndestination:y/b\nid:1\nack:client\n\n\x00".to_vec(),
        )
        .unwrap();

        if let ClientFrame::Subscribe(frame) = frame {
            assert_eq!("y/b", frame.destination().value());
            assert_eq!("1", frame.id().value());
            assert_eq!(AckType::Client, *frame.ack_type().value())
        } else {
            panic!("Not a SUBSCRIBE");
        }
    }

    #[test]
    #[allow(non_snake_case)]
    fn SUBSCRIBE_frame_requires_id() {
        let frame =
            ClientFrame::try_from(b"SUBSCRIBE\r\ndestination:y/b\nack:client\n\n\x00".to_vec());
        assert!(frame.is_err())
    }

    #[test]
    #[allow(non_snake_case)]
    fn SUBSCRIBE_frame_requires_destination() {
        let frame = ClientFrame::try_from(b"SUBSCRIBE\r\nid:1\nack:client\n\n\x00".to_vec());
        assert!(frame.is_err())
    }

    #[test]
    #[allow(non_snake_case)]
    fn SUBSCRIBE_frame_accepts_ack() {
        let frame = ClientFrame::try_from(
            b"SUBSCRIBE\r\ndestination:y/b\nid:1\nack:client-individual\n\n\x00".to_vec(),
        )
        .unwrap();

        if let ClientFrame::Subscribe(frame) = frame {
            assert_eq!(AckType::ClientIndividual, *frame.ack_type().value())
        } else {
            panic!("Not a SUBSCRIBE");
        }
    }

    #[test]
    #[allow(non_snake_case)]
    fn SUBSCRIBE_frame_disallows_body() {
        let frame = ClientFrame::try_from(
            b"SUBSCRIBE\r\ndestination:y/b\nid:1\nack:client-individual\n\nsdsd\x00".to_vec(),
        );

        assert!(frame.is_err())
    }

    #[test]
    #[allow(non_snake_case)]
    fn it_accepts_SEND_frame() {
        let frame =
            ClientFrame::try_from(b"SEND\ndestination:foo\r\n\nhello,world\x00".to_vec()).unwrap();

        if let ClientFrame::Send(frame) = frame {
            assert_eq!(
                "hello,world",
                std::str::from_utf8(frame.body().unwrap()).unwrap()
            );
        } else {
            panic!("Not a send");
        }
    }

    #[test]
    fn it_recognises_abort_frames() {
        let frame = ClientFrame::try_from(b"ABORT\ntransaction:trn-1\n\n\x00".to_vec()).unwrap();

        let ClientFrame::Abort(frame) = frame else {
            panic!("Not a Connect Frame!")
        };
        assert_eq!("trn-1", frame.transaction().value());
    }

    #[test]
    fn it_recognises_ack_frames_without_receipt() {
        let frame =
            ClientFrame::try_from(b"ACK\nid:foo\ntransaction:trn-1\n\n\x00".to_vec()).unwrap();

        let ClientFrame::Ack(frame) = frame else {
            panic!("Not a Ack Frame!")
        };
        assert_eq!("foo", frame.id().value());
        assert_eq!("trn-1", frame.transaction().value());
        assert_eq!(None, frame.receipt());
    }

    #[test]
    fn it_recognises_ack_frames_with_receipt() {
        let frame = ClientFrame::try_from(
            b"ACK\nid:foo\ntransaction:trn-1\nreceipt:recpt-x\n\n\x00".to_vec(),
        )
        .unwrap();

        let ClientFrame::Ack(frame) = frame else {
            panic!("Not a Ack Frame!")
        };
        assert_eq!("foo", frame.id().value());
        assert_eq!("trn-1", frame.transaction().value());
        assert_eq!("recpt-x", frame.receipt().unwrap().value());
    }

    #[test]
    fn it_recognises_begin_frames() {
        let frame = ClientFrame::try_from(b"BEGIN\ntransaction:trn-1\n\n\x00".to_vec()).unwrap();

        let ClientFrame::Begin(frame) = frame else {
            panic!("Not a Begin Frame!")
        };
        assert_eq!("trn-1", frame.transaction().value());
        assert_eq!(None, frame.receipt());
    }

    #[test]
    fn it_recognises_begin_frames_with_receipt() {
        let frame =
            ClientFrame::try_from(b"BEGIN\ntransaction:trn-1\nreceipt:recpt-x\n\n\x00".to_vec())
                .unwrap();

        let ClientFrame::Begin(frame) = frame else {
            panic!("Not a Begin Frame!")
        };
        assert_eq!("trn-1", frame.transaction().value());
        assert_eq!("recpt-x", frame.receipt().unwrap().value());
    }

    #[test]
    fn it_recognises_commit_frames() {
        let frame = ClientFrame::try_from(b"COMMIT\ntransaction:trn-1\n\n\x00".to_vec()).unwrap();

        let ClientFrame::Commit(frame) = frame else {
            panic!("Not a Commit Frame!")
        };
        assert_eq!("trn-1", frame.transaction().value());
        assert_eq!(None, frame.receipt());
    }

    #[test]
    fn it_recognises_commit_frames_with_receipt() {
        let frame =
            ClientFrame::try_from(b"COMMIT\ntransaction:trn-1\nreceipt:recpt-x\n\n\x00".to_vec())
                .unwrap();

        let ClientFrame::Commit(frame) = frame else {
            panic!("Not a Commit Frame!")
        };
        assert_eq!("trn-1", frame.transaction().value());
        assert_eq!("recpt-x", frame.receipt().unwrap().value());
    }

    #[test]
    fn it_recognises_nack_frames_without_receipt() {
        let frame =
            ClientFrame::try_from(b"NACK\nid:foo\ntransaction:trn-1\n\n\x00".to_vec()).unwrap();

        let ClientFrame::Nack(frame) = frame else {
            panic!("Not a Nack Frame!")
        };
        assert_eq!("foo", frame.id().value());
        assert_eq!("trn-1", frame.transaction().value());
        assert_eq!(None, frame.receipt());
    }

    #[test]
    fn it_recognises_nack_frames_with_receipt() {
        let frame = ClientFrame::try_from(
            b"NACK\nid:foo\ntransaction:trn-1\nreceipt:recpt-x\n\n\x00".to_vec(),
        )
        .unwrap();

        let ClientFrame::Nack(frame) = frame else {
            panic!("Not a Nack Frame!")
        };
        assert_eq!("foo", frame.id().value());
        assert_eq!("trn-1", frame.transaction().value());
        assert_eq!("recpt-x", frame.receipt().unwrap().value());
    }

    #[test]
    fn it_recognises_disconnect_frames() {
        let frame = ClientFrame::try_from(b"DISCONNECT\nreceipt:recpt-x\n\n\x00".to_vec()).unwrap();

        let ClientFrame::Disconnect(frame) = frame else {
            panic!("Not a Disconnect Frame!")
        };
        assert_eq!("recpt-x", frame.receipt().value());
    }

    #[test]
    fn it_recognises_unsubscribe_frames() {
        let frame = ClientFrame::try_from(b"UNSUBSCRIBE\nid:foo\n\n\x00".to_vec()).unwrap();

        let ClientFrame::Unsubscribe(frame) = frame else {
            panic!("Not a Unsubscribe Frame!")
        };
        assert_eq!("foo", frame.id().value());
        assert_eq!(None, frame.receipt());
    }

    #[test]
    fn it_recognises_unsubscribe_frames_with_receipt() {
        let frame = ClientFrame::try_from(b"UNSUBSCRIBE\nid:foo\nreceipt:recpt-x\n\n\x00".to_vec())
            .unwrap();

        let ClientFrame::Unsubscribe(frame) = frame else {
            panic!("Not a Unsubscribe Frame!")
        };
        assert_eq!("foo", frame.id().value());
        assert_eq!("recpt-x", frame.receipt().unwrap().value());
    }
}