more work
This commit is contained in:
parent
30b054df31
commit
d63b1f7ab3
6 changed files with 947 additions and 121 deletions
|
@ -7,8 +7,7 @@ edition = "2018"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = "1.0"
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
nom = "6"
|
nom = "6"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_derive = "1.0"
|
|
221
src/config.rs
Normal file
221
src/config.rs
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
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};
|
||||||
|
|
||||||
|
type SectionConfig<'a> = HashMap<&'a str, Value<'a>>;
|
||||||
|
|
||||||
|
/// 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, Serialize)]
|
||||||
|
pub struct GitConfig<'a>(HashMap<&'a str, HashMap<&'a str, SectionConfig<'a>>>);
|
||||||
|
|
||||||
|
const EMPTY_MARKER: &str = "@"; // Guaranteed to not be a {sub,}section or name.
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
for event in iter {
|
||||||
|
match event {
|
||||||
|
Event::Comment(_) => (),
|
||||||
|
Event::SectionHeader(ParsedSectionHeader {
|
||||||
|
name,
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, _) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ 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,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 => (),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.insert(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_section(&self, section_name: &str) -> Option<&SectionConfig<'_>> {
|
||||||
|
self.get_subsection(section_name, EMPTY_MARKER)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_section_value(&self, section_name: &str, key: &str) -> Option<&Value<'_>> {
|
||||||
|
self.get_section(section_name)
|
||||||
|
.map(|section| section.get(key))
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_subsection(
|
||||||
|
&self,
|
||||||
|
section_name: &str,
|
||||||
|
subsection_name: &str,
|
||||||
|
) -> Option<&SectionConfig<'_>> {
|
||||||
|
self.0
|
||||||
|
.get(section_name)
|
||||||
|
.map(|subsections| subsections.get(subsection_name))
|
||||||
|
.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
// mod de;
|
// mod de;
|
||||||
|
pub mod config;
|
||||||
mod error;
|
mod error;
|
||||||
// mod ser;
|
// mod ser;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
mod values;
|
pub mod values;
|
||||||
|
|
||||||
// pub use de::{from_str, Deserializer};
|
// pub use de::{from_str, Deserializer};
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
|
|
283
src/parser.rs
283
src/parser.rs
|
@ -8,18 +8,20 @@ use nom::multi::many1;
|
||||||
use nom::sequence::delimited;
|
use nom::sequence::delimited;
|
||||||
use nom::IResult;
|
use nom::IResult;
|
||||||
use nom::{branch::alt, multi::many0};
|
use nom::{branch::alt, multi::many0};
|
||||||
|
use std::iter::FusedIterator;
|
||||||
|
|
||||||
/// An event is any syntactic event that occurs in the config.
|
/// Syntactic event that occurs in the config.
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||||
pub enum Event<'a> {
|
pub enum Event<'a> {
|
||||||
Comment(Comment<'a>),
|
Comment(ParsedComment<'a>),
|
||||||
|
SectionHeader(ParsedSectionHeader<'a>),
|
||||||
Key(&'a str),
|
Key(&'a str),
|
||||||
|
///
|
||||||
|
Value(Value<'a>),
|
||||||
/// Represents any token used to signify a new line character. On Unix
|
/// Represents any token used to signify a new line character. On Unix
|
||||||
/// platforms, this is typically just `\n`, but can be any valid newline
|
/// platforms, this is typically just `\n`, but can be any valid newline
|
||||||
/// sequence.
|
/// sequence.
|
||||||
Newline(&'a str),
|
Newline(&'a str),
|
||||||
///
|
|
||||||
Value(Value<'a>),
|
|
||||||
/// Any value that isn't completed. This occurs when the value is continued
|
/// 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
|
/// onto the next line. A Newline event is guaranteed after, followed by
|
||||||
/// either another ValueNotDone or a ValueDone.
|
/// either another ValueNotDone or a ValueDone.
|
||||||
|
@ -28,32 +30,188 @@ pub enum Event<'a> {
|
||||||
ValueDone(&'a str),
|
ValueDone(&'a str),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||||
pub struct Section<'a> {
|
pub struct ParsedSection<'a> {
|
||||||
section_header: SectionHeader<'a>,
|
section_header: ParsedSectionHeader<'a>,
|
||||||
items: Vec<Event<'a>>,
|
items: Vec<Event<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
impl ParsedSection<'_> {
|
||||||
pub struct SectionHeader<'a> {
|
pub fn header(&self) -> &ParsedSectionHeader<'_> {
|
||||||
name: &'a str,
|
&self.section_header
|
||||||
subsection_name: Option<&'a str>,
|
}
|
||||||
|
|
||||||
|
pub fn take_header(&mut self) -> ParsedSectionHeader<'_> {
|
||||||
|
self.section_header
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn events(&self) -> &[Event<'_>] {
|
||||||
|
&self.items
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||||
pub struct Comment<'a> {
|
pub struct ParsedSectionHeader<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub subsection_name: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||||
|
pub struct ParsedComment<'a> {
|
||||||
comment_tag: char,
|
comment_tag: char,
|
||||||
comment: &'a str,
|
comment: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Parser<'a> {
|
#[derive(PartialEq, Debug)]
|
||||||
init_comments: Vec<Comment<'a>>,
|
pub enum ParserError<'a> {
|
||||||
sections: Vec<Section<'a>>,
|
InvalidInput(nom::Err<NomError<&'a str>>),
|
||||||
|
ConfigHasExtraData(&'a str),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(input: &str) -> Result<Parser<'_>, ()> {
|
#[doc(hidden)]
|
||||||
let (i, comments) = many0(comment)(input).unwrap();
|
impl<'a> From<nom::Err<NomError<&'a str>>> for ParserError<'a> {
|
||||||
let (i, sections) = many1(section)(i).unwrap();
|
fn from(e: nom::Err<NomError<&'a str>>) -> Self {
|
||||||
|
Self::InvalidInput(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A zero-copy `git-config` file parser.
|
||||||
|
///
|
||||||
|
/// # Non-perfect parser
|
||||||
|
///
|
||||||
|
/// This parser should successfully parse all sections and comments. However,
|
||||||
|
/// It will not parse whitespace. This attempts to closely follow the
|
||||||
|
/// non-normative specification found in [`git`'s documentation].
|
||||||
|
///
|
||||||
|
/// # Differences between a `.ini` parser
|
||||||
|
///
|
||||||
|
/// While the `git-config` format closely resembles the [`.ini` file format],
|
||||||
|
/// there are subtle differences that make them incompatible. For one, the file
|
||||||
|
/// format is not well defined, and there exists no formal specification to
|
||||||
|
/// adhere to. Thus, attempting to use an `.ini` parser on a `git-config` file
|
||||||
|
/// may successfully parse invalid configuration files.
|
||||||
|
///
|
||||||
|
/// For concrete examples, some notable differences are:
|
||||||
|
/// - `git-config` sections permit subsections via either a quoted string
|
||||||
|
/// (`[some-section "subsection"]`) or via the deprecated dot notation
|
||||||
|
/// (`[some-section.subsection]`). Successful parsing these section names is not
|
||||||
|
/// well defined in typical `.ini` parsers. This parser will handle these cases
|
||||||
|
/// perfectly.
|
||||||
|
/// - Comment markers are not strictly defined either. This parser will always
|
||||||
|
/// and only handle a semicolon or octothorpe (also known as a hash or number
|
||||||
|
/// sign).
|
||||||
|
/// - Global properties may be allowed in `.ini` parsers, but is strictly
|
||||||
|
/// disallowed by this parser.
|
||||||
|
/// - Only `\t`, `\n`, `\b` `\\` are valid escape characters.
|
||||||
|
/// - Quoted and semi-quoted values will be parsed (but quotes will be included
|
||||||
|
/// in event outputs). An example of a semi-quoted value is `5"hello world"`,
|
||||||
|
/// which should be interpreted as `5hello world`.
|
||||||
|
/// - Line continuations via a `\` character is supported.
|
||||||
|
/// - Whitespace handling similarly follows the `git-config` specification as
|
||||||
|
/// closely as possible, where excess whitespace after a non-quoted value are
|
||||||
|
/// trimmed, and line continuations onto a new line with excess spaces are kept.
|
||||||
|
/// - Only equal signs (optionally padded by spaces) are valid name/value
|
||||||
|
/// delimiters.
|
||||||
|
///
|
||||||
|
/// Note that that things such as case-sensitivity or duplicate sections are
|
||||||
|
/// _not_ handled. This parser is a low level _syntactic_ interpreter (as a
|
||||||
|
/// parser should be), and higher level wrappers around this parser (which may
|
||||||
|
/// or may not be zero-copy) should handle _semantic_ values.
|
||||||
|
///
|
||||||
|
/// # Trait Implementations
|
||||||
|
///
|
||||||
|
/// - This struct does _not_ implement [`FromStr`] due to lifetime
|
||||||
|
/// constraints implied on the required `from_str` method, but instead provides
|
||||||
|
/// [`Parser::from_str`].
|
||||||
|
///
|
||||||
|
/// [`.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
|
||||||
|
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||||
|
pub struct Parser<'a> {
|
||||||
|
init_comments: Vec<ParsedComment<'a>>,
|
||||||
|
sections: Vec<ParsedSection<'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`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the string provided is not a valid file, or we have
|
||||||
|
/// non-section data.
|
||||||
|
pub fn from_str(s: &'a str) -> Result<Self, ParserError> {
|
||||||
|
parse_from_str(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the leading comments (any comments before a section) from the
|
||||||
|
/// parser. Consider [`Parser::take_leading_comments`] if you need an owned
|
||||||
|
/// copy only once.
|
||||||
|
pub fn leading_comments(&self) -> &[ParsedComment<'a>] {
|
||||||
|
&self.init_comments
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes the leading comments (any comments before a section) from the
|
||||||
|
/// parser. Subsequent calls will return an empty vec. Consider
|
||||||
|
/// [`Parser::leading_comments`] if you only need a reference to the comments.
|
||||||
|
pub fn take_leading_comments(&mut self) -> Vec<ParsedComment<'a>> {
|
||||||
|
let mut to_return = vec![];
|
||||||
|
std::mem::swap(&mut self.init_comments, &mut to_return);
|
||||||
|
to_return
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sections(&self) -> &[ParsedSection<'a>] {
|
||||||
|
&self.sections
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_sections(&mut self) -> Vec<ParsedSection<'a>> {
|
||||||
|
let mut to_return = vec![];
|
||||||
|
std::mem::swap(&mut self.sections, &mut to_return);
|
||||||
|
to_return
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_vec(self) -> Vec<Event<'a>> {
|
||||||
|
self.into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_iter(self) -> impl Iterator<Item = Event<'a>> + FusedIterator {
|
||||||
|
let section_iter = self
|
||||||
|
.sections
|
||||||
|
.into_iter()
|
||||||
|
.map(|section| {
|
||||||
|
vec![Event::SectionHeader(section.section_header)]
|
||||||
|
.into_iter()
|
||||||
|
.chain(section.items)
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
self.init_comments
|
||||||
|
.into_iter()
|
||||||
|
.map(Event::Comment)
|
||||||
|
.chain(section_iter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the string provided is not a valid file, or we have
|
||||||
|
/// non-section data.
|
||||||
|
pub fn parse_from_str(input: &str) -> Result<Parser<'_>, ParserError> {
|
||||||
|
let (i, comments) = many0(comment)(input)?;
|
||||||
|
let (i, sections) = many1(section)(i)?;
|
||||||
|
|
||||||
|
if !i.is_empty() {
|
||||||
|
return Err(ParserError::ConfigHasExtraData(i));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Parser {
|
Ok(Parser {
|
||||||
init_comments: comments,
|
init_comments: comments,
|
||||||
|
@ -61,22 +219,22 @@ pub fn parse(input: &str) -> Result<Parser<'_>, ()> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn comment<'a>(i: &'a str) -> IResult<&'a str, Comment<'a>> {
|
fn comment<'a>(i: &'a str) -> IResult<&'a str, ParsedComment<'a>> {
|
||||||
let i = i.trim_start();
|
let i = i.trim_start();
|
||||||
let (i, comment_tag) = one_of(";#")(i)?;
|
let (i, comment_tag) = one_of(";#")(i)?;
|
||||||
let (i, comment) = take_till(is_char_newline)(i)?;
|
let (i, comment) = take_till(is_char_newline)(i)?;
|
||||||
Ok((
|
Ok((
|
||||||
i,
|
i,
|
||||||
Comment {
|
ParsedComment {
|
||||||
comment_tag,
|
comment_tag,
|
||||||
comment,
|
comment,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn section<'a>(i: &'a str) -> IResult<&'a str, Section<'a>> {
|
fn section<'a>(i: &'a str) -> IResult<&'a str, ParsedSection<'a>> {
|
||||||
|
let i = i.trim_start();
|
||||||
let (i, section_header) = section_header(i)?;
|
let (i, section_header) = section_header(i)?;
|
||||||
// need alt here for eof?
|
|
||||||
let (i, items) = many1(alt((
|
let (i, items) = many1(alt((
|
||||||
map(section_body, |(key, values)| {
|
map(section_body, |(key, values)| {
|
||||||
let mut vec = vec![Event::Key(key)];
|
let mut vec = vec![Event::Key(key)];
|
||||||
|
@ -87,14 +245,14 @@ fn section<'a>(i: &'a str) -> IResult<&'a str, Section<'a>> {
|
||||||
)))(i)?;
|
)))(i)?;
|
||||||
Ok((
|
Ok((
|
||||||
i,
|
i,
|
||||||
Section {
|
ParsedSection {
|
||||||
section_header,
|
section_header,
|
||||||
items: items.into_iter().flatten().collect(),
|
items: items.into_iter().flatten().collect(),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn section_header<'a>(i: &'a str) -> IResult<&'a str, SectionHeader<'a>> {
|
fn section_header<'a>(i: &'a str) -> IResult<&'a str, ParsedSectionHeader<'a>> {
|
||||||
let (i, _) = char('[')(i)?;
|
let (i, _) = char('[')(i)?;
|
||||||
// No spaces must be between section name and section start
|
// No spaces must be between section name and section start
|
||||||
let (i, name) = take_while(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '.')(i)?;
|
let (i, name) = take_while(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '.')(i)?;
|
||||||
|
@ -103,11 +261,11 @@ fn section_header<'a>(i: &'a str) -> IResult<&'a str, SectionHeader<'a>> {
|
||||||
// Either section does not have a subsection or using deprecated
|
// Either section does not have a subsection or using deprecated
|
||||||
// subsection syntax at this point.
|
// subsection syntax at this point.
|
||||||
let header = match name.rfind('.') {
|
let header = match name.rfind('.') {
|
||||||
Some(index) => SectionHeader {
|
Some(index) => ParsedSectionHeader {
|
||||||
name: &name[..index],
|
name: &name[..index],
|
||||||
subsection_name: Some(&name[index + 1..]),
|
subsection_name: Some(&name[index + 1..]),
|
||||||
},
|
},
|
||||||
None => SectionHeader {
|
None => ParsedSectionHeader {
|
||||||
name: name,
|
name: name,
|
||||||
subsection_name: None,
|
subsection_name: None,
|
||||||
},
|
},
|
||||||
|
@ -128,7 +286,7 @@ fn section_header<'a>(i: &'a str) -> IResult<&'a str, SectionHeader<'a>> {
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
i,
|
i,
|
||||||
SectionHeader {
|
ParsedSectionHeader {
|
||||||
name: name,
|
name: name,
|
||||||
// We know that there's some section name here, so if we get an
|
// We know that there's some section name here, so if we get an
|
||||||
// empty vec here then we actually parsed an empty section name.
|
// empty vec here then we actually parsed an empty section name.
|
||||||
|
@ -288,7 +446,7 @@ mod parse {
|
||||||
fn semicolon() {
|
fn semicolon() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
comment("; this is a semicolon comment").unwrap(),
|
comment("; this is a semicolon comment").unwrap(),
|
||||||
fully_consumed(Comment {
|
fully_consumed(ParsedComment {
|
||||||
comment_tag: ';',
|
comment_tag: ';',
|
||||||
comment: " this is a semicolon comment",
|
comment: " this is a semicolon comment",
|
||||||
})
|
})
|
||||||
|
@ -299,7 +457,7 @@ mod parse {
|
||||||
fn octothorpe() {
|
fn octothorpe() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
comment("# this is an octothorpe comment").unwrap(),
|
comment("# this is an octothorpe comment").unwrap(),
|
||||||
fully_consumed(Comment {
|
fully_consumed(ParsedComment {
|
||||||
comment_tag: '#',
|
comment_tag: '#',
|
||||||
comment: " this is an octothorpe comment",
|
comment: " this is an octothorpe comment",
|
||||||
})
|
})
|
||||||
|
@ -310,7 +468,7 @@ mod parse {
|
||||||
fn multiple_markers() {
|
fn multiple_markers() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
comment("###### this is an octothorpe comment").unwrap(),
|
comment("###### this is an octothorpe comment").unwrap(),
|
||||||
fully_consumed(Comment {
|
fully_consumed(ParsedComment {
|
||||||
comment_tag: '#',
|
comment_tag: '#',
|
||||||
comment: "##### this is an octothorpe comment",
|
comment: "##### this is an octothorpe comment",
|
||||||
})
|
})
|
||||||
|
@ -326,7 +484,7 @@ mod parse {
|
||||||
fn no_subsection() {
|
fn no_subsection() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section_header("[hello]").unwrap(),
|
section_header("[hello]").unwrap(),
|
||||||
fully_consumed(SectionHeader {
|
fully_consumed(ParsedSectionHeader {
|
||||||
name: "hello",
|
name: "hello",
|
||||||
subsection_name: None
|
subsection_name: None
|
||||||
})
|
})
|
||||||
|
@ -337,7 +495,7 @@ mod parse {
|
||||||
fn modern_subsection() {
|
fn modern_subsection() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section_header(r#"[hello "world"]"#).unwrap(),
|
section_header(r#"[hello "world"]"#).unwrap(),
|
||||||
fully_consumed(SectionHeader {
|
fully_consumed(ParsedSectionHeader {
|
||||||
name: "hello",
|
name: "hello",
|
||||||
subsection_name: Some("world")
|
subsection_name: Some("world")
|
||||||
})
|
})
|
||||||
|
@ -348,7 +506,7 @@ mod parse {
|
||||||
fn escaped_subsection() {
|
fn escaped_subsection() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section_header(r#"[hello "foo\\bar\""]"#).unwrap(),
|
section_header(r#"[hello "foo\\bar\""]"#).unwrap(),
|
||||||
fully_consumed(SectionHeader {
|
fully_consumed(ParsedSectionHeader {
|
||||||
name: "hello",
|
name: "hello",
|
||||||
subsection_name: Some(r#"foo\\bar\""#)
|
subsection_name: Some(r#"foo\\bar\""#)
|
||||||
})
|
})
|
||||||
|
@ -359,7 +517,7 @@ mod parse {
|
||||||
fn deprecated_subsection() {
|
fn deprecated_subsection() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section_header(r#"[hello.world]"#).unwrap(),
|
section_header(r#"[hello.world]"#).unwrap(),
|
||||||
fully_consumed(SectionHeader {
|
fully_consumed(ParsedSectionHeader {
|
||||||
name: "hello",
|
name: "hello",
|
||||||
subsection_name: Some("world")
|
subsection_name: Some("world")
|
||||||
})
|
})
|
||||||
|
@ -370,7 +528,7 @@ mod parse {
|
||||||
fn empty_legacy_subsection_name() {
|
fn empty_legacy_subsection_name() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section_header(r#"[hello.]"#).unwrap(),
|
section_header(r#"[hello.]"#).unwrap(),
|
||||||
fully_consumed(SectionHeader {
|
fully_consumed(ParsedSectionHeader {
|
||||||
name: "hello",
|
name: "hello",
|
||||||
subsection_name: Some("")
|
subsection_name: Some("")
|
||||||
})
|
})
|
||||||
|
@ -381,7 +539,7 @@ mod parse {
|
||||||
fn empty_modern_subsection_name() {
|
fn empty_modern_subsection_name() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section_header(r#"[hello ""]"#).unwrap(),
|
section_header(r#"[hello ""]"#).unwrap(),
|
||||||
fully_consumed(SectionHeader {
|
fully_consumed(ParsedSectionHeader {
|
||||||
name: "hello",
|
name: "hello",
|
||||||
subsection_name: Some("")
|
subsection_name: Some("")
|
||||||
})
|
})
|
||||||
|
@ -402,7 +560,7 @@ mod parse {
|
||||||
fn right_brace_in_subsection_name() {
|
fn right_brace_in_subsection_name() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section_header(r#"[hello "]"]"#).unwrap(),
|
section_header(r#"[hello "]"]"#).unwrap(),
|
||||||
fully_consumed(SectionHeader {
|
fully_consumed(ParsedSectionHeader {
|
||||||
name: "hello",
|
name: "hello",
|
||||||
subsection_name: Some("]")
|
subsection_name: Some("]")
|
||||||
})
|
})
|
||||||
|
@ -439,7 +597,7 @@ mod parse {
|
||||||
fn no_comment() {
|
fn no_comment() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value_impl("hello").unwrap(),
|
value_impl("hello").unwrap(),
|
||||||
fully_consumed(vec![Event::Value(Value::Other("hello"))])
|
fully_consumed(vec![Event::Value(Value::from_str("hello"))])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,7 +605,7 @@ mod parse {
|
||||||
fn no_comment_newline() {
|
fn no_comment_newline() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value_impl("hello\na").unwrap(),
|
value_impl("hello\na").unwrap(),
|
||||||
("\na", vec![Event::Value(Value::Other("hello"))])
|
("\na", vec![Event::Value(Value::from_str("hello"))])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -463,7 +621,7 @@ mod parse {
|
||||||
fn semicolon_comment_not_consumed() {
|
fn semicolon_comment_not_consumed() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value_impl("hello;world").unwrap(),
|
value_impl("hello;world").unwrap(),
|
||||||
(";world", vec![Event::Value(Value::Other("hello")),])
|
(";world", vec![Event::Value(Value::from_str("hello")),])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -471,7 +629,7 @@ mod parse {
|
||||||
fn octothorpe_comment_not_consumed() {
|
fn octothorpe_comment_not_consumed() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value_impl("hello#world").unwrap(),
|
value_impl("hello#world").unwrap(),
|
||||||
("#world", vec![Event::Value(Value::Other("hello")),])
|
("#world", vec![Event::Value(Value::from_str("hello")),])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -493,7 +651,7 @@ mod parse {
|
||||||
value_impl(r##"hello"#"world; a"##).unwrap(),
|
value_impl(r##"hello"#"world; a"##).unwrap(),
|
||||||
(
|
(
|
||||||
"; a",
|
"; a",
|
||||||
vec![Event::Value(Value::Other(r##"hello"#"world"##)),]
|
vec![Event::Value(Value::from_str(r##"hello"#"world"##)),]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -502,7 +660,10 @@ mod parse {
|
||||||
fn complex_test() {
|
fn complex_test() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value_impl(r#"value";";ahhhh"#).unwrap(),
|
value_impl(r#"value";";ahhhh"#).unwrap(),
|
||||||
(";ahhhh", vec![Event::Value(Value::Other(r#"value";""#)),])
|
(
|
||||||
|
";ahhhh",
|
||||||
|
vec![Event::Value(Value::from_str(r#"value";""#)),]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -572,8 +733,8 @@ mod parse {
|
||||||
d = "lol""#;
|
d = "lol""#;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section(section_data).unwrap(),
|
section(section_data).unwrap(),
|
||||||
fully_consumed(Section {
|
fully_consumed(ParsedSection {
|
||||||
section_header: SectionHeader {
|
section_header: ParsedSectionHeader {
|
||||||
name: "hello",
|
name: "hello",
|
||||||
subsection_name: None,
|
subsection_name: None,
|
||||||
},
|
},
|
||||||
|
@ -593,8 +754,8 @@ mod parse {
|
||||||
fn section_single_line() {
|
fn section_single_line() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section("[hello] c").unwrap(),
|
section("[hello] c").unwrap(),
|
||||||
fully_consumed(Section {
|
fully_consumed(ParsedSection {
|
||||||
section_header: SectionHeader {
|
section_header: ParsedSectionHeader {
|
||||||
name: "hello",
|
name: "hello",
|
||||||
subsection_name: None,
|
subsection_name: None,
|
||||||
},
|
},
|
||||||
|
@ -615,27 +776,27 @@ mod parse {
|
||||||
c = d"#;
|
c = d"#;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section(section_data).unwrap(),
|
section(section_data).unwrap(),
|
||||||
fully_consumed(Section {
|
fully_consumed(ParsedSection {
|
||||||
section_header: SectionHeader {
|
section_header: ParsedSectionHeader {
|
||||||
name: "hello",
|
name: "hello",
|
||||||
subsection_name: None,
|
subsection_name: None,
|
||||||
},
|
},
|
||||||
items: vec![
|
items: vec![
|
||||||
Event::Comment(Comment {
|
Event::Comment(ParsedComment {
|
||||||
comment_tag: ';',
|
comment_tag: ';',
|
||||||
comment: " commentA",
|
comment: " commentA",
|
||||||
}),
|
}),
|
||||||
Event::Key("a"),
|
Event::Key("a"),
|
||||||
Event::Value(Value::from_str("b")),
|
Event::Value(Value::from_str("b")),
|
||||||
Event::Comment(Comment {
|
Event::Comment(ParsedComment {
|
||||||
comment_tag: '#',
|
comment_tag: '#',
|
||||||
comment: " commentB",
|
comment: " commentB",
|
||||||
}),
|
}),
|
||||||
Event::Comment(Comment {
|
Event::Comment(ParsedComment {
|
||||||
comment_tag: ';',
|
comment_tag: ';',
|
||||||
comment: " commentC",
|
comment: " commentC",
|
||||||
}),
|
}),
|
||||||
Event::Comment(Comment {
|
Event::Comment(ParsedComment {
|
||||||
comment_tag: ';',
|
comment_tag: ';',
|
||||||
comment: " commentD",
|
comment: " commentD",
|
||||||
}),
|
}),
|
||||||
|
@ -651,8 +812,8 @@ mod parse {
|
||||||
// This test is absolute hell. Good luck if this fails.
|
// This test is absolute hell. Good luck if this fails.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section("[section] a = 1 \"\\\"\\\na ; e \"\\\"\\\nd # \"b\t ; c").unwrap(),
|
section("[section] a = 1 \"\\\"\\\na ; e \"\\\"\\\nd # \"b\t ; c").unwrap(),
|
||||||
fully_consumed(Section {
|
fully_consumed(ParsedSection {
|
||||||
section_header: SectionHeader {
|
section_header: ParsedSectionHeader {
|
||||||
name: "section",
|
name: "section",
|
||||||
subsection_name: None,
|
subsection_name: None,
|
||||||
},
|
},
|
||||||
|
@ -663,7 +824,7 @@ mod parse {
|
||||||
Event::ValueNotDone(r#"a ; e "\""#),
|
Event::ValueNotDone(r#"a ; e "\""#),
|
||||||
Event::Newline("\n"),
|
Event::Newline("\n"),
|
||||||
Event::ValueDone("d"),
|
Event::ValueDone("d"),
|
||||||
Event::Comment(Comment {
|
Event::Comment(ParsedComment {
|
||||||
comment_tag: '#',
|
comment_tag: '#',
|
||||||
comment: " \"b\t ; c"
|
comment: " \"b\t ; c"
|
||||||
})
|
})
|
||||||
|
@ -676,8 +837,8 @@ mod parse {
|
||||||
fn quote_split_over_two_lines() {
|
fn quote_split_over_two_lines() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
section("[section \"a\"] b =\"\\\n;\";a").unwrap(),
|
section("[section \"a\"] b =\"\\\n;\";a").unwrap(),
|
||||||
fully_consumed(Section {
|
fully_consumed(ParsedSection {
|
||||||
section_header: SectionHeader {
|
section_header: ParsedSectionHeader {
|
||||||
name: "section",
|
name: "section",
|
||||||
subsection_name: Some("a")
|
subsection_name: Some("a")
|
||||||
},
|
},
|
||||||
|
@ -686,7 +847,7 @@ mod parse {
|
||||||
Event::ValueNotDone("\""),
|
Event::ValueNotDone("\""),
|
||||||
Event::Newline("\n"),
|
Event::Newline("\n"),
|
||||||
Event::ValueDone(";\""),
|
Event::ValueDone(";\""),
|
||||||
Event::Comment(Comment {
|
Event::Comment(ParsedComment {
|
||||||
comment: "a",
|
comment: "a",
|
||||||
comment_tag: ';'
|
comment_tag: ';'
|
||||||
})
|
})
|
||||||
|
|
474
src/values.rs
474
src/values.rs
|
@ -1,50 +1,129 @@
|
||||||
use std::convert::{Infallible, TryFrom};
|
use std::{borrow::Cow, fmt::Display, str::FromStr};
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
use serde::{Serialize, Serializer};
|
||||||
|
|
||||||
|
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||||
pub enum Value<'a> {
|
pub enum Value<'a> {
|
||||||
Boolean(Boolean),
|
Boolean(Boolean),
|
||||||
Integer(Integer),
|
Integer(Integer),
|
||||||
Color(Color),
|
Color(Color),
|
||||||
Other(&'a str),
|
Other(Cow<'a, str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Value<'a> {
|
impl<'a> Value<'a> {
|
||||||
pub fn from_str(s: &'a str) -> Self {
|
pub fn from_str(s: &'a str) -> Self {
|
||||||
Self::Other(s)
|
// if s.
|
||||||
|
Self::Other(Cow::Borrowed(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_string(s: String) -> Self {
|
||||||
|
Self::Other(Cow::Owned(s))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
impl Serialize for Value<'_> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Value::Boolean(b) => b.serialize(serializer),
|
||||||
|
Value::Integer(i) => i.serialize(serializer),
|
||||||
|
Value::Color(c) => c.serialize(serializer),
|
||||||
|
Value::Other(i) => i.serialize(serializer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo display for value
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||||
pub enum Boolean {
|
pub enum Boolean {
|
||||||
True(TrueVariant),
|
True(TrueVariant),
|
||||||
False(FalseVariant),
|
False(FalseVariant),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
// todo: Display for boolean
|
||||||
|
|
||||||
|
impl Serialize for Boolean {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Boolean::True(_) => serializer.serialize_bool(true),
|
||||||
|
Boolean::False(_) => serializer.serialize_bool(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Boolean {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Ok(v) = TrueVariant::from_str(value) {
|
||||||
|
return Ok(Self::True(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(v) = FalseVariant::from_str(value) {
|
||||||
|
return Ok(Self::False(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||||
pub enum TrueVariant {
|
pub enum TrueVariant {
|
||||||
Yes,
|
Yes,
|
||||||
On,
|
On,
|
||||||
True,
|
True,
|
||||||
One,
|
One,
|
||||||
/// For variables defined without a `= <value>`.
|
/// For variables defined without a `= <value>`. This can never be created
|
||||||
|
/// from the FromStr trait, as an empty string is false without context.
|
||||||
Implicit,
|
Implicit,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for TrueVariant {
|
impl Display for TrueVariant {
|
||||||
type Error = ();
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
Self::Yes => write!(f, "yes"),
|
||||||
match value {
|
Self::On => write!(f, "on"),
|
||||||
"yes" => Ok(Self::Yes),
|
Self::True => write!(f, "true"),
|
||||||
"on" => Ok(Self::On),
|
Self::One => write!(f, "one"),
|
||||||
"true" => Ok(Self::True),
|
Self::Implicit => write!(f, "(implicit)"),
|
||||||
"one" => Ok(Self::One),
|
|
||||||
_ => Err(()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
impl Serialize for TrueVariant {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_bool(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for TrueVariant {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||||
|
if value.eq_ignore_ascii_case("yes") {
|
||||||
|
Ok(Self::Yes)
|
||||||
|
} else if value.eq_ignore_ascii_case("on") {
|
||||||
|
Ok(Self::On)
|
||||||
|
} else if value.eq_ignore_ascii_case("true") {
|
||||||
|
Ok(Self::True)
|
||||||
|
} else if value.eq_ignore_ascii_case("one") {
|
||||||
|
Ok(Self::One)
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||||
pub enum FalseVariant {
|
pub enum FalseVariant {
|
||||||
No,
|
No,
|
||||||
Off,
|
Off,
|
||||||
|
@ -53,51 +132,178 @@ pub enum FalseVariant {
|
||||||
EmptyString,
|
EmptyString,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for FalseVariant {
|
impl Display for FalseVariant {
|
||||||
type Error = ();
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::No => write!(f, "no"),
|
||||||
|
Self::Off => write!(f, "off"),
|
||||||
|
Self::False => write!(f, "false"),
|
||||||
|
Self::Zero => write!(f, "0"),
|
||||||
|
Self::EmptyString => write!(f, "\"\""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
impl Serialize for FalseVariant {
|
||||||
match value {
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
"no" => Ok(Self::No),
|
where
|
||||||
"off" => Ok(Self::Off),
|
S: serde::Serializer,
|
||||||
"false" => Ok(Self::False),
|
{
|
||||||
"zero" => Ok(Self::Zero),
|
serializer.serialize_bool(false)
|
||||||
"" => Ok(Self::EmptyString),
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for FalseVariant {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||||
|
if value.eq_ignore_ascii_case("no") {
|
||||||
|
Ok(Self::No)
|
||||||
|
} else if value.eq_ignore_ascii_case("off") {
|
||||||
|
Ok(Self::Off)
|
||||||
|
} else if value.eq_ignore_ascii_case("false") {
|
||||||
|
Ok(Self::False)
|
||||||
|
} else if value.eq_ignore_ascii_case("zero") {
|
||||||
|
Ok(Self::Zero)
|
||||||
|
} else if value.is_empty() {
|
||||||
|
Ok(Self::EmptyString)
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||||
|
pub struct Integer {
|
||||||
|
value: i64,
|
||||||
|
suffix: Option<IntegerSuffix>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Integer {}
|
||||||
|
|
||||||
|
impl Display for Integer {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.value)?;
|
||||||
|
if let Some(suffix) = self.suffix {
|
||||||
|
write!(f, "{}", suffix)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Integer {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
if let Some(suffix) = self.suffix {
|
||||||
|
serializer.serialize_i64(self.value << suffix.bitwise_offset())
|
||||||
|
} else {
|
||||||
|
serializer.serialize_i64(self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo from str for integer
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||||
|
enum IntegerSuffix {
|
||||||
|
Kilo,
|
||||||
|
Mega,
|
||||||
|
Giga,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntegerSuffix {
|
||||||
|
fn bitwise_offset(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Kilo => 10,
|
||||||
|
Self::Mega => 20,
|
||||||
|
Self::Giga => 30,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for IntegerSuffix {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Kilo => write!(f, "k"),
|
||||||
|
Self::Mega => write!(f, "m"),
|
||||||
|
Self::Giga => write!(f, "g"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for IntegerSuffix {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(match self {
|
||||||
|
Self::Kilo => "k",
|
||||||
|
Self::Mega => "m",
|
||||||
|
Self::Giga => "g",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for IntegerSuffix {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"k" => Ok(Self::Kilo),
|
||||||
|
"m" => Ok(Self::Mega),
|
||||||
|
"g" => Ok(Self::Giga),
|
||||||
_ => Err(()),
|
_ => Err(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for Boolean {
|
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
|
||||||
let value = value.to_lowercase();
|
|
||||||
let value = value.as_str();
|
|
||||||
if let Ok(v) = TrueVariant::try_from(value) {
|
|
||||||
return Ok(Self::True(v));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(v) = FalseVariant::try_from(value) {
|
|
||||||
return Ok(Self::False(v));
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo!()
|
|
||||||
#[derive(PartialEq, Debug)]
|
|
||||||
pub struct Integer {}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
|
||||||
pub struct Color {
|
pub struct Color {
|
||||||
foreground: ColorValue,
|
foreground: Option<ColorValue>,
|
||||||
background: Option<ColorValue>,
|
background: Option<ColorValue>,
|
||||||
attributes: Vec<ColorAttribute>,
|
attributes: Vec<ColorAttribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
impl Display for Color {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(fg) = self.foreground {
|
||||||
|
fg.fmt(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, " ")?;
|
||||||
|
|
||||||
|
if let Some(bg) = self.background {
|
||||||
|
bg.fmt(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.attributes
|
||||||
|
.iter()
|
||||||
|
.map(|attr| write!(f, " ").and_then(|_| attr.fmt(f)))
|
||||||
|
.collect::<Result<_, _>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Color {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Color {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||||
enum ColorValue {
|
enum ColorValue {
|
||||||
Normal,
|
Normal,
|
||||||
Black,
|
Black,
|
||||||
|
@ -120,8 +326,92 @@ enum ColorValue {
|
||||||
Rgb(u8, u8, u8),
|
Rgb(u8, u8, u8),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
impl Display for ColorValue {
|
||||||
enum ColorAttribute {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Normal => write!(f, "normal"),
|
||||||
|
Self::Black => write!(f, "black"),
|
||||||
|
Self::BrightBlack => write!(f, "brightblack"),
|
||||||
|
Self::Red => write!(f, "red"),
|
||||||
|
Self::BrightRed => write!(f, "brightred"),
|
||||||
|
Self::Green => write!(f, "green"),
|
||||||
|
Self::BrightGreen => write!(f, "brightgreen"),
|
||||||
|
Self::Yellow => write!(f, "yellow"),
|
||||||
|
Self::BrightYellow => write!(f, "brightyellow"),
|
||||||
|
Self::Blue => write!(f, "blue"),
|
||||||
|
Self::BrightBlue => write!(f, "brightblue"),
|
||||||
|
Self::Magenta => write!(f, "magenta"),
|
||||||
|
Self::BrightMagenta => write!(f, "brightmagenta"),
|
||||||
|
Self::Cyan => write!(f, "cyan"),
|
||||||
|
Self::BrightCyan => write!(f, "brightcyan"),
|
||||||
|
Self::White => write!(f, "white"),
|
||||||
|
Self::BrightWhite => write!(f, "brightwhite"),
|
||||||
|
Self::Ansi(num) => num.fmt(f),
|
||||||
|
Self::Rgb(r, g, b) => write!(f, "#{:02x}{:02x}{:02x}", r, g, b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for ColorValue {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ColorValue {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let bright = s.starts_with("bright");
|
||||||
|
match s {
|
||||||
|
"normal" => return Ok(Self::Normal),
|
||||||
|
"black" if !bright => return Ok(Self::Black),
|
||||||
|
"black" if bright => return Ok(Self::BrightBlack),
|
||||||
|
"red" if !bright => return Ok(Self::Red),
|
||||||
|
"red" if bright => return Ok(Self::BrightRed),
|
||||||
|
"green" if !bright => return Ok(Self::Green),
|
||||||
|
"green" if bright => return Ok(Self::BrightGreen),
|
||||||
|
"yellow" if !bright => return Ok(Self::Yellow),
|
||||||
|
"yellow" if bright => return Ok(Self::BrightYellow),
|
||||||
|
"blue" if !bright => return Ok(Self::Blue),
|
||||||
|
"blue" if bright => return Ok(Self::BrightBlue),
|
||||||
|
"magenta" if !bright => return Ok(Self::Magenta),
|
||||||
|
"magenta" if bright => return Ok(Self::BrightMagenta),
|
||||||
|
"cyan" if !bright => return Ok(Self::Cyan),
|
||||||
|
"cyan" if bright => return Ok(Self::BrightCyan),
|
||||||
|
"white" if !bright => return Ok(Self::White),
|
||||||
|
"white" if bright => return Ok(Self::BrightWhite),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(v) = u8::from_str(s) {
|
||||||
|
return Ok(Self::Ansi(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.starts_with("#") {
|
||||||
|
let s = &s[1..];
|
||||||
|
if s.len() == 6 {
|
||||||
|
let rgb = (
|
||||||
|
u8::from_str_radix(&s[..2], 16),
|
||||||
|
u8::from_str_radix(&s[2..4], 16),
|
||||||
|
u8::from_str_radix(&s[4..], 16),
|
||||||
|
);
|
||||||
|
match rgb {
|
||||||
|
(Ok(r), Ok(g), Ok(b)) => return Ok(Self::Rgb(r, g, b)),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||||
|
pub enum ColorAttribute {
|
||||||
Bold,
|
Bold,
|
||||||
NoBold,
|
NoBold,
|
||||||
Dim,
|
Dim,
|
||||||
|
@ -138,5 +428,77 @@ enum ColorAttribute {
|
||||||
NoStrike,
|
NoStrike,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
impl Display for ColorAttribute {
|
||||||
struct Pathname<'a>(&'a str);
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Bold => write!(f, "bold"),
|
||||||
|
Self::NoBold => write!(f, "nobold"),
|
||||||
|
Self::Dim => write!(f, "dim"),
|
||||||
|
Self::NoDim => write!(f, "nodim"),
|
||||||
|
Self::Ul => write!(f, "ul"),
|
||||||
|
Self::NoUl => write!(f, "noul"),
|
||||||
|
Self::Blink => write!(f, "blink"),
|
||||||
|
Self::NoBlink => write!(f, "noblink"),
|
||||||
|
Self::Reverse => write!(f, "reverse"),
|
||||||
|
Self::NoReverse => write!(f, "noreverse"),
|
||||||
|
Self::Italic => write!(f, "italic"),
|
||||||
|
Self::NoItalic => write!(f, "noitalic"),
|
||||||
|
Self::Strike => write!(f, "strike"),
|
||||||
|
Self::NoStrike => write!(f, "nostrike"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for ColorAttribute {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(match self {
|
||||||
|
Self::Bold => "bold",
|
||||||
|
Self::NoBold => "nobold",
|
||||||
|
Self::Dim => "dim",
|
||||||
|
Self::NoDim => "nodim",
|
||||||
|
Self::Ul => "ul",
|
||||||
|
Self::NoUl => "noul",
|
||||||
|
Self::Blink => "blink",
|
||||||
|
Self::NoBlink => "noblink",
|
||||||
|
Self::Reverse => "reverse",
|
||||||
|
Self::NoReverse => "noreverse",
|
||||||
|
Self::Italic => "italic",
|
||||||
|
Self::NoItalic => "noitalic",
|
||||||
|
Self::Strike => "strike",
|
||||||
|
Self::NoStrike => "nostrike",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ColorAttribute {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let inverted = s.starts_with("no");
|
||||||
|
let mut parsed = &s[2..];
|
||||||
|
if parsed.starts_with("-") {
|
||||||
|
parsed = &parsed[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
match parsed {
|
||||||
|
"bold" if !inverted => Ok(Self::Bold),
|
||||||
|
"bold" if inverted => Ok(Self::NoBold),
|
||||||
|
"dim" if !inverted => Ok(Self::Dim),
|
||||||
|
"dim" if inverted => Ok(Self::NoDim),
|
||||||
|
"ul" if !inverted => Ok(Self::Ul),
|
||||||
|
"ul" if inverted => Ok(Self::NoUl),
|
||||||
|
"blink" if !inverted => Ok(Self::Blink),
|
||||||
|
"blink" if inverted => Ok(Self::NoBlink),
|
||||||
|
"reverse" if !inverted => Ok(Self::Reverse),
|
||||||
|
"reverse" if inverted => Ok(Self::NoReverse),
|
||||||
|
"italic" if !inverted => Ok(Self::Italic),
|
||||||
|
"italic" if inverted => Ok(Self::NoItalic),
|
||||||
|
"strike" if !inverted => Ok(Self::Strike),
|
||||||
|
"strike" if inverted => Ok(Self::NoStrike),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
82
tests/parser_integration_tests.rs
Normal file
82
tests/parser_integration_tests.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
use serde_git_config::parser::{parse_from_str, Event, ParsedSectionHeader, Parser};
|
||||||
|
use serde_git_config::values::Value;
|
||||||
|
|
||||||
|
fn fully_consumed<T>(t: T) -> (&'static str, T) {
|
||||||
|
("", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section_header(name: &'static str, subname: impl Into<Option<&'static str>>) -> Event<'static> {
|
||||||
|
Event::SectionHeader(ParsedSectionHeader {
|
||||||
|
name,
|
||||||
|
subsection_name: subname.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(name: &'static str) -> Event<'static> {
|
||||||
|
Event::Key(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value(value: &'static str) -> Event<'static> {
|
||||||
|
Event::Value(Value::from_str(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn personal_config() {
|
||||||
|
let config = r#"[user]
|
||||||
|
email = code@eddie.sh
|
||||||
|
name = Edward Shen
|
||||||
|
[core]
|
||||||
|
autocrlf = input
|
||||||
|
[push]
|
||||||
|
default = simple
|
||||||
|
[commit]
|
||||||
|
gpgsign = true
|
||||||
|
[gpg]
|
||||||
|
program = gpg
|
||||||
|
[url "ssh://git@github.com/"]
|
||||||
|
insteadOf = "github://"
|
||||||
|
[url "ssh://git@git.eddie.sh/edward/"]
|
||||||
|
insteadOf = "gitea://"
|
||||||
|
[pull]
|
||||||
|
ff = only
|
||||||
|
[init]
|
||||||
|
defaultBranch = master"#;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_from_str(config)
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![
|
||||||
|
section_header("user", None),
|
||||||
|
name("email"),
|
||||||
|
value("code@eddie.sh"),
|
||||||
|
name("name"),
|
||||||
|
value("Edward Shen"),
|
||||||
|
section_header("core", None),
|
||||||
|
name("autocrlf"),
|
||||||
|
value("input"),
|
||||||
|
section_header("push", None),
|
||||||
|
name("default"),
|
||||||
|
value("simple"),
|
||||||
|
section_header("commit", None),
|
||||||
|
name("gpgsign"),
|
||||||
|
value("true"),
|
||||||
|
section_header("gpg", None),
|
||||||
|
name("program"),
|
||||||
|
value("gpg"),
|
||||||
|
section_header("url", "ssh://git@github.com/"),
|
||||||
|
name("insteadOf"),
|
||||||
|
value("github://"),
|
||||||
|
section_header("url", "ssh://git@git.eddie.sh/edward/"),
|
||||||
|
name("insteadOf"),
|
||||||
|
value("gitea://"),
|
||||||
|
section_header("pull", None),
|
||||||
|
name("ff"),
|
||||||
|
value("only"),
|
||||||
|
section_header("init", None),
|
||||||
|
name("defaultBranch"),
|
||||||
|
value("master"),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
Reference in a new issue