diff --git a/src/config.rs b/src/config.rs index 16a07d4..960c6e4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,11 @@ 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}; +use std::{ + collections::{HashMap, VecDeque}, + error::Error, +}; #[derive(PartialEq, Eq, Hash, Copy, Clone, PartialOrd, Ord, Debug)] pub enum GitConfigError<'a> { @@ -12,8 +15,23 @@ pub enum GitConfigError<'a> { SubSectionDoesNotExist(Option<&'a BStr>), /// The key does not exist in the requested section. KeyDoesNotExist(&'a BStr), + FailedConversion, } +impl Display for GitConfigError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + // Todo, try parse as utf8 first for better looking errors + Self::SectionDoesNotExist(s) => write!(f, "Subsection '{}' does not exist.", s), + Self::SubSectionDoesNotExist(s) => write!(f, "Subsection '{:?}' does not exist.", s), + Self::KeyDoesNotExist(k) => write!(f, "Name '{}' does not exist.", k), + Self::FailedConversion => write!(f, "Failed to convert to specified type."), + } + } +} + +impl Error for GitConfigError<'_> {} + /// 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. @@ -101,8 +119,12 @@ impl<'a> GitConfig<'a> { } } - /// Returns an uninterpreted value given a section, an optional subsection - /// and key. + /// Returns an interpreted value given a section, an optional subsection and + /// key. + /// + /// It's recommended to use one of the values in the [`values`] module as + /// the conversion is already implemented, but this function is flexible and + /// will accept any type that implements [`TryFrom<&[u8]>`][`TryFrom`]. /// /// # Multivar behavior /// @@ -131,13 +153,110 @@ impl<'a> GitConfig<'a> { /// ``` /// # use git_config::config::GitConfig; /// # use std::borrow::Cow; - /// # let git_config = GitConfig::from_str("[core]a=b\n[core]\na=c\na=d").unwrap(); + /// # use std::convert::TryFrom; + /// # let git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); /// assert_eq!(git_config.get_raw_value("core", None, "a"), Ok(&Cow::Borrowed("d".into()))); /// ``` /// /// Consider [`Self::get_raw_multi_value`] if you want to get all values of /// a multivar instead. /// + /// # Examples + /// + /// + /// Fetching a config value + /// ``` + /// # use git_config::config::{GitConfig, GitConfigError}; + /// # use git_config::values::{Integer, Value, Boolean}; + /// # use std::borrow::Cow; + /// # use std::convert::TryFrom; + /// let config = r#" + /// [core] + /// a = 10k + /// c + /// "#; + /// let git_config = GitConfig::try_from(config).unwrap(); + /// // You can either use the turbofish to determine the type... + /// let a_value = git_config.get_value::("core", None, "a")?; + /// // ... or explicitly declare the type to avoid the turbofish + /// let c_value: Boolean = git_config.get_value("core", None, "c")?; + /// # Ok::<(), GitConfigError>(()) + /// ``` + /// + /// # Errors + /// + /// This function will return an error if the key is not in the requested + /// section and subsection, if the section and subsection do not exist, or + /// if there was an issue converting the type into the requested variant. + /// + /// [`values`]: crate::values + /// [`TryFrom`]: std::convert::TryFrom + pub fn get_value<'b, 'c, T: TryFrom<&'c [u8]>, S: Into<&'b BStr>>( + &'c self, + section_name: S, + subsection_name: Option, + key: S, + ) -> Result> { + T::try_from(self.get_raw_value(section_name, subsection_name, key)?) + .map_err(|_| GitConfigError::FailedConversion) + } + + fn get_section_id_by_name_and_subname<'b>( + &'a self, + section_name: &'b BStr, + subsection_name: Option<&'b BStr>, + ) -> Result> { + self.get_section_ids_by_name_and_subname(section_name, subsection_name) + .map(|vec| { + // get_section_ids_by_name_and_subname is guaranteed to return + // a non-empty vec, so max can never return empty. + *vec.iter().max().unwrap() + }) + } + + /// Returns an uninterpreted value given a section, an optional subsection + /// and key. + /// + /// # Multivar behavior + /// + /// `git` is flexible enough to allow users to set a key multiple times in + /// any number of identically named sections. When this is the case, the key + /// is known as a "multivar". In this case, `get_raw_value` follows the + /// "last one wins" approach that `git-config` internally uses for multivar + /// resolution. + /// + /// Concretely, the following config has a multivar, `a`, with the values + /// of `b`, `c`, and `d`, while `e` is a single variable with the value + /// `f g h`. + /// + /// ```text + /// [core] + /// a = b + /// a = c + /// [core] + /// a = d + /// e = f g h + /// ``` + /// + /// Calling this function to fetch `a` with the above config will return + /// `d`, since the last valid config value is `a = d`: + /// + /// ``` + /// # use git_config::config::{GitConfig, GitConfigError}; + /// # use git_config::values::Value; + /// # use std::borrow::Cow; + /// # use std::convert::TryFrom; + /// # let git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); + /// assert_eq!( + /// git_config.get_value::("core", None, "a")?, + /// Value::Other(Cow::Borrowed("d".into())) + /// ); + /// # Ok::<(), GitConfigError>(()) + /// ``` + /// + /// Consider [`Self::get_raw_multi_value`] if you want to get all values of + /// a multivar instead. + /// /// # Errors /// /// This function will return an error if the key is not in the requested @@ -207,7 +326,8 @@ impl<'a> GitConfig<'a> { /// # use git_config::config::{GitConfig, GitConfigError}; /// # use std::borrow::Cow; /// # use bstr::BStr; - /// # let mut git_config = GitConfig::from_str("[core]a=b\n[core]\na=c\na=d").unwrap(); + /// # use std::convert::TryFrom; + /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); /// let mut mut_value = git_config.get_raw_value_mut("core", None, "a")?; /// assert_eq!(mut_value, &mut Cow::::Borrowed("d".into())); /// *mut_value = Cow::Borrowed("hello".into()); @@ -256,19 +376,6 @@ impl<'a> GitConfig<'a> { latest_value.ok_or(GitConfigError::KeyDoesNotExist(key)) } - fn get_section_id_by_name_and_subname<'b>( - &'a self, - section_name: &'b BStr, - subsection_name: Option<&'b BStr>, - ) -> Result> { - self.get_section_ids_by_name_and_subname(section_name, subsection_name) - .map(|vec| { - // get_section_ids_by_name_and_subname is guaranteed to return - // a non-empty vec, so max can never return empty. - *vec.iter().max().unwrap() - }) - } - /// Returns all uninterpreted values given a section, an optional subsection /// and key. If you have the following config: /// @@ -285,7 +392,8 @@ impl<'a> GitConfig<'a> { /// ``` /// # use git_config::config::GitConfig; /// # use std::borrow::Cow; - /// # let git_config = GitConfig::from_str("[core]a=b\n[core]\na=c\na=d").unwrap(); + /// # use std::convert::TryFrom; + /// # let git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); /// assert_eq!( /// git_config.get_raw_multi_value("core", None, "a"), /// Ok(vec![&Cow::Borrowed("b".into()), &Cow::Borrowed("c".into()), &Cow::Borrowed("d".into())]), @@ -351,7 +459,8 @@ impl<'a> GitConfig<'a> { /// # use git_config::config::{GitConfig, GitConfigError}; /// # use std::borrow::Cow; /// # use bstr::BStr; - /// # let mut git_config = GitConfig::from_str("[core]a=b\n[core]\na=c\na=d").unwrap(); + /// # use std::convert::TryFrom; + /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); /// assert_eq!( /// git_config.get_raw_multi_value("core", None, "a")?, /// vec![ @@ -888,6 +997,25 @@ mod get_raw_value { } } +#[cfg(test)] +mod get_value { + use super::*; + use crate::values::{Boolean, TrueVariant, Value}; + use std::error::Error; + + #[test] + fn single_section() -> Result<(), Box> { + let config = GitConfig::try_from("[core]\na=b\nc").unwrap(); + let first_value: Value = config.get_value("core", None, "a")?; + let second_value: Boolean = config.get_value("core", None, "c")?; + + assert_eq!(first_value, Value::Other(Cow::Borrowed("b".into()))); + assert_eq!(second_value, Boolean::True(TrueVariant::Implicit)); + + Ok(()) + } +} + #[cfg(test)] mod get_raw_multi_value { use super::*; diff --git a/src/parser.rs b/src/parser.rs index 85df39c..6d5e34c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -285,7 +285,7 @@ impl Display for ParserNode { /// /// - This struct does _not_ implement [`FromStr`] due to lifetime /// constraints implied on the required `from_str` method. Instead, it provides -/// [`Parser::from_str`]. +/// [`From<&'_ str>`]. /// /// # Idioms /// @@ -439,6 +439,7 @@ impl Display for ParserNode { /// [`.ini` file format]: https://en.wikipedia.org/wiki/INI_file /// [`git`'s documentation]: https://git-scm.com/docs/git-config#_configuration_file /// [`FromStr`]: std::str::FromStr +/// [`From<&'_ str>`]: std::convert::From #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct Parser<'a> { frontmatter: Vec>, diff --git a/src/values.rs b/src/values.rs index 097ccfa..6d423fb 100644 --- a/src/values.rs +++ b/src/values.rs @@ -1,10 +1,9 @@ //! Rust containers for valid `git-config` types. -use std::{borrow::Cow, convert::TryFrom, fmt::Display, str::FromStr}; - -use bstr::BStr; +use bstr::{BStr, ByteSlice}; #[cfg(feature = "serde")] use serde::{Serialize, Serializer}; +use std::{borrow::Cow, convert::TryFrom, fmt::Display, str::FromStr}; /// Fully enumerated valid types that a `git-config` value can be. #[allow(missing_docs)] @@ -43,6 +42,27 @@ impl<'a> From<&'a str> for Value<'a> { } } +impl<'a> From<&'a [u8]> for Value<'a> { + fn from(s: &'a [u8]) -> Self { + // All parsable values must be utf-8 valid + if let Ok(s) = std::str::from_utf8(s) { + if let Ok(bool) = Boolean::try_from(s) { + return Self::Boolean(bool); + } + + if let Ok(int) = Integer::from_str(s) { + return Self::Integer(int); + } + + if let Ok(color) = Color::from_str(s) { + return Self::Color(color); + } + } + + Self::Other(Cow::Borrowed(s.as_bstr())) + } +} + // todo display for value #[cfg(feature = "serde")] @@ -70,17 +90,25 @@ impl<'a> TryFrom<&'a str> for Boolean<'a> { type Error = (); fn try_from(value: &'a str) -> Result { + Self::try_from(value.as_bytes()) + } +} + +impl<'a> TryFrom<&'a [u8]> for Boolean<'a> { + type Error = (); + + fn try_from(value: &'a [u8]) -> Result { if let Ok(v) = TrueVariant::try_from(value) { return Ok(Self::True(v)); } - if value.eq_ignore_ascii_case("no") - || value.eq_ignore_ascii_case("off") - || value.eq_ignore_ascii_case("false") - || value.eq_ignore_ascii_case("zero") - || value == "\"\"" + if value.eq_ignore_ascii_case(b"no") + || value.eq_ignore_ascii_case(b"off") + || value.eq_ignore_ascii_case(b"false") + || value.eq_ignore_ascii_case(b"zero") + || value == b"\"\"" { - return Ok(Self::False(value)); + return Ok(Self::False(std::str::from_utf8(value).unwrap())); } Err(()) @@ -132,12 +160,22 @@ impl<'a> TryFrom<&'a str> for TrueVariant<'a> { type Error = (); fn try_from(value: &'a str) -> Result { - if value.eq_ignore_ascii_case("yes") - || value.eq_ignore_ascii_case("on") - || value.eq_ignore_ascii_case("true") - || value.eq_ignore_ascii_case("one") + Self::try_from(value.as_bytes()) + } +} + +impl<'a> TryFrom<&'a [u8]> for TrueVariant<'a> { + type Error = (); + + fn try_from(value: &'a [u8]) -> Result { + if value.eq_ignore_ascii_case(b"yes") + || value.eq_ignore_ascii_case(b"on") + || value.eq_ignore_ascii_case(b"true") + || value.eq_ignore_ascii_case(b"one") { - Ok(Self::Explicit(value)) + Ok(Self::Explicit(std::str::from_utf8(value).unwrap())) + } else if value.is_empty() { + Ok(Self::Implicit) } else { Err(()) } @@ -224,6 +262,14 @@ impl FromStr for Integer { } } +impl TryFrom<&[u8]> for Integer { + type Error = (); + + fn try_from(s: &[u8]) -> Result { + Self::from_str(std::str::from_utf8(s).map_err(|_| ())?).map_err(|_| ()) + } +} + #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum IntegerSuffix { Kilo, @@ -279,6 +325,14 @@ impl FromStr for IntegerSuffix { } } +impl TryFrom<&[u8]> for IntegerSuffix { + type Error = (); + + fn try_from(s: &[u8]) -> Result { + Self::from_str(std::str::from_utf8(s).map_err(|_| ())?).map_err(|_| ()) + } +} + #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct Color { foreground: Option, @@ -364,6 +418,14 @@ impl FromStr for Color { } } +impl TryFrom<&[u8]> for Color { + type Error = (); + + fn try_from(s: &[u8]) -> Result { + Self::from_str(std::str::from_utf8(s).map_err(|_| ())?).map_err(|_| ()) + } +} + #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] enum ColorValue { Normal, @@ -479,6 +541,14 @@ impl FromStr for ColorValue { } } +impl TryFrom<&[u8]> for ColorValue { + type Error = (); + + fn try_from(s: &[u8]) -> Result { + Self::from_str(std::str::from_utf8(s).map_err(|_| ())?).map_err(|_| ()) + } +} + #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum ColorAttribute { Bold, @@ -578,6 +648,14 @@ impl FromStr for ColorAttribute { } } +impl TryFrom<&[u8]> for ColorAttribute { + type Error = (); + + fn try_from(s: &[u8]) -> Result { + Self::from_str(std::str::from_utf8(s).map_err(|_| ())?).map_err(|_| ()) + } +} + #[cfg(test)] mod boolean { use super::*; @@ -625,7 +703,6 @@ mod boolean { fn from_str_err() { assert!(Boolean::try_from("yesn't").is_err()); assert!(Boolean::try_from("yesno").is_err()); - assert!(Boolean::try_from("").is_err()); } }