completely refactor config

This commit is contained in:
Edward Shen 2021-02-20 00:33:17 -05:00
parent 19fc0ebaaf
commit 1c770a9560
Signed by: edward
GPG key ID: 19182661E818369F
4 changed files with 265 additions and 249 deletions

View file

@ -1,223 +1,243 @@
use crate::parser::{parse_from_str, Event, ParsedSectionHeader, Parser};
use crate::values::Value;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, convert::TryFrom, io::Read};
use std::collections::HashMap;
type SectionConfig<'a> = HashMap<&'a str, Value<'a>>;
use crate::parser::{parse_from_str, Event, Parser, ParserError};
/// This struct provides a high level wrapper to access `git-config` file. This
/// struct exists primarily for reading a config rather than modifying it, as
/// it discards comments and unnecessary whitespace.
#[derive(Clone, Eq, PartialEq, Debug, Default)]
pub struct GitConfig<'a>(HashMap<&'a str, HashMap<&'a str, SectionConfig<'a>>>);
#[derive(PartialEq, Eq, Hash, Copy, Clone, PartialOrd, Ord)]
struct SectionId(usize);
const EMPTY_MARKER: &str = "@"; // Guaranteed to not be a {sub,}section or name.
enum LookupTreeNode<'a> {
Terminal(Vec<SectionId>),
NonTerminal(HashMap<&'a str, Vec<SectionId>>),
}
/// High level `git-config` reader and writer.
pub struct GitConfig<'a> {
front_matter_events: Vec<Event<'a>>,
section_lookup_tree: HashMap<&'a str, Vec<LookupTreeNode<'a>>>,
sections: HashMap<SectionId, Vec<Event<'a>>>,
section_header_separators: HashMap<SectionId, Option<&'a str>>,
section_id_counter: usize,
}
impl<'a> GitConfig<'a> {
/// Attempts to construct a instance given a [`Parser`] instance.
///
/// This is _not_ a zero-copy operation. Due to how partial values may be
/// provided, we necessarily need to copy and store these values until we
/// are done.
pub fn try_from_parser_with_options(
parser: Parser<'a>,
options: ConfigOptions,
) -> Result<Self, ()> {
Self::try_from_event_iter_with_options(parser.into_iter(), options)
/// Convenience constructor. Attempts to parse the provided string into a
/// [`GitConfig`].
pub fn from_str(str: &'a str) -> Result<Self, ParserError> {
Ok(Self::from_parser(parse_from_str(str)?))
}
pub fn try_from_event_iter_with_options(
iter: impl Iterator<Item = Event<'a>>,
options: ConfigOptions,
) -> Result<Self, ()> {
let mut sections: HashMap<&'a str, HashMap<&'a str, SectionConfig<'a>>> = HashMap::new();
let mut current_section_name = EMPTY_MARKER;
let mut current_subsection_name = EMPTY_MARKER;
let mut ignore_until_next_section = false;
let mut current_key = EMPTY_MARKER;
let mut value_scratch = String::new();
pub fn from_parser(parser: Parser<'a>) -> Self {
// Monotonically increasing
let mut section_id_counter: usize = 0;
for event in iter {
// Fields for the struct
let mut front_matter_events: Vec<Event<'a>> = vec![];
let mut sections: HashMap<SectionId, Vec<Event<'a>>> = HashMap::new();
let mut section_lookup_tree: HashMap<&str, Vec<LookupTreeNode>> = HashMap::new();
let mut section_header_separators = HashMap::new();
// Current section that we're building
let mut current_section_name: Option<&str> = None;
let mut current_subsection_name: Option<&str> = None;
let mut maybe_section: Option<Vec<Event<'a>>> = None;
for event in parser.into_iter() {
match event {
Event::Comment(_) => (),
Event::SectionHeader(ParsedSectionHeader {
name,
separator: _,
subsection_name,
}) => {
current_section_name = name;
match (sections.get_mut(name), options.on_duplicate_section) {
(Some(_), OnDuplicateBehavior::Error) => todo!(),
(Some(section), OnDuplicateBehavior::Overwrite) => {
section.clear();
}
(Some(_), OnDuplicateBehavior::KeepExisting) => {
ignore_until_next_section = true;
}
(None, _) => {
sections.insert(name, HashMap::default());
}
}
e @ Event::Comment(_) => match maybe_section {
Some(ref mut section) => section.push(e),
None => front_matter_events.push(e),
},
Event::SectionHeader(header) => {
// Push current section to struct
let new_section_id = SectionId(section_id_counter);
if let Some(section) = maybe_section.take() {
sections.insert(new_section_id, section);
let lookup = section_lookup_tree
.entry(current_section_name.unwrap())
.or_default();
match subsection_name {
Some(v) => current_subsection_name = v,
None => {
current_subsection_name = EMPTY_MARKER;
continue;
}
};
// subsection parsing
match (
sections
.get_mut(current_section_name)
.unwrap() // Guaranteed to exist at this point
.get_mut(current_subsection_name),
options.on_duplicate_section,
) {
(Some(_), OnDuplicateBehavior::Error) => todo!(),
(Some(section), OnDuplicateBehavior::Overwrite) => section.clear(),
(Some(_), OnDuplicateBehavior::KeepExisting) => {
ignore_until_next_section = true;
}
(None, _) => (),
let mut found_node = false;
if let Some(subsection_name) = current_subsection_name {
for node in lookup.iter_mut() {
if let LookupTreeNode::NonTerminal(subsection) = node {
found_node = true;
subsection
.entry(subsection_name)
.or_default()
.push(new_section_id);
break;
}
}
_ if ignore_until_next_section => (),
Event::Key(key) => {
current_key = key;
}
Event::Value(v) => {
Self::insert_value(
&mut sections,
current_section_name,
current_subsection_name,
current_key,
v,
options.on_duplicate_name,
)?;
}
Event::Newline(_) => (),
Event::ValueNotDone(v) => value_scratch.push_str(v),
Event::ValueDone(v) => {
let mut completed_value = String::new();
value_scratch.push_str(v);
std::mem::swap(&mut completed_value, &mut value_scratch);
Self::insert_value(
&mut sections,
current_section_name,
current_subsection_name,
current_key,
Value::from_string(completed_value),
options.on_duplicate_name,
)?;
}
Event::Whitespace(_) => (),
}
}
Ok(Self(sections))
}
fn insert_value(
map: &mut HashMap<&'a str, HashMap<&'a str, SectionConfig<'a>>>,
section: &str,
subsection: &str,
key: &'a str,
value: Value<'a>,
on_dup: OnDuplicateBehavior,
) -> Result<(), ()> {
let config = map.get_mut(section).unwrap().get_mut(subsection).unwrap();
if config.contains_key(key) {
match on_dup {
OnDuplicateBehavior::Error => return Err(()),
OnDuplicateBehavior::Overwrite => {
config.insert(key, value);
}
OnDuplicateBehavior::KeepExisting => (),
if !found_node {
let mut map = HashMap::new();
map.insert(subsection_name, vec![new_section_id]);
lookup.push(LookupTreeNode::NonTerminal(map));
}
} else {
config.insert(key, value);
for node in lookup.iter_mut() {
if let LookupTreeNode::Terminal(vec) = node {
found_node = true;
vec.push(new_section_id);
break;
}
}
if !found_node {
lookup.push(LookupTreeNode::Terminal(vec![new_section_id]))
}
}
Ok(())
section_id_counter += 1;
}
pub fn get_section(&self, section_name: &str) -> Option<&SectionConfig<'_>> {
self.get_subsection(section_name, EMPTY_MARKER)
// Initialize new section
let (name, subname) = (header.name, header.subsection_name);
maybe_section = Some(vec![]);
current_section_name = Some(name);
current_subsection_name = subname;
// 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.
section_header_separators
.insert(SectionId(section_id_counter), header.separator);
}
e @ Event::Key(_) => 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 => 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 => front_matter_events.push(e),
},
}
}
pub fn get_section_value(&self, section_name: &str, key: &str) -> Option<&Value<'_>> {
self.get_section(section_name)
.map(|section| section.get(key))
.flatten()
Self {
front_matter_events,
section_lookup_tree,
sections,
section_header_separators,
section_id_counter,
}
}
pub fn get_subsection(
pub fn get_raw_single_value<'b>(
&self,
section_name: &str,
subsection_name: &str,
) -> Option<&SectionConfig<'_>> {
self.0
.get(section_name)
.map(|subsections| subsections.get(subsection_name))
section_name: &'b str,
subsection_name: Option<&'b str>,
key: &'b str,
) -> Result<&'a str, GitConfigError<'b>> {
// 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))?;
// section_id is guaranteed to exist in self.sections, else we have a
// violated invariant.
let events = self.sections.get(&section_id).unwrap();
let mut found_key = false;
for event in events {
match event {
Event::Key(event_key) if *event_key == key => found_key = true,
Event::Value(v) if found_key => return Ok(v),
_ => (),
}
}
Err(GitConfigError::KeyDoesNotExist(key))
}
fn get_section_id_by_name_and_subname<'b>(
&'a self,
section_name: &'b str,
subsection_name: Option<&'b str>,
) -> Option<SectionId> {
self.get_section_ids_by_name_and_subname(section_name, subsection_name)
.map(|vec| vec.into_iter().max())
.flatten()
}
pub fn get_subsection_value(
&self,
section_name: &str,
subsection_name: &str,
key: &str,
) -> Option<&Value<'_>> {
self.get_subsection(section_name, subsection_name)
.map(|section| section.get(key))
.flatten()
pub fn get_raw_multi_value<'b>(
&'a self,
section_name: &'b str,
subsection_name: Option<&'b str>,
key: &'b str,
) -> Result<Vec<&'a str>, GitConfigError<'b>> {
let values = self
.get_section_ids_by_name_and_subname(section_name, subsection_name)
.ok_or(GitConfigError::SubSectionDoesNotExist(subsection_name))?
.iter()
.map(|section_id| {
let mut found_key = false;
// 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),
_ => (),
}
}
Err(GitConfigError::KeyDoesNotExist(key))
})
.filter_map(Result::ok)
.collect::<Vec<_>>();
if values.is_empty() {
Err(GitConfigError::KeyDoesNotExist(key))
} else {
Ok(values)
}
}
fn get_section_ids_by_name_and_subname<'b>(
&'a self,
section_name: &'b str,
subsection_name: Option<&'b str>,
) -> Option<Vec<SectionId>> {
let section_ids = self.section_lookup_tree.get(section_name)?;
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();
break;
}
}
maybe_ids.map(|v| vec![*v])
}
}
}
impl<'a> TryFrom<Parser<'a>> for GitConfig<'a> {
type Error = ();
fn try_from(parser: Parser<'a>) -> Result<Self, Self::Error> {
Self::try_from_parser_with_options(parser, ConfigOptions::default())
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct ConfigOptions {
on_duplicate_section: OnDuplicateBehavior,
on_duplicate_name: OnDuplicateBehavior,
}
impl ConfigOptions {
pub fn on_duplicate_section(&mut self, behavior: OnDuplicateBehavior) -> &mut Self {
self.on_duplicate_section = behavior;
self
}
pub fn on_duplicate_name(&mut self, behavior: OnDuplicateBehavior) -> &mut Self {
self.on_duplicate_name = behavior;
self
}
}
/// [`GitConfig`]'s valid possible actions when encountering a duplicate section
/// or key name within a section.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum OnDuplicateBehavior {
/// Fail the operation, returning an error instead. This is the strictest
/// behavior, and is the default.
Error,
/// Discard any data we had before on the
Overwrite,
KeepExisting,
}
impl Default for OnDuplicateBehavior {
fn default() -> Self {
Self::Error
}
pub enum GitConfigError<'a> {
SectionDoesNotExist(&'a str),
SubSectionDoesNotExist(Option<&'a str>),
KeyDoesNotExist(&'a str),
}

View file

@ -7,7 +7,6 @@
//! which can be converted into an [`Event`] iterator. The [`Parser`] also has
//! additional methods for accessing leading comments or events by section.
use crate::values::{Boolean, TrueVariant, Value};
use nom::bytes::complete::{escaped, tag, take_till, take_while};
use nom::character::complete::{char, none_of, one_of};
use nom::character::{is_newline, is_space};
@ -22,21 +21,32 @@ use std::iter::FusedIterator;
/// Syntactic events that occurs in the config.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum Event<'a> {
/// A comment with a comment tag and the comment itself. Note that the
/// comment itself may contain additional whitespace and comment markers
/// at the beginning.
Comment(ParsedComment<'a>),
/// A section header containing the section name and a subsection, if it
/// exists.
SectionHeader(ParsedSectionHeader<'a>),
/// A name to a value in a section.
Key(&'a str),
///
Value(Value<'a>),
/// A completed value. This may be any string, including the empty string,
/// if an implicit boolean value is used. Note that these values may contain
/// spaces and any special character. This value is also unprocessed, so it
/// it may contain double quotes that should be replaced.
Value(&'a str),
/// Represents any token used to signify a new line character. On Unix
/// platforms, this is typically just `\n`, but can be any valid newline
/// sequence.
Newline(&'a str),
/// Any value that isn't completed. This occurs when the value is continued
/// onto the next line. A Newline event is guaranteed after, followed by
/// either another ValueNotDone or a ValueDone.
/// either a ValueDone, a Whitespace, or another ValueNotDone.
ValueNotDone(&'a str),
/// The last line of a value which was continued onto another line.
ValueDone(&'a str),
/// A continuous section of insignificant whitespace. Values with internal
/// spaces will not be separated by this event.
Whitespace(&'a str),
}
@ -353,12 +363,7 @@ fn config_value<'a>(i: &'a str) -> IResult<&'a str, Vec<Event<'a>>> {
Ok((i, values))
}
} else {
Ok((
i,
vec![Event::Value(Value::Boolean(Boolean::True(
TrueVariant::Implicit,
)))],
))
Ok((i, vec![Event::Value("")]))
}
}
@ -449,7 +454,7 @@ fn value_impl<'a>(i: &'a str) -> IResult<&'a str, Vec<Event<'a>>> {
if partial_value_found {
events.push(Event::ValueDone(remainder_value));
} else {
events.push(Event::Value(Value::from_str(remainder_value)));
events.push(Event::Value(remainder_value));
}
Ok((i, events))
@ -643,7 +648,7 @@ mod parse {
fn no_comment() {
assert_eq!(
value_impl("hello").unwrap(),
fully_consumed(vec![Event::Value(Value::from_str("hello"))])
fully_consumed(vec![Event::Value("hello")])
);
}
@ -651,7 +656,7 @@ mod parse {
fn no_comment_newline() {
assert_eq!(
value_impl("hello\na").unwrap(),
("\na", vec![Event::Value(Value::from_str("hello"))])
("\na", vec![Event::Value("hello")])
)
}
@ -659,7 +664,7 @@ mod parse {
fn semicolon_comment_not_consumed() {
assert_eq!(
value_impl("hello;world").unwrap(),
(";world", vec![Event::Value(Value::from_str("hello")),])
(";world", vec![Event::Value("hello"),])
);
}
@ -667,7 +672,7 @@ mod parse {
fn octothorpe_comment_not_consumed() {
assert_eq!(
value_impl("hello#world").unwrap(),
("#world", vec![Event::Value(Value::from_str("hello")),])
("#world", vec![Event::Value("hello"),])
);
}
@ -675,10 +680,7 @@ mod parse {
fn values_with_extraneous_whitespace_without_comment() {
assert_eq!(
value_impl("hello ").unwrap(),
(
" ",
vec![Event::Value(Value::from_str("hello"))]
)
(" ", vec![Event::Value("hello")])
);
}
@ -686,17 +688,11 @@ mod parse {
fn values_with_extraneous_whitespace_before_comment() {
assert_eq!(
value_impl("hello #world").unwrap(),
(
" #world",
vec![Event::Value(Value::from_str("hello")),]
)
(" #world", vec![Event::Value("hello"),])
);
assert_eq!(
value_impl("hello ;world").unwrap(),
(
" ;world",
vec![Event::Value(Value::from_str("hello")),]
)
(" ;world", vec![Event::Value("hello"),])
);
}
@ -704,10 +700,7 @@ mod parse {
fn trans_escaped_comment_marker_not_consumed() {
assert_eq!(
value_impl(r##"hello"#"world; a"##).unwrap(),
(
"; a",
vec![Event::Value(Value::from_str(r##"hello"#"world"##)),]
)
("; a", vec![Event::Value(r##"hello"#"world"##)])
);
}
@ -715,10 +708,7 @@ mod parse {
fn complex_test() {
assert_eq!(
value_impl(r#"value";";ahhhh"#).unwrap(),
(
";ahhhh",
vec![Event::Value(Value::from_str(r#"value";""#)),]
)
(";ahhhh", vec![Event::Value(r#"value";""#)])
);
}
@ -807,17 +797,17 @@ mod parse {
Event::Key("a"),
Event::Whitespace(" "),
Event::Whitespace(" "),
Event::Value(Value::from_str("b")),
Event::Value("b"),
Event::Newline("\n"),
Event::Whitespace(" "),
Event::Key("c"),
Event::Value(Value::Boolean(Boolean::True(TrueVariant::Implicit))),
Event::Value(""),
Event::Newline("\n"),
Event::Whitespace(" "),
Event::Key("d"),
Event::Whitespace(" "),
Event::Whitespace(" "),
Event::Value(Value::from_str("\"lol\""))
Event::Value("\"lol\"")
]
})
)
@ -829,11 +819,7 @@ mod parse {
section("[hello] c").unwrap(),
fully_consumed(ParsedSection {
section_header: gen_section_header("hello", None),
events: vec![
Event::Whitespace(" "),
Event::Key("c"),
Event::Value(Value::Boolean(Boolean::True(TrueVariant::Implicit)))
]
events: vec![Event::Whitespace(" "), Event::Key("c"), Event::Value("")]
})
);
}
@ -860,7 +846,7 @@ mod parse {
Event::Key("a"),
Event::Whitespace(" "),
Event::Whitespace(" "),
Event::Value(Value::from_str("b")),
Event::Value("b"),
Event::Whitespace(" "),
Event::Comment(ParsedComment {
comment_tag: '#',
@ -883,7 +869,7 @@ mod parse {
Event::Key("c"),
Event::Whitespace(" "),
Event::Whitespace(" "),
Event::Value(Value::from_str("d")),
Event::Value("d"),
]
})
);
@ -947,7 +933,7 @@ mod parse {
events: vec![
Event::Key("hello"),
Event::Whitespace(" "),
Event::Value(Value::Boolean(Boolean::True(TrueVariant::Implicit))),
Event::Value(""),
Event::Comment(ParsedComment {
comment_tag: '#',
comment: "world",

View file

@ -12,7 +12,18 @@ pub enum Value<'a> {
impl<'a> Value<'a> {
pub fn from_str(s: &'a str) -> Self {
// if s.
if let Ok(bool) = Boolean::from_str(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))
}
@ -152,7 +163,11 @@ pub struct Integer {
suffix: Option<IntegerSuffix>,
}
impl Integer {}
impl Integer {
pub fn from_str(s: &str) -> Result<Self, ()> {
todo!()
}
}
impl Display for Integer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

View file

@ -1,9 +1,4 @@
use serde_git_config::parser::{parse_from_str, Event, ParsedSectionHeader};
use serde_git_config::values::Value;
fn fully_consumed<T>(t: T) -> (&'static str, T) {
("", t)
}
fn gen_section_header(
name: &str,
@ -30,7 +25,7 @@ fn name(name: &'static str) -> Event<'static> {
}
fn value(value: &'static str) -> Event<'static> {
Event::Value(Value::from_str(value))
Event::Value(value)
}
fn newline() -> Event<'static> {