diff --git a/src/config.rs b/src/config.rs index f99c91a..16a07d4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,10 @@ -use crate::parser::{parse_from_str, Event, ParsedSectionHeader, Parser, ParserError}; +use crate::parser::{parse_from_bytes, Event, ParsedSectionHeader, Parser, ParserError}; use bstr::BStr; use std::collections::{HashMap, VecDeque}; +use std::convert::TryFrom; use std::{borrow::Cow, fmt::Display}; -#[derive(Debug, PartialEq, Eq)] +#[derive(PartialEq, Eq, Hash, Copy, Clone, PartialOrd, Ord, Debug)] pub enum GitConfigError<'a> { /// The requested section does not exist. SectionDoesNotExist(&'a BStr), @@ -13,21 +14,6 @@ pub enum GitConfigError<'a> { KeyDoesNotExist(&'a BStr), } -/// High level `git-config` reader and writer. -pub struct GitConfig<'a> { - /// The list of events that occur before an actual section. Since a - /// `git-config` file prohibits global values, this vec is limited to only - /// comment, newline, and whitespace events. - front_matter_events: Vec>, - section_lookup_tree: HashMap, Vec>>, - /// SectionId to section mapping. The value of this HashMap contains actual - /// events - sections: HashMap>>, - section_headers: HashMap>, - section_id_counter: usize, - section_order: VecDeque, -} - /// The section ID is a monotonically increasing ID used to refer to sections. /// This value does not imply any ordering between sections, as new sections /// with higher section IDs may be in between lower ID sections. @@ -42,85 +28,28 @@ pub struct GitConfig<'a> { #[derive(PartialEq, Eq, Hash, Copy, Clone, PartialOrd, Ord, Debug)] struct SectionId(usize); -#[derive(Debug, PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone, Debug)] enum LookupTreeNode<'a> { Terminal(Vec), NonTerminal(HashMap, Vec>), } +/// High level `git-config` reader and writer. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct GitConfig<'a> { + /// The list of events that occur before an actual section. Since a + /// `git-config` file prohibits global values, this vec is limited to only + /// comment, newline, and whitespace events. + front_matter_events: Vec>, + section_lookup_tree: HashMap, Vec>>, + /// SectionId to section mapping. The value of this HashMap contains actual + /// events + sections: HashMap>>, + section_headers: HashMap>, + section_id_counter: usize, + section_order: VecDeque, +} impl<'a> GitConfig<'a> { - /// Convenience constructor. Attempts to parse the provided string into a - /// [`GitConfig`]. See [`parse_from_str`] for more information. - /// - /// [`parse_from_str`]: crate::parser::parse_from_str - pub fn from_str(str: &'a str) -> Result { - Ok(Self::from_parser(parse_from_str(str)?)) - } - - pub fn from_parser(parser: Parser<'a>) -> Self { - let mut new_self = Self { - front_matter_events: vec![], - sections: HashMap::new(), - section_lookup_tree: HashMap::new(), - section_headers: HashMap::new(), - section_id_counter: 0, - section_order: VecDeque::new(), - }; - - // Current section that we're building - let mut current_section_name: Option> = None; - let mut current_subsection_name: Option> = None; - let mut maybe_section: Option>> = None; - - for event in parser.into_iter() { - match event { - Event::SectionHeader(header) => { - new_self.push_section( - current_section_name, - current_subsection_name, - &mut maybe_section, - ); - - // Initialize new section - // We need to store the new, current id counter, so don't - // use new_section_id here and use the already incremented - // section id value. - new_self - .section_headers - .insert(SectionId(new_self.section_id_counter), header.clone()); - let (name, subname) = (header.name, header.subsection_name); - maybe_section = Some(vec![]); - current_section_name = Some(name); - current_subsection_name = subname; - } - e @ Event::Key(_) - | e @ Event::Value(_) - | e @ Event::ValueNotDone(_) - | e @ Event::ValueDone(_) - | e @ Event::KeyValueSeparator => maybe_section - .as_mut() - .expect("Got a section-only event before a section") - .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( - current_section_name, - current_subsection_name, - &mut maybe_section, - ); - - new_self - } - fn push_section( &mut self, current_section_name: Option>, @@ -574,9 +503,93 @@ impl<'a> GitConfig<'a> { } } +impl<'a> TryFrom<&'a str> for GitConfig<'a> { + type Error = ParserError<'a>; + + /// Convenience constructor. Attempts to parse the provided string into a + /// [`GitConfig`]. See [`parse_from_str`] for more information. + /// + /// [`parse_from_str`]: crate::parser::parse_from_str + fn try_from(s: &'a str) -> Result { + parse_from_bytes(s.as_bytes()).map(Self::from) + } +} + +impl<'a> TryFrom<&'a BStr> for GitConfig<'a> { + type Error = ParserError<'a>; + + /// Convenience constructor. Attempts to parse the provided byte string into + //// a [`GitConfig`]. See [`parse_from_bytes`] for more information. + /// + /// [`parse_from_bytes`]: crate::parser::parse_from_bytes + fn try_from(value: &'a BStr) -> Result { + parse_from_bytes(value).map(Self::from) + } +} + impl<'a> From> for GitConfig<'a> { - fn from(p: Parser<'a>) -> Self { - Self::from_parser(p) + fn from(parser: Parser<'a>) -> Self { + let mut new_self = Self { + front_matter_events: vec![], + sections: HashMap::new(), + section_lookup_tree: HashMap::new(), + section_headers: HashMap::new(), + section_id_counter: 0, + section_order: VecDeque::new(), + }; + + // Current section that we're building + let mut current_section_name: Option> = None; + let mut current_subsection_name: Option> = None; + let mut maybe_section: Option>> = None; + + for event in parser.into_iter() { + match event { + Event::SectionHeader(header) => { + new_self.push_section( + current_section_name, + current_subsection_name, + &mut maybe_section, + ); + + // Initialize new section + // We need to store the new, current id counter, so don't + // use new_section_id here and use the already incremented + // section id value. + new_self + .section_headers + .insert(SectionId(new_self.section_id_counter), header.clone()); + let (name, subname) = (header.name, header.subsection_name); + maybe_section = Some(vec![]); + current_section_name = Some(name); + current_subsection_name = subname; + } + e @ Event::Key(_) + | e @ Event::Value(_) + | e @ Event::ValueNotDone(_) + | e @ Event::ValueDone(_) + | e @ Event::KeyValueSeparator => maybe_section + .as_mut() + .expect("Got a section-only event before a section") + .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( + current_section_name, + current_subsection_name, + &mut maybe_section, + ); + + new_self } } @@ -606,7 +619,7 @@ mod from_parser { #[test] fn parse_empty() { - let config = GitConfig::from_str("").unwrap(); + let config = GitConfig::try_from("").unwrap(); assert!(config.section_headers.is_empty()); assert_eq!(config.section_id_counter, 0); assert!(config.section_lookup_tree.is_empty()); @@ -616,7 +629,7 @@ mod from_parser { #[test] fn parse_single_section() { - let mut config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + let mut config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); let expected_separators = { let mut map = HashMap::new(); map.insert(SectionId(0), section_header("core", None)); @@ -656,7 +669,7 @@ mod from_parser { #[test] fn parse_single_subsection() { - let mut config = GitConfig::from_str("[core.subsec]\na=b\nc=d").unwrap(); + let mut config = GitConfig::try_from("[core.subsec]\na=b\nc=d").unwrap(); let expected_separators = { let mut map = HashMap::new(); map.insert(SectionId(0), section_header("core", (".", "subsec"))); @@ -698,7 +711,7 @@ mod from_parser { #[test] fn parse_multiple_sections() { - let mut config = GitConfig::from_str("[core]\na=b\nc=d\n[other]e=f").unwrap(); + let mut config = GitConfig::try_from("[core]\na=b\nc=d\n[other]e=f").unwrap(); let expected_separators = { let mut map = HashMap::new(); map.insert(SectionId(0), section_header("core", None)); @@ -751,7 +764,7 @@ mod from_parser { #[test] fn parse_multiple_duplicate_sections() { - let mut config = GitConfig::from_str("[core]\na=b\nc=d\n[core]e=f").unwrap(); + let mut config = GitConfig::try_from("[core]\na=b\nc=d\n[core]e=f").unwrap(); let expected_separators = { let mut map = HashMap::new(); map.insert(SectionId(0), section_header("core", None)); @@ -805,7 +818,7 @@ mod get_raw_value { #[test] fn single_section() { - let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); assert_eq!( config.get_raw_value("core", None, "a"), Ok(&Cow::Borrowed("b".into())) @@ -818,7 +831,7 @@ mod get_raw_value { #[test] fn last_one_wins_respected_in_section() { - let config = GitConfig::from_str("[core]\na=b\na=d").unwrap(); + let config = GitConfig::try_from("[core]\na=b\na=d").unwrap(); assert_eq!( config.get_raw_value("core", None, "a"), Ok(&Cow::Borrowed("d".into())) @@ -827,7 +840,7 @@ mod get_raw_value { #[test] fn last_one_wins_respected_across_section() { - let config = GitConfig::from_str("[core]\na=b\n[core]\na=d").unwrap(); + let config = GitConfig::try_from("[core]\na=b\n[core]\na=d").unwrap(); assert_eq!( config.get_raw_value("core", None, "a"), Ok(&Cow::Borrowed("d".into())) @@ -836,7 +849,7 @@ mod get_raw_value { #[test] fn section_not_found() { - let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); assert_eq!( config.get_raw_value("foo", None, "a"), Err(GitConfigError::SectionDoesNotExist("foo".into())) @@ -845,7 +858,7 @@ mod get_raw_value { #[test] fn subsection_not_found() { - let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); assert_eq!( config.get_raw_value("core", Some("a"), "a"), Err(GitConfigError::SubSectionDoesNotExist(Some("a".into()))) @@ -854,7 +867,7 @@ mod get_raw_value { #[test] fn key_not_found() { - let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); assert_eq!( config.get_raw_value("core", None, "aaaaaa"), Err(GitConfigError::KeyDoesNotExist("aaaaaa".into())) @@ -863,7 +876,7 @@ mod get_raw_value { #[test] fn subsection_must_be_respected() { - let config = GitConfig::from_str("[core]a=b\n[core.a]a=c").unwrap(); + let config = GitConfig::try_from("[core]a=b\n[core.a]a=c").unwrap(); assert_eq!( config.get_raw_value("core", None, "a"), Ok(&Cow::Borrowed("b".into())) @@ -881,7 +894,7 @@ mod get_raw_multi_value { #[test] fn single_value_is_identical_to_single_value_query() { - let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + let config = GitConfig::try_from("[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() @@ -890,7 +903,7 @@ mod get_raw_multi_value { #[test] fn multi_value_in_section() { - let config = GitConfig::from_str("[core]\na=b\na=c").unwrap(); + let config = GitConfig::try_from("[core]\na=b\na=c").unwrap(); assert_eq!( config.get_raw_multi_value("core", None, "a").unwrap(), vec![&Cow::Borrowed("b"), &Cow::Borrowed("c")] @@ -899,7 +912,7 @@ mod get_raw_multi_value { #[test] fn multi_value_across_sections() { - let config = GitConfig::from_str("[core]\na=b\na=c\n[core]a=d").unwrap(); + let config = GitConfig::try_from("[core]\na=b\na=c\n[core]a=d").unwrap(); assert_eq!( config.get_raw_multi_value("core", None, "a").unwrap(), vec![ @@ -912,7 +925,7 @@ mod get_raw_multi_value { #[test] fn section_not_found() { - let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); assert_eq!( config.get_raw_multi_value("foo", None, "a"), Err(GitConfigError::SectionDoesNotExist("foo".into())) @@ -921,7 +934,7 @@ mod get_raw_multi_value { #[test] fn subsection_not_found() { - let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); assert_eq!( config.get_raw_multi_value("core", Some("a"), "a"), Err(GitConfigError::SubSectionDoesNotExist(Some("a".into()))) @@ -930,7 +943,7 @@ mod get_raw_multi_value { #[test] fn key_not_found() { - let config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); + let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); assert_eq!( config.get_raw_multi_value("core", None, "aaaaaa"), Err(GitConfigError::KeyDoesNotExist("aaaaaa".into())) @@ -939,7 +952,7 @@ mod get_raw_multi_value { #[test] fn subsection_must_be_respected() { - let config = GitConfig::from_str("[core]a=b\n[core.a]a=c").unwrap(); + let config = GitConfig::try_from("[core]a=b\n[core.a]a=c").unwrap(); assert_eq!( config.get_raw_multi_value("core", None, "a").unwrap(), vec![&Cow::Borrowed("b")] @@ -952,7 +965,7 @@ mod get_raw_multi_value { #[test] fn non_relevant_subsection_is_ignored() { - let config = GitConfig::from_str("[core]\na=b\na=c\n[core]a=d\n[core]g=g").unwrap(); + let config = GitConfig::try_from("[core]\na=b\na=c\n[core]a=d\n[core]g=g").unwrap(); assert_eq!( config.get_raw_multi_value("core", None, "a").unwrap(), vec![ @@ -973,7 +986,7 @@ mod display { let config = r#" "#; - assert_eq!(GitConfig::from_str(config).unwrap().to_string(), config); + assert_eq!(GitConfig::try_from(config).unwrap().to_string(), config); } #[test] @@ -997,7 +1010,7 @@ mod display { [init] defaultBranch = master"#; - assert_eq!(GitConfig::from_str(config).unwrap().to_string(), config); + assert_eq!(GitConfig::try_from(config).unwrap().to_string(), config); } #[test] @@ -1012,7 +1025,7 @@ mod display { [commit] gpgsign"#; - assert_eq!(GitConfig::from_str(config).unwrap().to_string(), config); + assert_eq!(GitConfig::try_from(config).unwrap().to_string(), config); } #[test] @@ -1028,6 +1041,6 @@ mod display { [init] defaultBranch = master"#; - assert_eq!(GitConfig::from_str(config).unwrap().to_string(), config); + assert_eq!(GitConfig::try_from(config).unwrap().to_string(), config); } } diff --git a/src/parser.rs b/src/parser.rs index bef9b11..85df39c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -19,9 +19,9 @@ use nom::error::{Error as NomError, ErrorKind}; use nom::multi::{many0, many1}; use nom::sequence::delimited; use nom::IResult; -use std::fmt::Display; use std::iter::FusedIterator; use std::{borrow::Cow, error::Error}; +use std::{convert::TryFrom, fmt::Display}; /// Syntactic events that occurs in the config. Despite all these variants /// holding a [`Cow`] instead over a simple reference, the parser will only emit @@ -446,36 +446,6 @@ pub struct Parser<'a> { } impl<'a> Parser<'a> { - /// Attempt to zero-copy parse the provided `&str`. On success, returns a - /// [`Parser`] that provides methods to accessing leading comments and - /// sections of a `git-config` file and can be converted into an iterator of - /// [`Event`] for higher level processing. - /// - /// This function is identical to [`parse_from_str`]. - /// - /// # Errors - /// - /// Returns an error if the string provided is not a valid `git-config`. - /// This generally is due to either invalid names or if there's extraneous - /// data succeeding valid `git-config` data. - pub fn from_str(s: &'a str) -> Result { - parse_from_str(s) - } - - /// Attempt to zero-copy parse the provided bytes. On success, returns a - /// [`Parser`] that provides methods to accessing leading comments and - /// sections of a `git-config` file and can be converted into an iterator of - /// [`Event`] for higher level processing. - /// - /// # Errors - /// - /// Returns an error if the string provided is not a valid `git-config`. - /// This generally is due to either invalid names or if there's extraneous - /// data succeeding valid `git-config` data. - pub fn from_bytes(s: impl Into<&'a BStr>) -> Result> { - parse_from_bytes(s.into()) - } - /// Returns the leading events (any comments, whitespace, or newlines before /// a section) from the parser. Consider [`Parser::take_frontmatter`] if /// you need an owned copy only once. If that function was called, then this @@ -530,6 +500,22 @@ impl<'a> Parser<'a> { } } +impl<'a> TryFrom<&'a str> for Parser<'a> { + type Error = ParserError<'a>; + + fn try_from(value: &'a str) -> Result { + parse_from_str(value) + } +} + +impl<'a> TryFrom<&'a [u8]> for Parser<'a> { + type Error = ParserError<'a>; + + fn try_from(value: &'a [u8]) -> Result { + parse_from_bytes(value) + } +} + /// Attempt to zero-copy parse the provided `&str`. On success, returns a /// [`Parser`] that provides methods to accessing leading comments and sections /// of a `git-config` file and can be converted into an iterator of [`Event`]