Implement get_value for GitConfig

This commit is contained in:
Edward Shen 2021-02-26 23:33:38 -05:00
parent 3e97f07b28
commit 16496d91a1
Signed by: edward
GPG key ID: 19182661E818369F
3 changed files with 242 additions and 36 deletions

View file

@ -1,8 +1,11 @@
use crate::parser::{parse_from_bytes, Event, ParsedSectionHeader, Parser, ParserError}; use crate::parser::{parse_from_bytes, Event, ParsedSectionHeader, Parser, ParserError};
use bstr::BStr; use bstr::BStr;
use std::collections::{HashMap, VecDeque};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::{borrow::Cow, fmt::Display}; use std::{borrow::Cow, fmt::Display};
use std::{
collections::{HashMap, VecDeque},
error::Error,
};
#[derive(PartialEq, Eq, Hash, Copy, Clone, PartialOrd, Ord, Debug)] #[derive(PartialEq, Eq, Hash, Copy, Clone, PartialOrd, Ord, Debug)]
pub enum GitConfigError<'a> { pub enum GitConfigError<'a> {
@ -12,8 +15,23 @@ pub enum GitConfigError<'a> {
SubSectionDoesNotExist(Option<&'a BStr>), SubSectionDoesNotExist(Option<&'a BStr>),
/// The key does not exist in the requested section. /// The key does not exist in the requested section.
KeyDoesNotExist(&'a BStr), 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. /// 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 /// This value does not imply any ordering between sections, as new sections
/// with higher section IDs may be in between lower ID 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 /// Returns an interpreted value given a section, an optional subsection and
/// and key. /// 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 /// # Multivar behavior
/// ///
@ -131,13 +153,110 @@ impl<'a> GitConfig<'a> {
/// ``` /// ```
/// # use git_config::config::GitConfig; /// # use git_config::config::GitConfig;
/// # use std::borrow::Cow; /// # 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()))); /// 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 /// Consider [`Self::get_raw_multi_value`] if you want to get all values of
/// a multivar instead. /// 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::<Integer, _>("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<S>,
key: S,
) -> Result<T, GitConfigError<'b>> {
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<SectionId, GitConfigError<'b>> {
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::<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 /// # Errors
/// ///
/// This function will return an error if the key is not in the requested /// 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 git_config::config::{GitConfig, GitConfigError};
/// # use std::borrow::Cow; /// # use std::borrow::Cow;
/// # use bstr::BStr; /// # 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")?; /// let mut mut_value = git_config.get_raw_value_mut("core", None, "a")?;
/// assert_eq!(mut_value, &mut Cow::<BStr>::Borrowed("d".into())); /// assert_eq!(mut_value, &mut Cow::<BStr>::Borrowed("d".into()));
/// *mut_value = Cow::Borrowed("hello".into()); /// *mut_value = Cow::Borrowed("hello".into());
@ -256,19 +376,6 @@ impl<'a> GitConfig<'a> {
latest_value.ok_or(GitConfigError::KeyDoesNotExist(key)) 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<SectionId, GitConfigError<'b>> {
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 /// Returns all uninterpreted values given a section, an optional subsection
/// and key. If you have the following config: /// and key. If you have the following config:
/// ///
@ -285,7 +392,8 @@ impl<'a> GitConfig<'a> {
/// ``` /// ```
/// # use git_config::config::GitConfig; /// # use git_config::config::GitConfig;
/// # use std::borrow::Cow; /// # 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!( /// assert_eq!(
/// git_config.get_raw_multi_value("core", None, "a"), /// git_config.get_raw_multi_value("core", None, "a"),
/// Ok(vec![&Cow::Borrowed("b".into()), &Cow::Borrowed("c".into()), &Cow::Borrowed("d".into())]), /// 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 git_config::config::{GitConfig, GitConfigError};
/// # use std::borrow::Cow; /// # use std::borrow::Cow;
/// # use bstr::BStr; /// # 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!( /// assert_eq!(
/// git_config.get_raw_multi_value("core", None, "a")?, /// git_config.get_raw_multi_value("core", None, "a")?,
/// vec![ /// 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<dyn Error>> {
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)] #[cfg(test)]
mod get_raw_multi_value { mod get_raw_multi_value {
use super::*; use super::*;

View file

@ -285,7 +285,7 @@ impl Display for ParserNode {
/// ///
/// - This struct does _not_ implement [`FromStr`] due to lifetime /// - This struct does _not_ implement [`FromStr`] due to lifetime
/// constraints implied on the required `from_str` method. Instead, it provides /// constraints implied on the required `from_str` method. Instead, it provides
/// [`Parser::from_str`]. /// [`From<&'_ str>`].
/// ///
/// # Idioms /// # Idioms
/// ///
@ -439,6 +439,7 @@ impl Display for ParserNode {
/// [`.ini` file format]: https://en.wikipedia.org/wiki/INI_file /// [`.ini` file format]: https://en.wikipedia.org/wiki/INI_file
/// [`git`'s documentation]: https://git-scm.com/docs/git-config#_configuration_file /// [`git`'s documentation]: https://git-scm.com/docs/git-config#_configuration_file
/// [`FromStr`]: std::str::FromStr /// [`FromStr`]: std::str::FromStr
/// [`From<&'_ str>`]: std::convert::From
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct Parser<'a> { pub struct Parser<'a> {
frontmatter: Vec<Event<'a>>, frontmatter: Vec<Event<'a>>,

View file

@ -1,10 +1,9 @@
//! Rust containers for valid `git-config` types. //! Rust containers for valid `git-config` types.
use std::{borrow::Cow, convert::TryFrom, fmt::Display, str::FromStr}; use bstr::{BStr, ByteSlice};
use bstr::BStr;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use serde::{Serialize, Serializer}; 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. /// Fully enumerated valid types that a `git-config` value can be.
#[allow(missing_docs)] #[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 // todo display for value
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
@ -70,17 +90,25 @@ impl<'a> TryFrom<&'a str> for Boolean<'a> {
type Error = (); type Error = ();
fn try_from(value: &'a str) -> Result<Self, Self::Error> { fn try_from(value: &'a str) -> Result<Self, Self::Error> {
Self::try_from(value.as_bytes())
}
}
impl<'a> TryFrom<&'a [u8]> for Boolean<'a> {
type Error = ();
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
if let Ok(v) = TrueVariant::try_from(value) { if let Ok(v) = TrueVariant::try_from(value) {
return Ok(Self::True(v)); return Ok(Self::True(v));
} }
if value.eq_ignore_ascii_case("no") if value.eq_ignore_ascii_case(b"no")
|| value.eq_ignore_ascii_case("off") || value.eq_ignore_ascii_case(b"off")
|| value.eq_ignore_ascii_case("false") || value.eq_ignore_ascii_case(b"false")
|| value.eq_ignore_ascii_case("zero") || value.eq_ignore_ascii_case(b"zero")
|| value == "\"\"" || value == b"\"\""
{ {
return Ok(Self::False(value)); return Ok(Self::False(std::str::from_utf8(value).unwrap()));
} }
Err(()) Err(())
@ -132,12 +160,22 @@ impl<'a> TryFrom<&'a str> for TrueVariant<'a> {
type Error = (); type Error = ();
fn try_from(value: &'a str) -> Result<Self, Self::Error> { fn try_from(value: &'a str) -> Result<Self, Self::Error> {
if value.eq_ignore_ascii_case("yes") Self::try_from(value.as_bytes())
|| value.eq_ignore_ascii_case("on") }
|| value.eq_ignore_ascii_case("true") }
|| value.eq_ignore_ascii_case("one")
impl<'a> TryFrom<&'a [u8]> for TrueVariant<'a> {
type Error = ();
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
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 { } else {
Err(()) Err(())
} }
@ -224,6 +262,14 @@ impl FromStr for Integer {
} }
} }
impl TryFrom<&[u8]> for Integer {
type Error = ();
fn try_from(s: &[u8]) -> Result<Self, Self::Error> {
Self::from_str(std::str::from_utf8(s).map_err(|_| ())?).map_err(|_| ())
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum IntegerSuffix { pub enum IntegerSuffix {
Kilo, Kilo,
@ -279,6 +325,14 @@ impl FromStr for IntegerSuffix {
} }
} }
impl TryFrom<&[u8]> for IntegerSuffix {
type Error = ();
fn try_from(s: &[u8]) -> Result<Self, Self::Error> {
Self::from_str(std::str::from_utf8(s).map_err(|_| ())?).map_err(|_| ())
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct Color { pub struct Color {
foreground: Option<ColorValue>, foreground: Option<ColorValue>,
@ -364,6 +418,14 @@ impl FromStr for Color {
} }
} }
impl TryFrom<&[u8]> for Color {
type Error = ();
fn try_from(s: &[u8]) -> Result<Self, Self::Error> {
Self::from_str(std::str::from_utf8(s).map_err(|_| ())?).map_err(|_| ())
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
enum ColorValue { enum ColorValue {
Normal, Normal,
@ -479,6 +541,14 @@ impl FromStr for ColorValue {
} }
} }
impl TryFrom<&[u8]> for ColorValue {
type Error = ();
fn try_from(s: &[u8]) -> Result<Self, Self::Error> {
Self::from_str(std::str::from_utf8(s).map_err(|_| ())?).map_err(|_| ())
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum ColorAttribute { pub enum ColorAttribute {
Bold, Bold,
@ -578,6 +648,14 @@ impl FromStr for ColorAttribute {
} }
} }
impl TryFrom<&[u8]> for ColorAttribute {
type Error = ();
fn try_from(s: &[u8]) -> Result<Self, Self::Error> {
Self::from_str(std::str::from_utf8(s).map_err(|_| ())?).map_err(|_| ())
}
}
#[cfg(test)] #[cfg(test)]
mod boolean { mod boolean {
use super::*; use super::*;
@ -625,7 +703,6 @@ mod boolean {
fn from_str_err() { fn from_str_err() {
assert!(Boolean::try_from("yesn't").is_err()); assert!(Boolean::try_from("yesn't").is_err());
assert!(Boolean::try_from("yesno").is_err()); assert!(Boolean::try_from("yesno").is_err());
assert!(Boolean::try_from("").is_err());
} }
} }