pub use crate::p1::{Error, Header, Result, Section, SubSection, SubSections};
#[derive(Debug)]
enum ParsingState {
WaitingForSection,
ReadingHeader,
ReadingBody,
ReadingSubsectionHeader,
ReadingSubSectionBody,
}
pub struct State {
state: ParsingState,
section: Option<Section>,
sub_section: Option<SubSection>,
sections: Vec<Section>,
}
fn colon_separated_values(line: &str) -> Result<(String, Option<String>)> {
if !line.contains(':') {
return Err(crate::p1::Error::InvalidInput {
message: format!(": is missing in: {}", line),
context: "".to_string(),
});
}
let mut parts = line.splitn(2, ':');
let name = parts.next().unwrap().trim().to_string();
let caption = match parts.next() {
Some(c) if c.trim().is_empty() => None,
Some(c) => Some(c.trim().to_string()),
None => None,
};
Ok((name, caption))
}
fn to_body(b: Option<String>) -> Option<String> {
match b {
Some(b) if b.trim().is_empty() => None,
Some(b) => Some(b.trim().to_string()),
None => None,
}
}
impl State {
fn waiting_for_section(&mut self, line: &str) -> Result<()> {
if line.trim().is_empty() {
return Ok(());
}
if !line.starts_with("-- ") {
return Err(crate::p1::Error::InvalidInput {
message: format!("Expecting -- , found: {}", line),
context: "".to_string(),
});
}
if let Some(mut s) = self.section.take() {
if let Some(mut sub) = self.sub_section.take() {
sub.body = to_body(sub.body.take());
s.sub_sections.0.push(sub)
}
s.body = to_body(s.body.take());
self.sections.push(s);
}
let line = &line[2..];
let (name, caption) = colon_separated_values(line)?;
self.section = Some(Section {
name,
caption,
header: Default::default(),
body: None,
sub_sections: Default::default(),
});
self.state = ParsingState::ReadingHeader;
Ok(())
}
fn reading_header(&mut self, line: &str) -> Result<()> {
let line = line.trim();
if line.trim().is_empty() {
self.state = ParsingState::ReadingBody;
return Ok(());
}
if line.starts_with("-- ") {
return self.waiting_for_section(line);
}
if line.starts_with("--- ") {
return self.read_subsection(line);
}
if !line.contains(':') {
return self.reading_body(line);
}
let (name, value) = colon_separated_values(line)?;
if let Some(mut s) = self.section.take() {
s.header.add(
name.as_str(),
value.unwrap_or_else(|| "".to_string()).as_str(),
);
self.section = Some(s);
}
Ok(())
}
fn reading_sub_header(&mut self, line: &str) -> Result<()> {
let line = line.trim();
if line.trim().is_empty() {
self.state = ParsingState::ReadingSubSectionBody;
return Ok(());
}
if line.starts_with("-- ") {
return self.waiting_for_section(line);
}
if line.starts_with("--- ") {
return self.read_subsection(line);
}
if !line.contains(':') {
return self.reading_sub_body(line);
}
let (name, value) = colon_separated_values(line)?;
if let Some(mut s) = self.sub_section.take() {
s.header.add(
name.as_str(),
value.unwrap_or_else(|| "".to_string()).as_str(),
);
self.sub_section = Some(s);
}
Ok(())
}
fn reading_body(&mut self, line: &str) -> Result<()> {
self.state = ParsingState::ReadingBody;
if line.starts_with("-- ") {
return self.waiting_for_section(line);
}
if line.starts_with("--- ") {
return self.read_subsection(line);
}
let line = if line.starts_with("\\-- ") || line.starts_with("\\--- ") {
&line[1..]
} else {
line
};
if let Some(mut s) = self.section.take() {
s.body = Some(match s.body {
Some(ref b) => b.to_string() + line + "\n",
None => line.to_string() + "\n",
});
self.section = Some(s);
}
Ok(())
}
fn reading_sub_body(&mut self, line: &str) -> Result<()> {
self.state = ParsingState::ReadingSubSectionBody;
if line.starts_with("-- ") {
return self.waiting_for_section(line);
}
if line.starts_with("--- ") {
return self.read_subsection(line);
}
if let Some(mut s) = self.sub_section.take() {
s.body = Some(match s.body {
Some(ref b) => b.to_string() + line + "\n",
None => line.to_string() + "\n",
});
self.sub_section = Some(s);
}
Ok(())
}
fn read_subsection(&mut self, line: &str) -> Result<()> {
if let Some(mut sub) = self.sub_section.take() {
sub.body = to_body(sub.body.take());
if let Some(mut s) = self.section.take() {
s.sub_sections.0.push(sub);
self.section = Some(s);
}
};
let line = &line[3..];
let (name, caption) = colon_separated_values(line)?;
self.sub_section = Some(SubSection {
name,
caption,
header: Default::default(),
body: None,
});
self.state = ParsingState::ReadingSubsectionHeader;
Ok(())
}
fn finalize(mut self) -> Vec<Section> {
if let Some(mut s) = self.section.take() {
if let Some(mut sub) = self.sub_section.take() {
sub.body = to_body(sub.body.take());
s.sub_sections.0.push(sub)
}
s.body = to_body(s.body.take());
self.sections.push(s)
} else if self.sub_section.is_some() {
unreachable!("subsection without section!")
};
self.sections
}
}
pub fn parse(s: &str) -> Result<Vec<Section>> {
let mut state = State {
state: ParsingState::WaitingForSection,
section: None,
sub_section: None,
sections: vec![],
};
for mut line in s.split('\n') {
if line.starts_with(';') {
continue;
}
if line.starts_with("\\;") {
line = &line[1..];
}
match state.state {
ParsingState::WaitingForSection => state.waiting_for_section(line)?,
ParsingState::ReadingHeader => state.reading_header(line)?,
ParsingState::ReadingBody => state.reading_body(line)?,
ParsingState::ReadingSubsectionHeader => state.reading_sub_header(line)?,
ParsingState::ReadingSubSectionBody => state.reading_sub_body(line)?,
}
}
Ok(state.finalize())
}
#[cfg(test)]
mod test {
use indoc::indoc;
use pretty_assertions::assert_eq;
macro_rules! p {
($s:expr, $t: expr,) => {
p!($s, $t)
};
($s:expr, $t: expr) => {
assert_eq!(super::parse($s).unwrap_or_else(|e| panic!("{}", e)), $t)
};
}
macro_rules! f {
($s:expr, $m: expr,) => {
f!($s, $m)
};
($s:expr, $m: expr) => {
match super::parse($s) {
Ok(r) => panic!("expected failure, found: {:?}", r),
Err(e) => {
let expected = $m.trim();
let f2 = e.to_string();
let found = f2.trim();
if expected != found {
let patch = diffy::create_patch(expected, found);
let f = diffy::PatchFormatter::new().with_color();
print!(
"{}",
f.fmt_patch(&patch)
.to_string()
.replace("\\ No newline at end of file", "")
);
println!("expected:\n{}\nfound:\n{}\n", expected, f2);
panic!("test failed")
}
}
}
};
}
#[test]
fn sub_section() {
p!(
"-- foo:\n\n--- bar:",
super::Section::with_name("foo")
.add_sub_section(super::SubSection::with_name("bar"))
.list()
);
p!(
"-- foo: hello\n--- bar:",
super::Section::with_name("foo")
.and_caption("hello")
.add_sub_section(super::SubSection::with_name("bar"))
.list()
);
p!(
"-- foo:\nk:v\n--- bar:",
super::Section::with_name("foo")
.add_header("k", "v")
.add_sub_section(super::SubSection::with_name("bar"))
.list()
);
p!(
"-- foo:\nhello world\n--- bar:",
super::Section::with_name("foo")
.and_body("hello world")
.add_sub_section(super::SubSection::with_name("bar"))
.list()
);
p!(
indoc!(
"
-- foo:
body ho
--- dodo:
-- bar:
bar body
"
),
vec![
super::Section::with_name("foo")
.and_body("body ho")
.add_sub_section(super::SubSection::with_name("dodo")),
super::Section::with_name("bar").and_body("bar body")
],
);
p!(
indoc!(
"
-- foo:
body ho
-- bar:
bar body
--- dodo:
"
),
vec![
super::Section::with_name("foo").and_body("body ho"),
super::Section::with_name("bar")
.and_body("bar body")
.add_sub_section(super::SubSection::with_name("dodo"))
],
);
p!(
indoc!(
"
-- foo:
body ho
-- bar:
bar body
--- dodo:
--- rat:
"
),
vec![
super::Section::with_name("foo").and_body("body ho"),
super::Section::with_name("bar")
.and_body("bar body")
.add_sub_section(super::SubSection::with_name("dodo"))
.add_sub_section(super::SubSection::with_name("rat"))
],
);
p!(
indoc!(
"
-- foo:
body ho
-- bar:
bar body
--- dodo:
--- rat:
"
),
vec![
super::Section::with_name("foo").and_body("body ho"),
super::Section::with_name("bar")
.and_body("bar body")
.add_sub_section(super::SubSection::with_name("dodo"))
.add_sub_section(super::SubSection::with_name("rat"))
],
);
p!(
indoc!(
"
-- foo:
body ho
-- bar:
bar body
--- dodo:
--- rat:
"
),
vec![
super::Section::with_name("foo").and_body("body ho"),
super::Section::with_name("bar")
.and_body("bar body")
.add_sub_section(super::SubSection::with_name("dodo"))
.add_sub_section(super::SubSection::with_name("rat"))
],
);
p!(
indoc!(
"
-- foo:
body ho
-- bar:
bar body
--- dodo:
hello
"
),
vec![
super::Section::with_name("foo").and_body("body ho"),
super::Section::with_name("bar")
.and_body("bar body")
.add_sub_section(super::SubSection::with_name("dodo").and_body("hello"))
],
);
p!(
"-- foo:\nhello world\n--- bar:",
super::Section::with_name("foo")
.and_body("hello world")
.add_sub_section(super::SubSection::with_name("bar"))
.list()
);
p!(
"-- foo:\nhello world\n--- bar: foo",
super::Section::with_name("foo")
.and_body("hello world")
.add_sub_section(super::SubSection::with_name("bar").and_caption("foo"))
.list()
);
}
#[test]
fn activity() {
p!(
indoc!(
"
-- step:
method: GET
--- realm.rr.activity:
okind:
oid:
ekind:
null
"
),
vec![super::Section::with_name("step")
.add_header("method", "GET")
.add_sub_section(
super::SubSection::with_name("realm.rr.activity")
.add_header("okind", "")
.add_header("oid", "")
.add_header("ekind", "")
.and_body("null")
)]
)
}
#[test]
fn escaping() {
p!(
indoc!(
"
-- hello:
\\-- yo: whats up?
\\--- foo: bar
"
),
super::Section::with_name("hello")
.and_body("-- yo: whats up?\n--- foo: bar")
.list()
)
}
#[test]
fn comments() {
p!(
indoc!(
"
; yo
-- foo:
; yo
key: value
body ho
; yo
-- bar:
; yo
b: ba
; yo
bar body
; yo
--- dodo:
; yo
k: v
; yo
hello
; yo
"
),
vec![
super::Section::with_name("foo")
.and_body("body ho")
.add_header("key", "value"),
super::Section::with_name("bar")
.and_body("bar body")
.add_header("b", "ba")
.add_sub_section(
super::SubSection::with_name("dodo")
.add_header("k", "v")
.and_body("hello")
)
],
);
}
#[test]
fn two() {
p!(
indoc!(
"
-- foo:
key: value
body ho
-- bar:
b: ba
bar body
--- dodo:
k: v
hello
"
),
vec![
super::Section::with_name("foo")
.and_body("body ho")
.add_header("key", "value"),
super::Section::with_name("bar")
.and_body("bar body")
.add_header("b", "ba")
.add_sub_section(
super::SubSection::with_name("dodo")
.add_header("k", "v")
.and_body("hello")
)
],
);
}
#[test]
fn empty_key() {
p!(
"-- foo:\nkey: \n",
super::Section::with_name("foo")
.add_header("key", "")
.list()
);
p!(
"-- foo:\n--- bar:\nkey:\n",
super::Section::with_name("foo")
.add_sub_section(super::SubSection::with_name("bar").add_header("key", ""))
.list()
)
}
#[test]
fn with_dash_dash() {
p!(
indoc!(
r#"
-- hello:
hello -- world: yo
"#
),
super::Section::with_name("hello")
.and_body("hello -- world: yo")
.list()
);
p!(
indoc!(
r#"
-- hello:
--- realm.rr.step.body:
{
"body": "-- h0: Hello World\n\n-- markdown:\n\ndemo cr 1\n",
"kind": "content",
"track": "amitu/index",
"version": "2020-11-16T04:13:14.642892+00:00"
}
"#
),
super::Section::with_name("hello")
.add_sub_section(super::SubSection::with_name("realm.rr.step.body").and_body(
&indoc!(
r#"
{
"body": "-- h0: Hello World\n\n-- markdown:\n\ndemo cr 1\n",
"kind": "content",
"track": "amitu/index",
"version": "2020-11-16T04:13:14.642892+00:00"
}"#
)
))
.list()
);
}
#[test]
fn indented_body() {
p!(
&indoc!(
"
-- markdown:
hello world is
not enough
lol
"
),
super::Section::with_name("markdown")
.and_body("hello world is\n\n not enough\n\n lol")
.list(),
);
}
#[test]
fn basic() {
p!(
"-- foo: bar",
super::Section::with_name("foo").and_caption("bar").list()
);
p!("-- foo:", super::Section::with_name("foo").list());
p!("-- foo: ", super::Section::with_name("foo").list());
p!(
"-- foo:\nkey: value",
super::Section::with_name("foo")
.add_header("key", "value")
.list()
);
p!(
"-- foo:\nkey: value\nk2:v2",
super::Section::with_name("foo")
.add_header("key", "value")
.add_header("k2", "v2")
.list()
);
p!(
"-- foo:\nbody ho",
super::Section::with_name("foo").and_body("body ho").list()
);
p!(
indoc!(
"
-- foo:
body ho
-- bar:
bar body
"
),
vec![
super::Section::with_name("foo").and_body("body ho"),
super::Section::with_name("bar").and_body("bar body")
],
);
p!(
indoc!(
"
-- foo:
body ho
yo
-- bar:
bar body
"
),
vec![
super::Section::with_name("foo").and_body("body ho\n\nyo"),
super::Section::with_name("bar").and_body("bar body")
],
);
f!("invalid", "invalid input: Expecting -- , found: invalid")
}
}