diff --git a/src/config.rs b/src/config.rs index 972b4e6..127f8d5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,8 +13,11 @@ enum LookupTreeNode<'a> { #[derive(Debug, PartialEq, Eq)] pub enum GitConfigError<'a> { + /// The requested section does not exist. SectionDoesNotExist(&'a str), + /// The requested subsection does not exist. SubSectionDoesNotExist(Option<&'a str>), + /// The key does not exist in the requested section. KeyDoesNotExist(&'a str), } @@ -50,10 +53,6 @@ impl<'a> GitConfig<'a> { for event in parser.into_iter() { match event { - e @ Event::Comment(_) => match maybe_section { - Some(ref mut section) => section.push(e), - None => new_self.front_matter_events.push(e), - }, Event::SectionHeader(header) => { new_self.push_section( &mut current_section_name, @@ -73,33 +72,24 @@ impl<'a> GitConfig<'a> { .section_header_separators .insert(SectionId(new_self.section_id_counter), header.separator); } - e @ Event::Key(_) => maybe_section + e @ Event::Key(_) + | e @ Event::Value(_) + | e @ Event::ValueNotDone(_) + | e @ Event::ValueDone(_) => maybe_section .as_mut() .expect("Got a section-only event before a section") .push(e), - e @ Event::Value(_) => maybe_section - .as_mut() - .expect("Got a section-only event before a section") - .push(e), - e @ Event::Newline(_) => match maybe_section { - Some(ref mut section) => section.push(e), - None => new_self.front_matter_events.push(e), - }, - e @ Event::ValueNotDone(_) => maybe_section - .as_mut() - .expect("Got a section-only event before a section") - .push(e), - e @ Event::ValueDone(_) => maybe_section - .as_mut() - .expect("Got a section-only event before a section") - .push(e), - e @ Event::Whitespace(_) => match maybe_section { - Some(ref mut section) => section.push(e), - None => new_self.front_matter_events.push(e), - }, + e @ Event::Comment(_) | e @ Event::Newline(_) | e @ Event::Whitespace(_) => { + match maybe_section { + Some(ref mut section) => section.push(e), + None => new_self.front_matter_events.push(e), + } + } } } + // The last section doesn't get pushed since we only push if there's a + // new section header, so we need to call push one more time. new_self.push_section( &mut current_section_name, &mut current_subsection_name, @@ -178,11 +168,14 @@ impl<'a> GitConfig<'a> { /// ``` /// # use serde_git_config::config::GitConfig; /// # let git_config = GitConfig::from_str("[core]a=b\n[core]\na=c\na=d").unwrap(); - /// assert_eq!(git_config.get_raw_single_value("core", None, "a"), Ok("d")); + /// assert_eq!(git_config.get_raw_value("core", None, "a"), Ok("d")); /// ``` /// - /// The the resolution is as follows - pub fn get_raw_single_value<'b>( + /// # Errors + /// + /// This function will return an error if the key is not in the requested + /// section and subsection. + pub fn get_raw_value<'b>( &self, section_name: &'b str, subsection_name: Option<&'b str>, @@ -191,16 +184,13 @@ impl<'a> GitConfig<'a> { // Note: cannot wrap around the raw_multi_value method because we need // to guarantee that the highest section id is used (so that we follow // the "last one wins" resolution strategy by `git-config`). - let section_id = self - .get_section_id_by_name_and_subname(section_name, subsection_name) - .ok_or(GitConfigError::SubSectionDoesNotExist(subsection_name))?; + let section_id = self.get_section_id_by_name_and_subname(section_name, subsection_name)?; // section_id is guaranteed to exist in self.sections, else we have a // violated invariant. let events = self.sections.get(§ion_id).unwrap(); let mut found_key = false; let mut latest_value = None; - // logic needs fixing for last one wins rule for event in events { match event { Event::Key(event_key) if *event_key == key => found_key = true, @@ -219,10 +209,13 @@ impl<'a> GitConfig<'a> { &'a self, section_name: &'b str, subsection_name: Option<&'b str>, - ) -> Option { + ) -> Result> { self.get_section_ids_by_name_and_subname(section_name, subsection_name) - .map(|vec| vec.into_iter().max()) - .flatten() + .map(|vec| { + // get_section_ids_by_name_and_subname is guaranteed to return + // a non-empty vec, so max can never return empty. + *vec.into_iter().max().unwrap() + }) } pub fn get_raw_multi_value<'b>( @@ -232,26 +225,33 @@ impl<'a> GitConfig<'a> { key: &'b str, ) -> Result, GitConfigError<'b>> { let values = self - .get_section_ids_by_name_and_subname(section_name, subsection_name) - .ok_or(GitConfigError::SubSectionDoesNotExist(subsection_name))? + .get_section_ids_by_name_and_subname(section_name, subsection_name)? .iter() .map(|section_id| { let mut found_key = false; + let mut events = vec![]; // section_id is guaranteed to exist in self.sections, else we have a // violated invariant. for event in self.sections.get(section_id).unwrap() { match event { Event::Key(event_key) if *event_key == key => found_key = true, - Event::Value(v) if found_key => return Ok(*v), + Event::Value(v) if found_key => { + events.push(*v); + found_key = false; + } _ => (), } } - Err(GitConfigError::KeyDoesNotExist(key)) + if events.is_empty() { + Err(GitConfigError::KeyDoesNotExist(key)) + } else { + Ok(events) + } }) .filter_map(Result::ok) + .flatten() .collect::>(); - if values.is_empty() { Err(GitConfigError::KeyDoesNotExist(key)) } else { @@ -263,85 +263,324 @@ impl<'a> GitConfig<'a> { &'a self, section_name: &'b str, subsection_name: Option<&'b str>, - ) -> Option> { - let section_ids = self.section_lookup_tree.get(section_name)?; + ) -> Result<&[SectionId], GitConfigError<'b>> { + let section_ids = self + .section_lookup_tree + .get(section_name) + .ok_or(GitConfigError::SectionDoesNotExist(section_name))?; + let mut maybe_ids = None; + // Don't simplify if and matches here -- the for loop currently needs + // `n + 1` checks, while the if and matches will result in the for loop + // needing `2n` checks. if let Some(subsect_name) = subsection_name { - let mut maybe_ids = None; for node in section_ids { if let LookupTreeNode::NonTerminal(subsection_lookup) = node { maybe_ids = subsection_lookup.get(subsect_name); break; } } - maybe_ids.map(|vec| vec.clone()) } else { - let mut maybe_ids = None; for node in section_ids { if let LookupTreeNode::Terminal(subsection_lookup) = node { - maybe_ids = subsection_lookup.iter().max(); + maybe_ids = Some(subsection_lookup); break; } } - maybe_ids.map(|v| vec![*v]) } + maybe_ids + .map(Vec::as_slice) + .ok_or(GitConfigError::SubSectionDoesNotExist(subsection_name)) } } #[cfg(test)] -mod git_config { - mod from_parser { - use super::super::*; +mod from_parser { + use super::*; - #[test] - fn parse_empty() { - let config = GitConfig::from_str("").unwrap(); - assert!(config.section_header_separators.is_empty()); - assert_eq!(config.section_id_counter, 0); - assert!(config.section_lookup_tree.is_empty()); - assert!(config.sections.is_empty()); - } + #[test] + fn parse_empty() { + let config = GitConfig::from_str("").unwrap(); + assert!(config.section_header_separators.is_empty()); + assert_eq!(config.section_id_counter, 0); + assert!(config.section_lookup_tree.is_empty()); + assert!(config.sections.is_empty()); + } - #[test] - fn parse_single_section() { - let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); - let expected_separators = { - let mut map = HashMap::new(); - map.insert(SectionId(0), None); - map - }; - assert_eq!(config.section_header_separators, expected_separators); - assert_eq!(config.section_id_counter, 1); - let expected_lookup_tree = { - let mut tree = HashMap::new(); - tree.insert("core", vec![LookupTreeNode::Terminal(vec![SectionId(0)])]); - tree - }; - assert_eq!(config.section_lookup_tree, expected_lookup_tree); - let expected_sections = { - let mut sections = HashMap::new(); - sections.insert( - SectionId(0), - vec![ - Event::Newline("\n"), - Event::Key("a"), - Event::Value("b"), - Event::Newline("\n"), - Event::Key("c"), - Event::Value("d"), - ], - ); - sections - }; - assert_eq!(config.sections, expected_sections); - } + #[test] + fn parse_single_section() { + let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + let expected_separators = { + let mut map = HashMap::new(); + map.insert(SectionId(0), None); + map + }; + assert_eq!(config.section_header_separators, expected_separators); + assert_eq!(config.section_id_counter, 1); + let expected_lookup_tree = { + let mut tree = HashMap::new(); + tree.insert("core", vec![LookupTreeNode::Terminal(vec![SectionId(0)])]); + tree + }; + assert_eq!(config.section_lookup_tree, expected_lookup_tree); + let expected_sections = { + let mut sections = HashMap::new(); + sections.insert( + SectionId(0), + vec![ + Event::Newline("\n"), + Event::Key("a"), + Event::Value("b"), + Event::Newline("\n"), + Event::Key("c"), + Event::Value("d"), + ], + ); + sections + }; + assert_eq!(config.sections, expected_sections); + } - #[test] - fn parse_single_subsection() {} + #[test] + fn parse_single_subsection() { + let config = GitConfig::from_str("[core.subsec]\na=b\nc=d").unwrap(); + let expected_separators = { + let mut map = HashMap::new(); + map.insert(SectionId(0), Some(".")); + map + }; + assert_eq!(config.section_header_separators, expected_separators); + assert_eq!(config.section_id_counter, 1); + let expected_lookup_tree = { + let mut tree = HashMap::new(); + let mut inner_tree = HashMap::new(); + inner_tree.insert("subsec", vec![SectionId(0)]); + tree.insert("core", vec![LookupTreeNode::NonTerminal(inner_tree)]); + tree + }; + assert_eq!(config.section_lookup_tree, expected_lookup_tree); + let expected_sections = { + let mut sections = HashMap::new(); + sections.insert( + SectionId(0), + vec![ + Event::Newline("\n"), + Event::Key("a"), + Event::Value("b"), + Event::Newline("\n"), + Event::Key("c"), + Event::Value("d"), + ], + ); + sections + }; + assert_eq!(config.sections, expected_sections); + } - #[test] - fn parse_multiple_sections() {} + #[test] + fn parse_multiple_sections() { + let config = GitConfig::from_str("[core]\na=b\nc=d\n[other]e=f").unwrap(); + let expected_separators = { + let mut map = HashMap::new(); + map.insert(SectionId(0), None); + map.insert(SectionId(1), None); + map + }; + assert_eq!(config.section_header_separators, expected_separators); + assert_eq!(config.section_id_counter, 2); + let expected_lookup_tree = { + let mut tree = HashMap::new(); + tree.insert("core", vec![LookupTreeNode::Terminal(vec![SectionId(0)])]); + tree.insert("other", vec![LookupTreeNode::Terminal(vec![SectionId(1)])]); + tree + }; + assert_eq!(config.section_lookup_tree, expected_lookup_tree); + let expected_sections = { + let mut sections = HashMap::new(); + sections.insert( + SectionId(0), + vec![ + Event::Newline("\n"), + Event::Key("a"), + Event::Value("b"), + Event::Newline("\n"), + Event::Key("c"), + Event::Value("d"), + Event::Newline("\n"), + ], + ); + sections.insert(SectionId(1), vec![Event::Key("e"), Event::Value("f")]); + sections + }; + assert_eq!(config.sections, expected_sections); + } - #[test] - fn parse_multiple_duplicate_sections() {} + #[test] + fn parse_multiple_duplicate_sections() { + let config = GitConfig::from_str("[core]\na=b\nc=d\n[core]e=f").unwrap(); + let expected_separators = { + let mut map = HashMap::new(); + map.insert(SectionId(0), None); + map.insert(SectionId(1), None); + map + }; + assert_eq!(config.section_header_separators, expected_separators); + assert_eq!(config.section_id_counter, 2); + let expected_lookup_tree = { + let mut tree = HashMap::new(); + tree.insert( + "core", + vec![LookupTreeNode::Terminal(vec![SectionId(0), SectionId(1)])], + ); + tree + }; + assert_eq!(config.section_lookup_tree, expected_lookup_tree); + let expected_sections = { + let mut sections = HashMap::new(); + sections.insert( + SectionId(0), + vec![ + Event::Newline("\n"), + Event::Key("a"), + Event::Value("b"), + Event::Newline("\n"), + Event::Key("c"), + Event::Value("d"), + Event::Newline("\n"), + ], + ); + sections.insert(SectionId(1), vec![Event::Key("e"), Event::Value("f")]); + sections + }; + assert_eq!(config.sections, expected_sections); + } +} + +#[cfg(test)] +mod get_raw_value { + use super::*; + + #[test] + fn single_section() { + let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + assert_eq!(config.get_raw_value("core", None, "a"), Ok("b")); + assert_eq!(config.get_raw_value("core", None, "c"), Ok("d")); + } + + #[test] + fn last_one_wins_respected_in_section() { + let config = GitConfig::from_str("[core]\na=b\na=d").unwrap(); + assert_eq!(config.get_raw_value("core", None, "a"), Ok("d")); + } + + #[test] + fn last_one_wins_respected_across_section() { + let config = GitConfig::from_str("[core]\na=b\n[core]\na=d").unwrap(); + assert_eq!(config.get_raw_value("core", None, "a"), Ok("d")); + } + + #[test] + fn section_not_found() { + let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + assert_eq!( + config.get_raw_value("foo", None, "a"), + Err(GitConfigError::SectionDoesNotExist("foo")) + ); + } + + #[test] + fn subsection_not_found() { + let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + assert_eq!( + config.get_raw_value("core", Some("a"), "a"), + Err(GitConfigError::SubSectionDoesNotExist(Some("a"))) + ); + } + + #[test] + fn key_not_found() { + let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + assert_eq!( + config.get_raw_value("core", None, "aaaaaa"), + Err(GitConfigError::KeyDoesNotExist("aaaaaa")) + ); + } + + #[test] + fn subsection_must_be_respected() { + let config = GitConfig::from_str("[core]a=b\n[core.a]a=c").unwrap(); + assert_eq!(config.get_raw_value("core", None, "a"), Ok("b")); + assert_eq!(config.get_raw_value("core", Some("a"), "a"), Ok("c")); + } +} + +#[cfg(test)] +mod get_raw_multi_value { + use super::*; + + #[test] + fn single_value_is_identical_to_single_value_query() { + let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + assert_eq!( + vec![config.get_raw_value("core", None, "a").unwrap()], + config.get_raw_multi_value("core", None, "a").unwrap() + ); + } + + #[test] + fn multi_value_in_section() { + let config = GitConfig::from_str("[core]\na=b\na=c").unwrap(); + assert_eq!( + config.get_raw_multi_value("core", None, "a").unwrap(), + vec!["b", "c"] + ); + } + + #[test] + fn multi_value_across_sections() { + let config = GitConfig::from_str("[core]\na=b\na=c\n[core]a=d").unwrap(); + assert_eq!( + config.get_raw_multi_value("core", None, "a").unwrap(), + vec!["b", "c", "d"] + ); + } + + #[test] + fn section_not_found() { + let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + assert_eq!( + config.get_raw_multi_value("foo", None, "a"), + Err(GitConfigError::SectionDoesNotExist("foo")) + ); + } + + #[test] + fn subsection_not_found() { + let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + assert_eq!( + config.get_raw_multi_value("core", Some("a"), "a"), + Err(GitConfigError::SubSectionDoesNotExist(Some("a"))) + ); + } + + #[test] + fn key_not_found() { + let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + assert_eq!( + config.get_raw_multi_value("core", None, "aaaaaa"), + Err(GitConfigError::KeyDoesNotExist("aaaaaa")) + ); + } + + #[test] + fn subsection_must_be_respected() { + let config = GitConfig::from_str("[core]a=b\n[core.a]a=c").unwrap(); + assert_eq!( + config.get_raw_multi_value("core", None, "a").unwrap(), + vec!["b"] + ); + assert_eq!( + config.get_raw_multi_value("core", Some("a"), "a").unwrap(), + vec!["c"] + ); } } diff --git a/src/parser.rs b/src/parser.rs index 0ea6f04..590a241 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,11 +1,13 @@ //! This module handles parsing a `git-config`. Generally speaking, you want to -//! use a higher abstraction unless you have some explicit reason to work with -//! events instead. +//! use a higher abstraction such as [`GitConfig`] unless you have some explicit +//! reason to work with events instead. //! //! The general workflow for interacting with this is to use one of the //! `parse_from_*` function variants. These will return a [`Parser`] on success, //! which can be converted into an [`Event`] iterator. The [`Parser`] also has //! additional methods for accessing leading comments or events by section. +//! +//! [`GitConfig`]: crate::config::GitConfig use nom::bytes::complete::{escaped, tag, take_till, take_while}; use nom::character::complete::{char, none_of, one_of}; @@ -616,477 +618,475 @@ fn take_common<'a, F: Fn(char) -> bool>(i: &'a str, f: F) -> IResult<&'a str, &' Ok((i, v)) } } + #[cfg(test)] -mod parse { +fn fully_consumed(t: T) -> (&'static str, T) { + ("", t) +} + +#[cfg(test)] +fn gen_section_header( + name: &str, + subsection: impl Into>, +) -> 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, + } + } +} + +#[cfg(test)] +mod comments { use super::*; - fn fully_consumed(t: T) -> (&'static str, T) { - ("", t) + #[test] + fn semicolon() { + assert_eq!( + comment("; this is a semicolon comment").unwrap(), + fully_consumed(ParsedComment { + comment_tag: ';', + comment: " this is a semicolon comment", + }) + ); } - fn gen_section_header( - name: &str, - subsection: impl Into>, - ) -> 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, - } - } + #[test] + fn octothorpe() { + assert_eq!( + comment("# this is an octothorpe comment").unwrap(), + fully_consumed(ParsedComment { + comment_tag: '#', + comment: " this is an octothorpe comment", + }) + ); } - mod comments { - use super::super::*; - use super::*; + #[test] + fn multiple_markers() { + assert_eq!( + comment("###### this is an octothorpe comment").unwrap(), + fully_consumed(ParsedComment { + comment_tag: '#', + comment: "##### this is an octothorpe comment", + }) + ); + } +} - #[test] - fn semicolon() { - assert_eq!( - comment("; this is a semicolon comment").unwrap(), - fully_consumed(ParsedComment { - comment_tag: ';', - comment: " this is a semicolon comment", - }) - ); - } +#[cfg(test)] +mod section_headers { + use super::*; - #[test] - fn octothorpe() { - assert_eq!( - comment("# this is an octothorpe comment").unwrap(), - fully_consumed(ParsedComment { - comment_tag: '#', - comment: " this is an octothorpe comment", - }) - ); - } - - #[test] - fn multiple_markers() { - assert_eq!( - comment("###### this is an octothorpe comment").unwrap(), - fully_consumed(ParsedComment { - comment_tag: '#', - comment: "##### this is an octothorpe comment", - }) - ); - } + #[test] + fn no_subsection() { + assert_eq!( + section_header("[hello]").unwrap(), + fully_consumed(gen_section_header("hello", None)), + ); } - mod section_headers { - use super::super::*; - use super::*; - - #[test] - fn no_subsection() { - assert_eq!( - section_header("[hello]").unwrap(), - fully_consumed(gen_section_header("hello", None)), - ); - } - - #[test] - fn modern_subsection() { - assert_eq!( - section_header(r#"[hello "world"]"#).unwrap(), - fully_consumed(gen_section_header("hello", (" ", "world"))), - ); - } - - #[test] - fn escaped_subsection() { - assert_eq!( - section_header(r#"[hello "foo\\bar\""]"#).unwrap(), - fully_consumed(gen_section_header("hello", (" ", r#"foo\\bar\""#))), - ); - } - - #[test] - fn deprecated_subsection() { - assert_eq!( - section_header(r#"[hello.world]"#).unwrap(), - fully_consumed(gen_section_header("hello", (".", "world"))) - ); - } - - #[test] - fn empty_legacy_subsection_name() { - assert_eq!( - section_header(r#"[hello.]"#).unwrap(), - fully_consumed(gen_section_header("hello", (".", ""))) - ); - } - - #[test] - fn empty_modern_subsection_name() { - assert_eq!( - section_header(r#"[hello ""]"#).unwrap(), - fully_consumed(gen_section_header("hello", (" ", ""))) - ); - } - - #[test] - fn newline_in_header() { - assert!(section_header("[hello\n]").is_err()) - } - - #[test] - fn null_byte_in_header() { - assert!(section_header("[hello\0]").is_err()) - } - - #[test] - fn right_brace_in_subsection_name() { - assert_eq!( - section_header(r#"[hello "]"]"#).unwrap(), - fully_consumed(gen_section_header("hello", (" ", "]"))) - ); - } + #[test] + fn modern_subsection() { + assert_eq!( + section_header(r#"[hello "world"]"#).unwrap(), + fully_consumed(gen_section_header("hello", (" ", "world"))), + ); } - mod config_name { - use super::super::*; - use super::*; - - #[test] - fn just_name() { - assert_eq!(config_name("name").unwrap(), fully_consumed("name")); - } - - #[test] - fn must_start_with_alphabetic() { - assert!(config_name("4aaa").is_err()); - assert!(config_name("-aaa").is_err()); - } - - #[test] - fn cannot_be_empty() { - assert!(config_name("").is_err()) - } + #[test] + fn escaped_subsection() { + assert_eq!( + section_header(r#"[hello "foo\\bar\""]"#).unwrap(), + fully_consumed(gen_section_header("hello", (" ", r#"foo\\bar\""#))), + ); } - mod value_no_continuation { - use super::super::*; - use super::*; - - #[test] - fn no_comment() { - assert_eq!( - value_impl("hello").unwrap(), - fully_consumed(vec![Event::Value("hello")]) - ); - } - - #[test] - fn no_comment_newline() { - assert_eq!( - value_impl("hello\na").unwrap(), - ("\na", vec![Event::Value("hello")]) - ) - } - - #[test] - fn semicolon_comment_not_consumed() { - assert_eq!( - value_impl("hello;world").unwrap(), - (";world", vec![Event::Value("hello"),]) - ); - } - - #[test] - fn octothorpe_comment_not_consumed() { - assert_eq!( - value_impl("hello#world").unwrap(), - ("#world", vec![Event::Value("hello"),]) - ); - } - - #[test] - fn values_with_extraneous_whitespace_without_comment() { - assert_eq!( - value_impl("hello ").unwrap(), - (" ", vec![Event::Value("hello")]) - ); - } - - #[test] - fn values_with_extraneous_whitespace_before_comment() { - assert_eq!( - value_impl("hello #world").unwrap(), - (" #world", vec![Event::Value("hello"),]) - ); - assert_eq!( - value_impl("hello ;world").unwrap(), - (" ;world", vec![Event::Value("hello"),]) - ); - } - - #[test] - fn trans_escaped_comment_marker_not_consumed() { - assert_eq!( - value_impl(r##"hello"#"world; a"##).unwrap(), - ("; a", vec![Event::Value(r##"hello"#"world"##)]) - ); - } - - #[test] - fn complex_test() { - assert_eq!( - value_impl(r#"value";";ahhhh"#).unwrap(), - (";ahhhh", vec![Event::Value(r#"value";""#)]) - ); - } - - #[test] - fn garbage_after_continution_is_err() { - assert!(value_impl("hello \\afwjdls").is_err()); - } + #[test] + fn deprecated_subsection() { + assert_eq!( + section_header(r#"[hello.world]"#).unwrap(), + fully_consumed(gen_section_header("hello", (".", "world"))) + ); } - mod value_continuation { - use super::super::*; - use super::*; + #[test] + fn empty_legacy_subsection_name() { + assert_eq!( + section_header(r#"[hello.]"#).unwrap(), + fully_consumed(gen_section_header("hello", (".", ""))) + ); + } - #[test] - fn simple_continuation() { - assert_eq!( - value_impl("hello\\\nworld").unwrap(), - fully_consumed(vec![ - Event::ValueNotDone("hello"), + #[test] + fn empty_modern_subsection_name() { + assert_eq!( + section_header(r#"[hello ""]"#).unwrap(), + fully_consumed(gen_section_header("hello", (" ", ""))) + ); + } + + #[test] + fn newline_in_header() { + assert!(section_header("[hello\n]").is_err()) + } + + #[test] + fn null_byte_in_header() { + assert!(section_header("[hello\0]").is_err()) + } + + #[test] + fn right_brace_in_subsection_name() { + assert_eq!( + section_header(r#"[hello "]"]"#).unwrap(), + fully_consumed(gen_section_header("hello", (" ", "]"))) + ); + } +} + +#[cfg(test)] +mod config_name { + use super::*; + + #[test] + fn just_name() { + assert_eq!(config_name("name").unwrap(), fully_consumed("name")); + } + + #[test] + fn must_start_with_alphabetic() { + assert!(config_name("4aaa").is_err()); + assert!(config_name("-aaa").is_err()); + } + + #[test] + fn cannot_be_empty() { + assert!(config_name("").is_err()) + } +} + +#[cfg(test)] +mod value_no_continuation { + use super::*; + + #[test] + fn no_comment() { + assert_eq!( + value_impl("hello").unwrap(), + fully_consumed(vec![Event::Value("hello")]) + ); + } + + #[test] + fn no_comment_newline() { + assert_eq!( + value_impl("hello\na").unwrap(), + ("\na", vec![Event::Value("hello")]) + ) + } + + #[test] + fn semicolon_comment_not_consumed() { + assert_eq!( + value_impl("hello;world").unwrap(), + (";world", vec![Event::Value("hello"),]) + ); + } + + #[test] + fn octothorpe_comment_not_consumed() { + assert_eq!( + value_impl("hello#world").unwrap(), + ("#world", vec![Event::Value("hello"),]) + ); + } + + #[test] + fn values_with_extraneous_whitespace_without_comment() { + assert_eq!( + value_impl("hello ").unwrap(), + (" ", vec![Event::Value("hello")]) + ); + } + + #[test] + fn values_with_extraneous_whitespace_before_comment() { + assert_eq!( + value_impl("hello #world").unwrap(), + (" #world", vec![Event::Value("hello"),]) + ); + assert_eq!( + value_impl("hello ;world").unwrap(), + (" ;world", vec![Event::Value("hello"),]) + ); + } + + #[test] + fn trans_escaped_comment_marker_not_consumed() { + assert_eq!( + value_impl(r##"hello"#"world; a"##).unwrap(), + ("; a", vec![Event::Value(r##"hello"#"world"##)]) + ); + } + + #[test] + fn complex_test() { + assert_eq!( + value_impl(r#"value";";ahhhh"#).unwrap(), + (";ahhhh", vec![Event::Value(r#"value";""#)]) + ); + } + + #[test] + fn garbage_after_continution_is_err() { + assert!(value_impl("hello \\afwjdls").is_err()); + } +} + +#[cfg(test)] +mod value_continuation { + use super::*; + + #[test] + fn simple_continuation() { + assert_eq!( + value_impl("hello\\\nworld").unwrap(), + fully_consumed(vec![ + Event::ValueNotDone("hello"), + Event::Newline("\n"), + Event::ValueDone("world") + ]) + ); + } + + #[test] + fn continuation_with_whitespace() { + assert_eq!( + value_impl("hello\\\n world").unwrap(), + fully_consumed(vec![ + Event::ValueNotDone("hello"), + Event::Newline("\n"), + Event::ValueDone(" world") + ]) + ) + } + + #[test] + fn complex_continuation_with_leftover_comment() { + assert_eq!( + value_impl("1 \"\\\"\\\na ; e \"\\\"\\\nd # \"b\t ; c").unwrap(), + ( + " # \"b\t ; c", + vec![ + Event::ValueNotDone(r#"1 "\""#), Event::Newline("\n"), - Event::ValueDone("world") - ]) - ); - } - - #[test] - fn continuation_with_whitespace() { - assert_eq!( - value_impl("hello\\\n world").unwrap(), - fully_consumed(vec![ - Event::ValueNotDone("hello"), + Event::ValueNotDone(r#"a ; e "\""#), Event::Newline("\n"), - Event::ValueDone(" world") - ]) + Event::ValueDone("d"), + ] ) - } - - #[test] - fn complex_continuation_with_leftover_comment() { - assert_eq!( - value_impl("1 \"\\\"\\\na ; e \"\\\"\\\nd # \"b\t ; c").unwrap(), - ( - " # \"b\t ; c", - vec![ - Event::ValueNotDone(r#"1 "\""#), - Event::Newline("\n"), - Event::ValueNotDone(r#"a ; e "\""#), - Event::Newline("\n"), - Event::ValueDone("d"), - ] - ) - ); - } - - #[test] - fn quote_split_over_two_lines_with_leftover_comment() { - assert_eq!( - value_impl("\"\\\n;\";a").unwrap(), - ( - ";a", - vec![ - Event::ValueNotDone("\""), - Event::Newline("\n"), - Event::ValueDone(";\""), - ] - ) - ) - } + ); } - mod section { - use super::super::*; - use super::*; + #[test] + fn quote_split_over_two_lines_with_leftover_comment() { + assert_eq!( + value_impl("\"\\\n;\";a").unwrap(), + ( + ";a", + vec![ + Event::ValueNotDone("\""), + Event::Newline("\n"), + Event::ValueDone(";\""), + ] + ) + ) + } +} - #[test] - fn empty_section() { - assert_eq!( - section("[test]").unwrap(), - fully_consumed(ParsedSection { - section_header: gen_section_header("test", None), - events: vec![] - }) - ); - } +#[cfg(test)] +mod section { + use super::*; - #[test] - fn simple_section() { - let section_data = r#"[hello] + #[test] + fn empty_section() { + assert_eq!( + section("[test]").unwrap(), + fully_consumed(ParsedSection { + section_header: gen_section_header("test", None), + events: vec![] + }) + ); + } + + #[test] + fn simple_section() { + let section_data = r#"[hello] a = b c d = "lol""#; - assert_eq!( - section(section_data).unwrap(), - fully_consumed(ParsedSection { - section_header: gen_section_header("hello", None), - events: vec![ - Event::Newline("\n"), - Event::Whitespace(" "), - Event::Key("a"), - Event::Whitespace(" "), - Event::Whitespace(" "), - Event::Value("b"), - Event::Newline("\n"), - Event::Whitespace(" "), - Event::Key("c"), - Event::Value(""), - Event::Newline("\n"), - Event::Whitespace(" "), - Event::Key("d"), - Event::Whitespace(" "), - Event::Whitespace(" "), - Event::Value("\"lol\"") - ] - }) - ) - } + assert_eq!( + section(section_data).unwrap(), + fully_consumed(ParsedSection { + section_header: gen_section_header("hello", None), + events: vec![ + Event::Newline("\n"), + Event::Whitespace(" "), + Event::Key("a"), + Event::Whitespace(" "), + Event::Whitespace(" "), + Event::Value("b"), + Event::Newline("\n"), + Event::Whitespace(" "), + Event::Key("c"), + Event::Value(""), + Event::Newline("\n"), + Event::Whitespace(" "), + Event::Key("d"), + Event::Whitespace(" "), + Event::Whitespace(" "), + Event::Value("\"lol\"") + ] + }) + ) + } - #[test] - fn section_single_line() { - assert_eq!( - section("[hello] c").unwrap(), - fully_consumed(ParsedSection { - section_header: gen_section_header("hello", None), - events: vec![Event::Whitespace(" "), Event::Key("c"), Event::Value("")] - }) - ); - } + #[test] + fn section_single_line() { + assert_eq!( + section("[hello] c").unwrap(), + fully_consumed(ParsedSection { + section_header: gen_section_header("hello", None), + events: vec![Event::Whitespace(" "), Event::Key("c"), Event::Value("")] + }) + ); + } - #[test] - fn section_very_commented() { - let section_data = r#"[hello] ; commentA + #[test] + fn section_very_commented() { + let section_data = r#"[hello] ; commentA a = b # commentB ; commentC ; commentD c = d"#; - assert_eq!( - section(section_data).unwrap(), - fully_consumed(ParsedSection { - section_header: gen_section_header("hello", None), - events: vec![ - Event::Whitespace(" "), - Event::Comment(ParsedComment { - comment_tag: ';', - comment: " commentA", - }), - Event::Newline("\n"), - Event::Whitespace(" "), - Event::Key("a"), - Event::Whitespace(" "), - Event::Whitespace(" "), - Event::Value("b"), - Event::Whitespace(" "), - Event::Comment(ParsedComment { - comment_tag: '#', - comment: " commentB", - }), - Event::Newline("\n"), - Event::Whitespace(" "), - Event::Comment(ParsedComment { - comment_tag: ';', - comment: " commentC", - }), - Event::Newline("\n"), - Event::Whitespace(" "), - Event::Comment(ParsedComment { - comment_tag: ';', - comment: " commentD", - }), - Event::Newline("\n"), - Event::Whitespace(" "), - Event::Key("c"), - Event::Whitespace(" "), - Event::Whitespace(" "), - Event::Value("d"), - ] - }) - ); - } + assert_eq!( + section(section_data).unwrap(), + fully_consumed(ParsedSection { + section_header: gen_section_header("hello", None), + events: vec![ + Event::Whitespace(" "), + Event::Comment(ParsedComment { + comment_tag: ';', + comment: " commentA", + }), + Event::Newline("\n"), + Event::Whitespace(" "), + Event::Key("a"), + Event::Whitespace(" "), + Event::Whitespace(" "), + Event::Value("b"), + Event::Whitespace(" "), + Event::Comment(ParsedComment { + comment_tag: '#', + comment: " commentB", + }), + Event::Newline("\n"), + Event::Whitespace(" "), + Event::Comment(ParsedComment { + comment_tag: ';', + comment: " commentC", + }), + Event::Newline("\n"), + Event::Whitespace(" "), + Event::Comment(ParsedComment { + comment_tag: ';', + comment: " commentD", + }), + Event::Newline("\n"), + Event::Whitespace(" "), + Event::Key("c"), + Event::Whitespace(" "), + Event::Whitespace(" "), + Event::Value("d"), + ] + }) + ); + } - #[test] - fn complex_continuation() { - // This test is absolute hell. Good luck if this fails. - assert_eq!( - section("[section] a = 1 \"\\\"\\\na ; e \"\\\"\\\nd # \"b\t ; c").unwrap(), - fully_consumed(ParsedSection { - section_header: gen_section_header("section", None), - events: vec![ - Event::Whitespace(" "), - Event::Key("a"), - Event::Whitespace(" "), - Event::Whitespace(" "), - Event::ValueNotDone(r#"1 "\""#), - Event::Newline("\n"), - Event::ValueNotDone(r#"a ; e "\""#), - Event::Newline("\n"), - Event::ValueDone("d"), - Event::Whitespace(" "), - Event::Comment(ParsedComment { - comment_tag: '#', - comment: " \"b\t ; c" - }) - ] - }) - ); - } + #[test] + fn complex_continuation() { + // This test is absolute hell. Good luck if this fails. + assert_eq!( + section("[section] a = 1 \"\\\"\\\na ; e \"\\\"\\\nd # \"b\t ; c").unwrap(), + fully_consumed(ParsedSection { + section_header: gen_section_header("section", None), + events: vec![ + Event::Whitespace(" "), + Event::Key("a"), + Event::Whitespace(" "), + Event::Whitespace(" "), + Event::ValueNotDone(r#"1 "\""#), + Event::Newline("\n"), + Event::ValueNotDone(r#"a ; e "\""#), + Event::Newline("\n"), + Event::ValueDone("d"), + Event::Whitespace(" "), + Event::Comment(ParsedComment { + comment_tag: '#', + comment: " \"b\t ; c" + }) + ] + }) + ); + } - #[test] - fn quote_split_over_two_lines() { - assert_eq!( - section("[section \"a\"] b =\"\\\n;\";a").unwrap(), - fully_consumed(ParsedSection { - section_header: gen_section_header("section", (" ", "a")), - events: vec![ - Event::Whitespace(" "), - Event::Key("b"), - Event::Whitespace(" "), - Event::ValueNotDone("\""), - Event::Newline("\n"), - Event::ValueDone(";\""), - Event::Comment(ParsedComment { - comment_tag: ';', - comment: "a", - }) - ] - }) - ) - } + #[test] + fn quote_split_over_two_lines() { + assert_eq!( + section("[section \"a\"] b =\"\\\n;\";a").unwrap(), + fully_consumed(ParsedSection { + section_header: gen_section_header("section", (" ", "a")), + events: vec![ + Event::Whitespace(" "), + Event::Key("b"), + Event::Whitespace(" "), + Event::ValueNotDone("\""), + Event::Newline("\n"), + Event::ValueDone(";\""), + Event::Comment(ParsedComment { + comment_tag: ';', + comment: "a", + }) + ] + }) + ) + } - #[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(""), - Event::Comment(ParsedComment { - comment_tag: '#', - comment: "world", - }), - ] - }) - ); - } + #[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(""), + Event::Comment(ParsedComment { + comment_tag: '#', + comment: "world", + }), + ] + }) + ); } }