Skip to content

Instantly share code, notes, and snippets.

@huntc
Last active September 10, 2021 06:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save huntc/b37dcf020a1b37fb56bad841ac8b6a2f to your computer and use it in GitHub Desktop.
Save huntc/b37dcf020a1b37fb56bad841ac8b6a2f to your computer and use it in GitHub Desktop.
Rust nom parsing for Server Sent Events
use nom::{
bytes::complete::take_while1,
character::complete::{char, line_ending, not_line_ending},
combinator::opt,
sequence::pair,
IResult,
};
#[derive(Debug, PartialEq)]
pub enum Field {
Event(String),
Data(String),
Id(String),
Retry(u32),
}
#[derive(Debug, PartialEq)]
pub enum Event {
Comment,
Empty,
Field(Option<Field>),
}
fn event(input: &str) -> IResult<&str, Event> {
let (input, comment) = opt(char(':'))(input)?;
if comment.is_some() {
let (input, _) = pair(not_line_ending, line_ending)(input)?;
let e = Event::Comment;
Ok((input, e))
} else {
match opt(take_while1(|c| c != '\n' && c != '\r' && c != ':'))(input)? {
(input, Some(n)) => {
let (input, v_delim) = opt(pair(char(':'), char(' ')))(input)?;
let (input, v) = if v_delim.is_some() {
let (input, v) = not_line_ending(input)?;
let (input, _) = line_ending(input)?;
(input, v)
} else {
let (input, _) = line_ending(input)?;
(input, "")
};
Ok((
input,
match n {
"event" => Event::Field(Some(Field::Event(v.to_string()))),
"data" => Event::Field(Some(Field::Data(format!("{}\n", v)))),
"id" => Event::Field(Some(Field::Id(v.to_string()))),
"retry" => match v.parse::<u32>() {
Ok(t) => Event::Field(Some(Field::Retry(t))),
Err(_) => Event::Field(None),
},
_ => Event::Field(None),
},
))
}
(input, None) => {
let (input, _) = line_ending(input)?;
Ok((input, Event::Empty))
}
}
}
}
#[cfg(test)]
mod tests {
use nom::{
error::{Error, ErrorKind},
Err,
};
use super::*;
#[test]
fn parse_empty_line() {
assert_eq!(event("\r\n"), Ok(("", Event::Empty)));
}
#[test]
fn parse_empty_comment() {
assert_eq!(event(":\r\n"), Ok(("", Event::Comment)));
}
#[test]
fn parse_comment() {
assert_eq!(event(":Some comment\n"), Ok(("", Event::Comment)));
}
#[test]
fn parse_field_with_value() {
assert_eq!(
event("data: Some Value\n"),
Ok((
"",
Event::Field(Some(Field::Data("Some Value\n".to_string())))
))
);
}
#[test]
fn parse_field_with_empty_value() {
assert_eq!(
event("data: \n"),
Ok(("", Event::Field(Some(Field::Data("\n".to_string())))))
);
}
#[test]
fn parse_field_with_value_expected() {
assert_eq!(
event("data:\n"),
Err(Err::Error(Error {
input: ":\n",
code: ErrorKind::CrLf
}))
);
}
#[test]
fn parse_field_with_no_value() {
assert_eq!(
event("data\n"),
Ok(("", Event::Field(Some(Field::Data("\n".to_string())))))
);
}
#[test]
fn parse_event_field() {
assert_eq!(
event("event: some-event\r\n"),
Ok((
"",
Event::Field(Some(Field::Event("some-event".to_string())))
))
);
}
#[test]
fn parse_data_field() {
assert_eq!(
event("data: Some Data\r\n"),
Ok((
"",
Event::Field(Some(Field::Data("Some Data\n".to_string())))
))
);
}
#[test]
fn parse_id_field() {
assert_eq!(
event("id: 10\r\n"),
Ok(("", Event::Field(Some(Field::Id("10".to_string())))))
);
}
#[test]
fn parse_retry_field() {
assert_eq!(
event("retry: 10\r\n"),
Ok(("", Event::Field(Some(Field::Retry(10)))))
);
}
#[test]
fn parse_invalid_retry_field() {
assert_eq!(event("retry: x\r\n"), Ok(("", Event::Field(None))));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment