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
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
nom = "6"
|
||||
|
||||
[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;
|
||||
pub mod config;
|
||||
mod error;
|
||||
// mod ser;
|
||||
pub mod parser;
|
||||
mod values;
|
||||
pub mod values;
|
||||
|
||||
// pub use de::{from_str, Deserializer};
|
||||
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::IResult;
|
||||
use nom::{branch::alt, multi::many0};
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
/// An event is any syntactic event that occurs in the config.
|
||||
#[derive(PartialEq, Debug)]
|
||||
/// Syntactic event that occurs in the config.
|
||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub enum Event<'a> {
|
||||
Comment(Comment<'a>),
|
||||
Comment(ParsedComment<'a>),
|
||||
SectionHeader(ParsedSectionHeader<'a>),
|
||||
Key(&'a str),
|
||||
///
|
||||
Value(Value<'a>),
|
||||
/// 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),
|
||||
///
|
||||
Value(Value<'a>),
|
||||
/// 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.
|
||||
|
@ -28,32 +30,188 @@ pub enum Event<'a> {
|
|||
ValueDone(&'a str),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct Section<'a> {
|
||||
section_header: SectionHeader<'a>,
|
||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||
pub struct ParsedSection<'a> {
|
||||
section_header: ParsedSectionHeader<'a>,
|
||||
items: Vec<Event<'a>>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct SectionHeader<'a> {
|
||||
name: &'a str,
|
||||
subsection_name: Option<&'a str>,
|
||||
impl ParsedSection<'_> {
|
||||
pub fn header(&self) -> &ParsedSectionHeader<'_> {
|
||||
&self.section_header
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct Comment<'a> {
|
||||
pub fn take_header(&mut self) -> ParsedSectionHeader<'_> {
|
||||
self.section_header
|
||||
}
|
||||
|
||||
pub fn events(&self) -> &[Event<'_>] {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||
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: &'a str,
|
||||
}
|
||||
|
||||
pub struct Parser<'a> {
|
||||
init_comments: Vec<Comment<'a>>,
|
||||
sections: Vec<Section<'a>>,
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum ParserError<'a> {
|
||||
InvalidInput(nom::Err<NomError<&'a str>>),
|
||||
ConfigHasExtraData(&'a str),
|
||||
}
|
||||
|
||||
pub fn parse(input: &str) -> Result<Parser<'_>, ()> {
|
||||
let (i, comments) = many0(comment)(input).unwrap();
|
||||
let (i, sections) = many1(section)(i).unwrap();
|
||||
#[doc(hidden)]
|
||||
impl<'a> From<nom::Err<NomError<&'a str>>> for ParserError<'a> {
|
||||
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 {
|
||||
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, comment_tag) = one_of(";#")(i)?;
|
||||
let (i, comment) = take_till(is_char_newline)(i)?;
|
||||
Ok((
|
||||
i,
|
||||
Comment {
|
||||
ParsedComment {
|
||||
comment_tag,
|
||||
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)?;
|
||||
// need alt here for eof?
|
||||
let (i, items) = many1(alt((
|
||||
map(section_body, |(key, values)| {
|
||||
let mut vec = vec![Event::Key(key)];
|
||||
|
@ -87,14 +245,14 @@ fn section<'a>(i: &'a str) -> IResult<&'a str, Section<'a>> {
|
|||
)))(i)?;
|
||||
Ok((
|
||||
i,
|
||||
Section {
|
||||
ParsedSection {
|
||||
section_header,
|
||||
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)?;
|
||||
// No spaces must be between section name and section start
|
||||
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
|
||||
// subsection syntax at this point.
|
||||
let header = match name.rfind('.') {
|
||||
Some(index) => SectionHeader {
|
||||
Some(index) => ParsedSectionHeader {
|
||||
name: &name[..index],
|
||||
subsection_name: Some(&name[index + 1..]),
|
||||
},
|
||||
None => SectionHeader {
|
||||
None => ParsedSectionHeader {
|
||||
name: name,
|
||||
subsection_name: None,
|
||||
},
|
||||
|
@ -128,7 +286,7 @@ fn section_header<'a>(i: &'a str) -> IResult<&'a str, SectionHeader<'a>> {
|
|||
|
||||
Ok((
|
||||
i,
|
||||
SectionHeader {
|
||||
ParsedSectionHeader {
|
||||
name: name,
|
||||
// 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.
|
||||
|
@ -288,7 +446,7 @@ mod parse {
|
|||
fn semicolon() {
|
||||
assert_eq!(
|
||||
comment("; this is a semicolon comment").unwrap(),
|
||||
fully_consumed(Comment {
|
||||
fully_consumed(ParsedComment {
|
||||
comment_tag: ';',
|
||||
comment: " this is a semicolon comment",
|
||||
})
|
||||
|
@ -299,7 +457,7 @@ mod parse {
|
|||
fn octothorpe() {
|
||||
assert_eq!(
|
||||
comment("# this is an octothorpe comment").unwrap(),
|
||||
fully_consumed(Comment {
|
||||
fully_consumed(ParsedComment {
|
||||
comment_tag: '#',
|
||||
comment: " this is an octothorpe comment",
|
||||
})
|
||||
|
@ -310,7 +468,7 @@ mod parse {
|
|||
fn multiple_markers() {
|
||||
assert_eq!(
|
||||
comment("###### this is an octothorpe comment").unwrap(),
|
||||
fully_consumed(Comment {
|
||||
fully_consumed(ParsedComment {
|
||||
comment_tag: '#',
|
||||
comment: "##### this is an octothorpe comment",
|
||||
})
|
||||
|
@ -326,7 +484,7 @@ mod parse {
|
|||
fn no_subsection() {
|
||||
assert_eq!(
|
||||
section_header("[hello]").unwrap(),
|
||||
fully_consumed(SectionHeader {
|
||||
fully_consumed(ParsedSectionHeader {
|
||||
name: "hello",
|
||||
subsection_name: None
|
||||
})
|
||||
|
@ -337,7 +495,7 @@ mod parse {
|
|||
fn modern_subsection() {
|
||||
assert_eq!(
|
||||
section_header(r#"[hello "world"]"#).unwrap(),
|
||||
fully_consumed(SectionHeader {
|
||||
fully_consumed(ParsedSectionHeader {
|
||||
name: "hello",
|
||||
subsection_name: Some("world")
|
||||
})
|
||||
|
@ -348,7 +506,7 @@ mod parse {
|
|||
fn escaped_subsection() {
|
||||
assert_eq!(
|
||||
section_header(r#"[hello "foo\\bar\""]"#).unwrap(),
|
||||
fully_consumed(SectionHeader {
|
||||
fully_consumed(ParsedSectionHeader {
|
||||
name: "hello",
|
||||
subsection_name: Some(r#"foo\\bar\""#)
|
||||
})
|
||||
|
@ -359,7 +517,7 @@ mod parse {
|
|||
fn deprecated_subsection() {
|
||||
assert_eq!(
|
||||
section_header(r#"[hello.world]"#).unwrap(),
|
||||
fully_consumed(SectionHeader {
|
||||
fully_consumed(ParsedSectionHeader {
|
||||
name: "hello",
|
||||
subsection_name: Some("world")
|
||||
})
|
||||
|
@ -370,7 +528,7 @@ mod parse {
|
|||
fn empty_legacy_subsection_name() {
|
||||
assert_eq!(
|
||||
section_header(r#"[hello.]"#).unwrap(),
|
||||
fully_consumed(SectionHeader {
|
||||
fully_consumed(ParsedSectionHeader {
|
||||
name: "hello",
|
||||
subsection_name: Some("")
|
||||
})
|
||||
|
@ -381,7 +539,7 @@ mod parse {
|
|||
fn empty_modern_subsection_name() {
|
||||
assert_eq!(
|
||||
section_header(r#"[hello ""]"#).unwrap(),
|
||||
fully_consumed(SectionHeader {
|
||||
fully_consumed(ParsedSectionHeader {
|
||||
name: "hello",
|
||||
subsection_name: Some("")
|
||||
})
|
||||
|
@ -402,7 +560,7 @@ mod parse {
|
|||
fn right_brace_in_subsection_name() {
|
||||
assert_eq!(
|
||||
section_header(r#"[hello "]"]"#).unwrap(),
|
||||
fully_consumed(SectionHeader {
|
||||
fully_consumed(ParsedSectionHeader {
|
||||
name: "hello",
|
||||
subsection_name: Some("]")
|
||||
})
|
||||
|
@ -439,7 +597,7 @@ mod parse {
|
|||
fn no_comment() {
|
||||
assert_eq!(
|
||||
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() {
|
||||
assert_eq!(
|
||||
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() {
|
||||
assert_eq!(
|
||||
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() {
|
||||
assert_eq!(
|
||||
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(),
|
||||
(
|
||||
"; 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() {
|
||||
assert_eq!(
|
||||
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""#;
|
||||
assert_eq!(
|
||||
section(section_data).unwrap(),
|
||||
fully_consumed(Section {
|
||||
section_header: SectionHeader {
|
||||
fully_consumed(ParsedSection {
|
||||
section_header: ParsedSectionHeader {
|
||||
name: "hello",
|
||||
subsection_name: None,
|
||||
},
|
||||
|
@ -593,8 +754,8 @@ mod parse {
|
|||
fn section_single_line() {
|
||||
assert_eq!(
|
||||
section("[hello] c").unwrap(),
|
||||
fully_consumed(Section {
|
||||
section_header: SectionHeader {
|
||||
fully_consumed(ParsedSection {
|
||||
section_header: ParsedSectionHeader {
|
||||
name: "hello",
|
||||
subsection_name: None,
|
||||
},
|
||||
|
@ -615,27 +776,27 @@ mod parse {
|
|||
c = d"#;
|
||||
assert_eq!(
|
||||
section(section_data).unwrap(),
|
||||
fully_consumed(Section {
|
||||
section_header: SectionHeader {
|
||||
fully_consumed(ParsedSection {
|
||||
section_header: ParsedSectionHeader {
|
||||
name: "hello",
|
||||
subsection_name: None,
|
||||
},
|
||||
items: vec![
|
||||
Event::Comment(Comment {
|
||||
Event::Comment(ParsedComment {
|
||||
comment_tag: ';',
|
||||
comment: " commentA",
|
||||
}),
|
||||
Event::Key("a"),
|
||||
Event::Value(Value::from_str("b")),
|
||||
Event::Comment(Comment {
|
||||
Event::Comment(ParsedComment {
|
||||
comment_tag: '#',
|
||||
comment: " commentB",
|
||||
}),
|
||||
Event::Comment(Comment {
|
||||
Event::Comment(ParsedComment {
|
||||
comment_tag: ';',
|
||||
comment: " commentC",
|
||||
}),
|
||||
Event::Comment(Comment {
|
||||
Event::Comment(ParsedComment {
|
||||
comment_tag: ';',
|
||||
comment: " commentD",
|
||||
}),
|
||||
|
@ -651,8 +812,8 @@ mod parse {
|
|||
// This test is absolute hell. Good luck if this fails.
|
||||
assert_eq!(
|
||||
section("[section] a = 1 \"\\\"\\\na ; e \"\\\"\\\nd # \"b\t ; c").unwrap(),
|
||||
fully_consumed(Section {
|
||||
section_header: SectionHeader {
|
||||
fully_consumed(ParsedSection {
|
||||
section_header: ParsedSectionHeader {
|
||||
name: "section",
|
||||
subsection_name: None,
|
||||
},
|
||||
|
@ -663,7 +824,7 @@ mod parse {
|
|||
Event::ValueNotDone(r#"a ; e "\""#),
|
||||
Event::Newline("\n"),
|
||||
Event::ValueDone("d"),
|
||||
Event::Comment(Comment {
|
||||
Event::Comment(ParsedComment {
|
||||
comment_tag: '#',
|
||||
comment: " \"b\t ; c"
|
||||
})
|
||||
|
@ -676,8 +837,8 @@ mod parse {
|
|||
fn quote_split_over_two_lines() {
|
||||
assert_eq!(
|
||||
section("[section \"a\"] b =\"\\\n;\";a").unwrap(),
|
||||
fully_consumed(Section {
|
||||
section_header: SectionHeader {
|
||||
fully_consumed(ParsedSection {
|
||||
section_header: ParsedSectionHeader {
|
||||
name: "section",
|
||||
subsection_name: Some("a")
|
||||
},
|
||||
|
@ -686,7 +847,7 @@ mod parse {
|
|||
Event::ValueNotDone("\""),
|
||||
Event::Newline("\n"),
|
||||
Event::ValueDone(";\""),
|
||||
Event::Comment(Comment {
|
||||
Event::Comment(ParsedComment {
|
||||
comment: "a",
|
||||
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> {
|
||||
Boolean(Boolean),
|
||||
Integer(Integer),
|
||||
Color(Color),
|
||||
Other(&'a str),
|
||||
Other(Cow<'a, str>),
|
||||
}
|
||||
|
||||
impl<'a> Value<'a> {
|
||||
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 {
|
||||
True(TrueVariant),
|
||||
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 {
|
||||
Yes,
|
||||
On,
|
||||
True,
|
||||
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,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for TrueVariant {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
"yes" => Ok(Self::Yes),
|
||||
"on" => Ok(Self::On),
|
||||
"true" => Ok(Self::True),
|
||||
"one" => Ok(Self::One),
|
||||
_ => Err(()),
|
||||
impl Display for TrueVariant {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Yes => write!(f, "yes"),
|
||||
Self::On => write!(f, "on"),
|
||||
Self::True => write!(f, "true"),
|
||||
Self::One => write!(f, "one"),
|
||||
Self::Implicit => write!(f, "(implicit)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
No,
|
||||
Off,
|
||||
|
@ -53,51 +132,178 @@ pub enum FalseVariant {
|
|||
EmptyString,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for FalseVariant {
|
||||
type Error = ();
|
||||
impl Display for FalseVariant {
|
||||
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> {
|
||||
match value {
|
||||
"no" => Ok(Self::No),
|
||||
"off" => Ok(Self::Off),
|
||||
"false" => Ok(Self::False),
|
||||
"zero" => Ok(Self::Zero),
|
||||
"" => Ok(Self::EmptyString),
|
||||
impl Serialize for FalseVariant {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_bool(false)
|
||||
}
|
||||
}
|
||||
|
||||
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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Boolean {
|
||||
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)]
|
||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub struct Color {
|
||||
foreground: ColorValue,
|
||||
foreground: Option<ColorValue>,
|
||||
background: Option<ColorValue>,
|
||||
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 {
|
||||
Normal,
|
||||
Black,
|
||||
|
@ -120,8 +326,92 @@ enum ColorValue {
|
|||
Rgb(u8, u8, u8),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum ColorAttribute {
|
||||
impl Display for ColorValue {
|
||||
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,
|
||||
NoBold,
|
||||
Dim,
|
||||
|
@ -138,5 +428,77 @@ enum ColorAttribute {
|
|||
NoStrike,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
struct Pathname<'a>(&'a str);
|
||||
impl Display for ColorAttribute {
|
||||
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