parser is now perfect
This commit is contained in:
parent
eacb903dfd
commit
c244975a0a
3 changed files with 289 additions and 100 deletions
|
@ -42,6 +42,7 @@ impl<'a> GitConfig<'a> {
|
||||||
Event::Comment(_) => (),
|
Event::Comment(_) => (),
|
||||||
Event::SectionHeader(ParsedSectionHeader {
|
Event::SectionHeader(ParsedSectionHeader {
|
||||||
name,
|
name,
|
||||||
|
separator: _,
|
||||||
subsection_name,
|
subsection_name,
|
||||||
}) => {
|
}) => {
|
||||||
current_section_name = name;
|
current_section_name = name;
|
||||||
|
@ -112,6 +113,7 @@ impl<'a> GitConfig<'a> {
|
||||||
options.on_duplicate_name,
|
options.on_duplicate_name,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
Event::Whitespace(_) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
265
src/parser.rs
265
src/parser.rs
|
@ -8,7 +8,7 @@
|
||||||
//! additional methods for accessing leading comments or events by section.
|
//! additional methods for accessing leading comments or events by section.
|
||||||
|
|
||||||
use crate::values::{Boolean, TrueVariant, Value};
|
use crate::values::{Boolean, TrueVariant, Value};
|
||||||
use nom::bytes::complete::{escaped, tag, take_till, take_while};
|
use nom::bytes::complete::{escaped, tag, take_till, take_until, take_while};
|
||||||
use nom::character::complete::{char, none_of, one_of};
|
use nom::character::complete::{char, none_of, one_of};
|
||||||
use nom::character::{is_newline, is_space};
|
use nom::character::{is_newline, is_space};
|
||||||
use nom::combinator::{map, opt};
|
use nom::combinator::{map, opt};
|
||||||
|
@ -37,6 +37,7 @@ pub enum Event<'a> {
|
||||||
ValueNotDone(&'a str),
|
ValueNotDone(&'a str),
|
||||||
/// The last line of a value which was continued onto another line.
|
/// The last line of a value which was continued onto another line.
|
||||||
ValueDone(&'a str),
|
ValueDone(&'a str),
|
||||||
|
Whitespace(&'a str),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||||
|
@ -49,6 +50,13 @@ pub struct ParsedSection<'a> {
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||||
pub struct ParsedSectionHeader<'a> {
|
pub struct ParsedSectionHeader<'a> {
|
||||||
pub name: &'a str,
|
pub name: &'a str,
|
||||||
|
/// The separator used to determine if the section contains a subsection.
|
||||||
|
/// This is either a period `.` or a string of whitespace. Note that
|
||||||
|
/// reconstruction of subsection format is dependent on this value. If this
|
||||||
|
/// is all whitespace, then the subsection name needs to be surrounded by
|
||||||
|
/// quotes to have perfect reconstruction.
|
||||||
|
pub separator: Option<&'a str>,
|
||||||
|
/// The subsection name without quotes if any exist.
|
||||||
pub subsection_name: Option<&'a str>,
|
pub subsection_name: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,11 +82,10 @@ impl<'a> From<nom::Err<NomError<&'a str>>> for ParserError<'a> {
|
||||||
|
|
||||||
/// A zero-copy `git-config` file parser.
|
/// A zero-copy `git-config` file parser.
|
||||||
///
|
///
|
||||||
/// # Non-perfect parser
|
/// This is parser is considered a perfect parser, where a `git-config` file
|
||||||
///
|
/// can be identically reconstructed from the events emitted from this parser.
|
||||||
/// This parser should successfully parse all sections and comments. However,
|
/// Events emitted from this parser are bound to the lifetime of the provided
|
||||||
/// It will not parse whitespace. This attempts to closely follow the
|
/// `str` as this parser performs no copies from the input.
|
||||||
/// non-normative specification found in [`git`'s documentation].
|
|
||||||
///
|
///
|
||||||
/// # Differences between a `.ini` parser
|
/// # Differences between a `.ini` parser
|
||||||
///
|
///
|
||||||
|
@ -222,7 +229,6 @@ pub fn parse_from_str(input: &str) -> Result<Parser<'_>, ParserError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn comment<'a>(i: &'a str) -> IResult<&'a str, ParsedComment<'a>> {
|
fn comment<'a>(i: &'a str) -> IResult<&'a str, ParsedComment<'a>> {
|
||||||
let i = i.trim_start();
|
|
||||||
let (i, comment_tag) = one_of(";#")(i)?;
|
let (i, comment_tag) = one_of(";#")(i)?;
|
||||||
let (i, comment) = take_till(is_char_newline)(i)?;
|
let (i, comment) = take_till(is_char_newline)(i)?;
|
||||||
Ok((
|
Ok((
|
||||||
|
@ -235,9 +241,10 @@ fn comment<'a>(i: &'a str) -> IResult<&'a str, ParsedComment<'a>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn section<'a>(i: &'a str) -> IResult<&'a str, ParsedSection<'a>> {
|
fn section<'a>(i: &'a str) -> IResult<&'a str, ParsedSection<'a>> {
|
||||||
let i = i.trim_start();
|
|
||||||
let (i, section_header) = section_header(i)?;
|
let (i, section_header) = section_header(i)?;
|
||||||
let (i, items) = many1(alt((
|
let (i, items) = many0(alt((
|
||||||
|
map(take_spaces, |space| vec![Event::Whitespace(space)]),
|
||||||
|
map(take_newline, |newline| vec![Event::Newline(newline)]),
|
||||||
map(section_body, |(key, values)| {
|
map(section_body, |(key, values)| {
|
||||||
let mut vec = vec![Event::Key(key)];
|
let mut vec = vec![Event::Key(key)];
|
||||||
vec.extend(values);
|
vec.extend(values);
|
||||||
|
@ -265,10 +272,12 @@ fn section_header<'a>(i: &'a str) -> IResult<&'a str, ParsedSectionHeader<'a>> {
|
||||||
let header = match name.rfind('.') {
|
let header = match name.rfind('.') {
|
||||||
Some(index) => ParsedSectionHeader {
|
Some(index) => ParsedSectionHeader {
|
||||||
name: &name[..index],
|
name: &name[..index],
|
||||||
subsection_name: Some(&name[index + 1..]),
|
separator: name.get(index..index + 1),
|
||||||
|
subsection_name: name.get(index + 1..),
|
||||||
},
|
},
|
||||||
None => ParsedSectionHeader {
|
None => ParsedSectionHeader {
|
||||||
name: name,
|
name: name,
|
||||||
|
separator: None,
|
||||||
subsection_name: None,
|
subsection_name: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -278,7 +287,7 @@ fn section_header<'a>(i: &'a str) -> IResult<&'a str, ParsedSectionHeader<'a>> {
|
||||||
|
|
||||||
// Section header must be using modern subsection syntax at this point.
|
// Section header must be using modern subsection syntax at this point.
|
||||||
|
|
||||||
let (i, _) = take_spaces(i)?;
|
let (i, whitespace) = take_spaces(i)?;
|
||||||
|
|
||||||
let (i, subsection_name) = delimited(
|
let (i, subsection_name) = delimited(
|
||||||
char('"'),
|
char('"'),
|
||||||
|
@ -290,6 +299,7 @@ fn section_header<'a>(i: &'a str) -> IResult<&'a str, ParsedSectionHeader<'a>> {
|
||||||
i,
|
i,
|
||||||
ParsedSectionHeader {
|
ParsedSectionHeader {
|
||||||
name: name,
|
name: name,
|
||||||
|
separator: Some(whitespace),
|
||||||
// We know that there's some section name here, so if we get an
|
// We know that there's some section name here, so if we get an
|
||||||
// empty vec here then we actually parsed an empty section name.
|
// empty vec here then we actually parsed an empty section name.
|
||||||
subsection_name: subsection_name.or(Some("")),
|
subsection_name: subsection_name.or(Some("")),
|
||||||
|
@ -297,18 +307,18 @@ fn section_header<'a>(i: &'a str) -> IResult<&'a str, ParsedSectionHeader<'a>> {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn take_spaces<'a>(i: &'a str) -> IResult<&'a str, &'a str> {
|
|
||||||
take_while(|c: char| c.is_ascii() && is_space(c as u8))(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn section_body<'a>(i: &'a str) -> IResult<&'a str, (&'a str, Vec<Event<'a>>)> {
|
fn section_body<'a>(i: &'a str) -> IResult<&'a str, (&'a str, Vec<Event<'a>>)> {
|
||||||
let i = i.trim_start();
|
|
||||||
// maybe need to check for [ here
|
// maybe need to check for [ here
|
||||||
let (i, name) = config_name(i)?;
|
let (i, name) = config_name(i)?;
|
||||||
let (i, _) = take_spaces(i)?;
|
let (i, whitespace) = opt(take_spaces)(i)?;
|
||||||
let (i, value) = config_value(i)?;
|
let (i, value) = config_value(i)?;
|
||||||
|
if let Some(whitespace) = whitespace {
|
||||||
|
let mut events = vec![Event::Whitespace(whitespace)];
|
||||||
|
events.extend(value);
|
||||||
|
Ok((i, (name, events)))
|
||||||
|
} else {
|
||||||
Ok((i, (name, value)))
|
Ok((i, (name, value)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses the config name of a config pair. Assumes the input has already been
|
/// Parses the config name of a config pair. Assumes the input has already been
|
||||||
|
@ -333,8 +343,15 @@ fn config_name<'a>(i: &'a str) -> IResult<&'a str, &'a str> {
|
||||||
|
|
||||||
fn config_value<'a>(i: &'a str) -> IResult<&'a str, Vec<Event<'a>>> {
|
fn config_value<'a>(i: &'a str) -> IResult<&'a str, Vec<Event<'a>>> {
|
||||||
if let (i, Some(_)) = opt(char('='))(i)? {
|
if let (i, Some(_)) = opt(char('='))(i)? {
|
||||||
let (i, _) = take_spaces(i)?;
|
let (i, whitespace) = opt(take_spaces)(i)?;
|
||||||
value_impl(i)
|
let (i, values) = value_impl(i)?;
|
||||||
|
if let Some(whitespace) = whitespace {
|
||||||
|
let mut events = vec![Event::Whitespace(whitespace)];
|
||||||
|
events.extend(values);
|
||||||
|
Ok((i, events))
|
||||||
|
} else {
|
||||||
|
Ok((i, values))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok((
|
Ok((
|
||||||
i,
|
i,
|
||||||
|
@ -418,20 +435,49 @@ fn value_impl<'a>(i: &'a str) -> IResult<&'a str, Vec<Event<'a>>> {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let remainder_value = &i[offset..parsed_index].trim_end();
|
let (i, remainder_value) = {
|
||||||
|
let mut new_index = parsed_index;
|
||||||
|
for index in (offset..parsed_index).rev() {
|
||||||
|
if !(i.as_bytes()[index] as char).is_whitespace() {
|
||||||
|
new_index = index + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(&i[new_index..], &i[offset..new_index])
|
||||||
|
};
|
||||||
|
|
||||||
if partial_value_found {
|
if partial_value_found {
|
||||||
events.push(Event::ValueDone(remainder_value));
|
events.push(Event::ValueDone(remainder_value));
|
||||||
} else {
|
} else {
|
||||||
events.push(Event::Value(Value::from_str(remainder_value)));
|
events.push(Event::Value(Value::from_str(remainder_value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((&i[parsed_index..], events))
|
Ok((i, events))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_char_newline(c: char) -> bool {
|
fn is_char_newline(c: char) -> bool {
|
||||||
c.is_ascii() && is_newline(c as u8)
|
c.is_ascii() && is_newline(c as u8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn take_spaces<'a>(i: &'a str) -> IResult<&'a str, &'a str> {
|
||||||
|
take_common(i, |c: char| c.is_ascii() && is_space(c as u8))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_newline<'a>(i: &'a str) -> IResult<&'a str, &'a str> {
|
||||||
|
take_common(i, is_char_newline)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_common<'a, F: Fn(char) -> bool>(i: &'a str, f: F) -> IResult<&'a str, &'a str> {
|
||||||
|
let (i, v) = take_while(f)(i)?;
|
||||||
|
if v.is_empty() {
|
||||||
|
Err(nom::Err::Error(NomError {
|
||||||
|
input: i,
|
||||||
|
code: ErrorKind::Eof,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok((i, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod parse {
|
mod parse {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -440,6 +486,25 @@ mod parse {
|
||||||
("", t)
|
("", t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn gen_section_header(
|
||||||
|
name: &str,
|
||||||
|
subsection: impl Into<Option<(&'static str, &'static str)>>,
|
||||||
|
) -> ParsedSectionHeader<'_> {
|
||||||
|
if let Some((separator, subsection_name)) = subsection.into() {
|
||||||
|
ParsedSectionHeader {
|
||||||
|
name,
|
||||||
|
separator: Some(separator),
|
||||||
|
subsection_name: Some(subsection_name),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParsedSectionHeader {
|
||||||
|
name,
|
||||||
|
separator: None,
|
||||||
|
subsection_name: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mod comments {
|
mod comments {
|
||||||
use super::super::*;
|
use super::super::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -486,10 +551,7 @@ mod parse {
|
||||||
fn no_subsection() {
|
fn no_subsection() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section_header("[hello]").unwrap(),
|
section_header("[hello]").unwrap(),
|
||||||
fully_consumed(ParsedSectionHeader {
|
fully_consumed(gen_section_header("hello", None)),
|
||||||
name: "hello",
|
|
||||||
subsection_name: None
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -497,10 +559,7 @@ mod parse {
|
||||||
fn modern_subsection() {
|
fn modern_subsection() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section_header(r#"[hello "world"]"#).unwrap(),
|
section_header(r#"[hello "world"]"#).unwrap(),
|
||||||
fully_consumed(ParsedSectionHeader {
|
fully_consumed(gen_section_header("hello", (" ", "world"))),
|
||||||
name: "hello",
|
|
||||||
subsection_name: Some("world")
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -508,10 +567,7 @@ mod parse {
|
||||||
fn escaped_subsection() {
|
fn escaped_subsection() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section_header(r#"[hello "foo\\bar\""]"#).unwrap(),
|
section_header(r#"[hello "foo\\bar\""]"#).unwrap(),
|
||||||
fully_consumed(ParsedSectionHeader {
|
fully_consumed(gen_section_header("hello", (" ", r#"foo\\bar\""#))),
|
||||||
name: "hello",
|
|
||||||
subsection_name: Some(r#"foo\\bar\""#)
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -519,10 +575,7 @@ mod parse {
|
||||||
fn deprecated_subsection() {
|
fn deprecated_subsection() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section_header(r#"[hello.world]"#).unwrap(),
|
section_header(r#"[hello.world]"#).unwrap(),
|
||||||
fully_consumed(ParsedSectionHeader {
|
fully_consumed(gen_section_header("hello", (".", "world")))
|
||||||
name: "hello",
|
|
||||||
subsection_name: Some("world")
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -530,10 +583,7 @@ mod parse {
|
||||||
fn empty_legacy_subsection_name() {
|
fn empty_legacy_subsection_name() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section_header(r#"[hello.]"#).unwrap(),
|
section_header(r#"[hello.]"#).unwrap(),
|
||||||
fully_consumed(ParsedSectionHeader {
|
fully_consumed(gen_section_header("hello", (".", "")))
|
||||||
name: "hello",
|
|
||||||
subsection_name: Some("")
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -541,10 +591,7 @@ mod parse {
|
||||||
fn empty_modern_subsection_name() {
|
fn empty_modern_subsection_name() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section_header(r#"[hello ""]"#).unwrap(),
|
section_header(r#"[hello ""]"#).unwrap(),
|
||||||
fully_consumed(ParsedSectionHeader {
|
fully_consumed(gen_section_header("hello", (" ", "")))
|
||||||
name: "hello",
|
|
||||||
subsection_name: Some("")
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -562,10 +609,7 @@ mod parse {
|
||||||
fn right_brace_in_subsection_name() {
|
fn right_brace_in_subsection_name() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section_header(r#"[hello "]"]"#).unwrap(),
|
section_header(r#"[hello "]"]"#).unwrap(),
|
||||||
fully_consumed(ParsedSectionHeader {
|
fully_consumed(gen_section_header("hello", (" ", "]")))
|
||||||
name: "hello",
|
|
||||||
subsection_name: Some("]")
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -611,14 +655,6 @@ mod parse {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_comment_is_trimmed() {
|
|
||||||
assert_eq!(
|
|
||||||
value_impl("hello").unwrap(),
|
|
||||||
value_impl("hello ").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn semicolon_comment_not_consumed() {
|
fn semicolon_comment_not_consumed() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -636,14 +672,31 @@ mod parse {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn values_with_comments_are_trimmed() {
|
fn values_with_extraneous_whitespace_without_comment() {
|
||||||
|
assert_eq!(
|
||||||
|
value_impl("hello ").unwrap(),
|
||||||
|
(
|
||||||
|
" ",
|
||||||
|
vec![Event::Value(Value::from_str("hello"))]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn values_with_extraneous_whitespace_before_comment() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value_impl("hello#world").unwrap(),
|
|
||||||
value_impl("hello #world").unwrap(),
|
value_impl("hello #world").unwrap(),
|
||||||
|
(
|
||||||
|
" #world",
|
||||||
|
vec![Event::Value(Value::from_str("hello")),]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value_impl("hello;world").unwrap(),
|
|
||||||
value_impl("hello ;world").unwrap(),
|
value_impl("hello ;world").unwrap(),
|
||||||
|
(
|
||||||
|
" ;world",
|
||||||
|
vec![Event::Value(Value::from_str("hello")),]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -695,7 +748,7 @@ mod parse {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value_impl("1 \"\\\"\\\na ; e \"\\\"\\\nd # \"b\t ; c").unwrap(),
|
value_impl("1 \"\\\"\\\na ; e \"\\\"\\\nd # \"b\t ; c").unwrap(),
|
||||||
(
|
(
|
||||||
"# \"b\t ; c",
|
" # \"b\t ; c",
|
||||||
vec![
|
vec![
|
||||||
Event::ValueNotDone(r#"1 "\""#),
|
Event::ValueNotDone(r#"1 "\""#),
|
||||||
Event::Newline("\n"),
|
Event::Newline("\n"),
|
||||||
|
@ -727,6 +780,17 @@ mod parse {
|
||||||
use super::super::*;
|
use super::super::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_section() {
|
||||||
|
assert_eq!(
|
||||||
|
section("[test]").unwrap(),
|
||||||
|
fully_consumed(ParsedSection {
|
||||||
|
section_header: gen_section_header("test", None),
|
||||||
|
events: vec![]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn simple_section() {
|
fn simple_section() {
|
||||||
let section_data = r#"[hello]
|
let section_data = r#"[hello]
|
||||||
|
@ -736,16 +800,23 @@ mod parse {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section(section_data).unwrap(),
|
section(section_data).unwrap(),
|
||||||
fully_consumed(ParsedSection {
|
fully_consumed(ParsedSection {
|
||||||
section_header: ParsedSectionHeader {
|
section_header: gen_section_header("hello", None),
|
||||||
name: "hello",
|
|
||||||
subsection_name: None,
|
|
||||||
},
|
|
||||||
events: vec![
|
events: vec![
|
||||||
|
Event::Newline("\n"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Key("a"),
|
Event::Key("a"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Value(Value::from_str("b")),
|
Event::Value(Value::from_str("b")),
|
||||||
|
Event::Newline("\n"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Key("c"),
|
Event::Key("c"),
|
||||||
Event::Value(Value::Boolean(Boolean::True(TrueVariant::Implicit))),
|
Event::Value(Value::Boolean(Boolean::True(TrueVariant::Implicit))),
|
||||||
|
Event::Newline("\n"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Key("d"),
|
Event::Key("d"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Value(Value::from_str("\"lol\""))
|
Event::Value(Value::from_str("\"lol\""))
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -757,11 +828,9 @@ mod parse {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section("[hello] c").unwrap(),
|
section("[hello] c").unwrap(),
|
||||||
fully_consumed(ParsedSection {
|
fully_consumed(ParsedSection {
|
||||||
section_header: ParsedSectionHeader {
|
section_header: gen_section_header("hello", None),
|
||||||
name: "hello",
|
|
||||||
subsection_name: None,
|
|
||||||
},
|
|
||||||
events: vec![
|
events: vec![
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Key("c"),
|
Event::Key("c"),
|
||||||
Event::Value(Value::Boolean(Boolean::True(TrueVariant::Implicit)))
|
Event::Value(Value::Boolean(Boolean::True(TrueVariant::Implicit)))
|
||||||
]
|
]
|
||||||
|
@ -779,30 +848,41 @@ mod parse {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section(section_data).unwrap(),
|
section(section_data).unwrap(),
|
||||||
fully_consumed(ParsedSection {
|
fully_consumed(ParsedSection {
|
||||||
section_header: ParsedSectionHeader {
|
section_header: gen_section_header("hello", None),
|
||||||
name: "hello",
|
|
||||||
subsection_name: None,
|
|
||||||
},
|
|
||||||
events: vec![
|
events: vec![
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Comment(ParsedComment {
|
Event::Comment(ParsedComment {
|
||||||
comment_tag: ';',
|
comment_tag: ';',
|
||||||
comment: " commentA",
|
comment: " commentA",
|
||||||
}),
|
}),
|
||||||
|
Event::Newline("\n"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Key("a"),
|
Event::Key("a"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Value(Value::from_str("b")),
|
Event::Value(Value::from_str("b")),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Comment(ParsedComment {
|
Event::Comment(ParsedComment {
|
||||||
comment_tag: '#',
|
comment_tag: '#',
|
||||||
comment: " commentB",
|
comment: " commentB",
|
||||||
}),
|
}),
|
||||||
|
Event::Newline("\n"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Comment(ParsedComment {
|
Event::Comment(ParsedComment {
|
||||||
comment_tag: ';',
|
comment_tag: ';',
|
||||||
comment: " commentC",
|
comment: " commentC",
|
||||||
}),
|
}),
|
||||||
|
Event::Newline("\n"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Comment(ParsedComment {
|
Event::Comment(ParsedComment {
|
||||||
comment_tag: ';',
|
comment_tag: ';',
|
||||||
comment: " commentD",
|
comment: " commentD",
|
||||||
}),
|
}),
|
||||||
|
Event::Newline("\n"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Key("c"),
|
Event::Key("c"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Value(Value::from_str("d")),
|
Event::Value(Value::from_str("d")),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -815,17 +895,18 @@ mod parse {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section("[section] a = 1 \"\\\"\\\na ; e \"\\\"\\\nd # \"b\t ; c").unwrap(),
|
section("[section] a = 1 \"\\\"\\\na ; e \"\\\"\\\nd # \"b\t ; c").unwrap(),
|
||||||
fully_consumed(ParsedSection {
|
fully_consumed(ParsedSection {
|
||||||
section_header: ParsedSectionHeader {
|
section_header: gen_section_header("section", None),
|
||||||
name: "section",
|
|
||||||
subsection_name: None,
|
|
||||||
},
|
|
||||||
events: vec![
|
events: vec![
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Key("a"),
|
Event::Key("a"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::ValueNotDone(r#"1 "\""#),
|
Event::ValueNotDone(r#"1 "\""#),
|
||||||
Event::Newline("\n"),
|
Event::Newline("\n"),
|
||||||
Event::ValueNotDone(r#"a ; e "\""#),
|
Event::ValueNotDone(r#"a ; e "\""#),
|
||||||
Event::Newline("\n"),
|
Event::Newline("\n"),
|
||||||
Event::ValueDone("d"),
|
Event::ValueDone("d"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Comment(ParsedComment {
|
Event::Comment(ParsedComment {
|
||||||
comment_tag: '#',
|
comment_tag: '#',
|
||||||
comment: " \"b\t ; c"
|
comment: " \"b\t ; c"
|
||||||
|
@ -840,22 +921,40 @@ mod parse {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section("[section \"a\"] b =\"\\\n;\";a").unwrap(),
|
section("[section \"a\"] b =\"\\\n;\";a").unwrap(),
|
||||||
fully_consumed(ParsedSection {
|
fully_consumed(ParsedSection {
|
||||||
section_header: ParsedSectionHeader {
|
section_header: gen_section_header("section", (" ", "a")),
|
||||||
name: "section",
|
|
||||||
subsection_name: Some("a")
|
|
||||||
},
|
|
||||||
events: vec![
|
events: vec![
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::Key("b"),
|
Event::Key("b"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
Event::ValueNotDone("\""),
|
Event::ValueNotDone("\""),
|
||||||
Event::Newline("\n"),
|
Event::Newline("\n"),
|
||||||
Event::ValueDone(";\""),
|
Event::ValueDone(";\""),
|
||||||
Event::Comment(ParsedComment {
|
Event::Comment(ParsedComment {
|
||||||
|
comment_tag: ';',
|
||||||
comment: "a",
|
comment: "a",
|
||||||
comment_tag: ';'
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn section_handles_extranous_whitespace_before_comment() {
|
||||||
|
assert_eq!(
|
||||||
|
section("[s]hello #world").unwrap(),
|
||||||
|
fully_consumed(ParsedSection {
|
||||||
|
section_header: gen_section_header("s", None),
|
||||||
|
events: vec![
|
||||||
|
Event::Key("hello"),
|
||||||
|
Event::Whitespace(" "),
|
||||||
|
Event::Value(Value::Boolean(Boolean::True(TrueVariant::Implicit))),
|
||||||
|
Event::Comment(ParsedComment {
|
||||||
|
comment_tag: '#',
|
||||||
|
comment: "world",
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,30 @@
|
||||||
use serde_git_config::parser::{parse_from_str, Event, ParsedSectionHeader, Parser};
|
use serde_git_config::parser::{parse_from_str, Event, ParsedSectionHeader};
|
||||||
use serde_git_config::values::Value;
|
use serde_git_config::values::Value;
|
||||||
|
|
||||||
fn fully_consumed<T>(t: T) -> (&'static str, T) {
|
fn fully_consumed<T>(t: T) -> (&'static str, T) {
|
||||||
("", t)
|
("", t)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn section_header(name: &'static str, subname: impl Into<Option<&'static str>>) -> Event<'static> {
|
fn gen_section_header(
|
||||||
Event::SectionHeader(ParsedSectionHeader {
|
name: &str,
|
||||||
|
subsection: impl Into<Option<(&'static str, &'static str)>>,
|
||||||
|
) -> Event<'_> {
|
||||||
|
Event::SectionHeader(
|
||||||
|
if let Some((separator, subsection_name)) = subsection.into() {
|
||||||
|
ParsedSectionHeader {
|
||||||
name,
|
name,
|
||||||
subsection_name: subname.into(),
|
separator: Some(separator),
|
||||||
})
|
subsection_name: Some(subsection_name),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParsedSectionHeader {
|
||||||
|
name,
|
||||||
|
separator: None,
|
||||||
|
subsection_name: None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name(name: &'static str) -> Event<'static> {
|
fn name(name: &'static str) -> Event<'static> {
|
||||||
Event::Key(name)
|
Event::Key(name)
|
||||||
}
|
}
|
||||||
|
@ -20,7 +33,16 @@ fn value(value: &'static str) -> Event<'static> {
|
||||||
Event::Value(Value::from_str(value))
|
Event::Value(Value::from_str(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn newline() -> Event<'static> {
|
||||||
|
Event::Newline("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn whitespace(value: &'static str) -> Event<'static> {
|
||||||
|
Event::Whitespace(value)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
fn personal_config() {
|
fn personal_config() {
|
||||||
let config = r#"[user]
|
let config = r#"[user]
|
||||||
email = code@eddie.sh
|
email = code@eddie.sh
|
||||||
|
@ -48,34 +70,100 @@ fn personal_config() {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
vec![
|
vec![
|
||||||
section_header("user", None),
|
gen_section_header("user", None),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
name("email"),
|
name("email"),
|
||||||
|
whitespace(" "),
|
||||||
|
whitespace(" "),
|
||||||
value("code@eddie.sh"),
|
value("code@eddie.sh"),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
name("name"),
|
name("name"),
|
||||||
|
whitespace(" "),
|
||||||
|
whitespace(" "),
|
||||||
value("Edward Shen"),
|
value("Edward Shen"),
|
||||||
section_header("core", None),
|
newline(),
|
||||||
|
|
||||||
|
gen_section_header("core", None),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
name("autocrlf"),
|
name("autocrlf"),
|
||||||
|
whitespace(" "),
|
||||||
|
whitespace(" "),
|
||||||
value("input"),
|
value("input"),
|
||||||
section_header("push", None),
|
newline(),
|
||||||
|
|
||||||
|
gen_section_header("push", None),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
name("default"),
|
name("default"),
|
||||||
|
whitespace(" "),
|
||||||
|
whitespace(" "),
|
||||||
value("simple"),
|
value("simple"),
|
||||||
section_header("commit", None),
|
newline(),
|
||||||
|
|
||||||
|
gen_section_header("commit", None),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
name("gpgsign"),
|
name("gpgsign"),
|
||||||
|
whitespace(" "),
|
||||||
|
whitespace(" "),
|
||||||
value("true"),
|
value("true"),
|
||||||
section_header("gpg", None),
|
newline(),
|
||||||
|
|
||||||
|
gen_section_header("gpg", None),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
name("program"),
|
name("program"),
|
||||||
|
whitespace(" "),
|
||||||
|
whitespace(" "),
|
||||||
value("gpg"),
|
value("gpg"),
|
||||||
section_header("url", "ssh://git@github.com/"),
|
newline(),
|
||||||
|
|
||||||
|
gen_section_header("url", (" ", "ssh://git@github.com/")),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
name("insteadOf"),
|
name("insteadOf"),
|
||||||
|
whitespace(" "),
|
||||||
|
whitespace(" "),
|
||||||
value("\"github://\""),
|
value("\"github://\""),
|
||||||
section_header("url", "ssh://git@git.eddie.sh/edward/"),
|
newline(),
|
||||||
|
|
||||||
|
gen_section_header("url", (" ", "ssh://git@git.eddie.sh/edward/")),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
name("insteadOf"),
|
name("insteadOf"),
|
||||||
|
whitespace(" "),
|
||||||
|
whitespace(" "),
|
||||||
value("\"gitea://\""),
|
value("\"gitea://\""),
|
||||||
section_header("pull", None),
|
newline(),
|
||||||
|
|
||||||
|
gen_section_header("pull", None),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
name("ff"),
|
name("ff"),
|
||||||
|
whitespace(" "),
|
||||||
|
whitespace(" "),
|
||||||
value("only"),
|
value("only"),
|
||||||
section_header("init", None),
|
newline(),
|
||||||
|
|
||||||
|
gen_section_header("init", None),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
name("defaultBranch"),
|
name("defaultBranch"),
|
||||||
|
whitespace(" "),
|
||||||
|
whitespace(" "),
|
||||||
value("master"),
|
value("master"),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
Reference in a new issue