use std::{ borrow::Cow, fmt::{self, Display}, }; #[cfg(test)] use proptest::prelude::*; use proptest_derive::Arbitrary; use winnow::{Parser, error::ParserError}; #[derive(Copy, Clone, Debug, PartialEq)] pub enum Separator { Eq, Colon, } impl Separator { fn parser<'a, E: ParserError<&'a str>>(input: &mut &'a str) -> Result { use winnow::{combinator::*, prelude::*, token::*}; alt(( literal('=').value(Self::Eq), literal(':').value(Self::Colon), )) .parse_next(input) } } impl Display for Separator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Separator::Eq => write!(f, "="), Separator::Colon => write!(f, ":"), } } } #[derive(Copy, Clone, Debug, Arbitrary, PartialEq)] pub enum QuoteType { Single, Double, Backtick, } impl QuoteType { fn parse<'a, E: ParserError<&'a str>>(input: &mut &'a str) -> Result { use winnow::{combinator::*, prelude::*, token::*}; alt(( literal('\'').value(Self::Single), literal('\"').value(Self::Double), literal('`').value(Self::Backtick), )) .parse_next(input) } } impl Display for QuoteType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { QuoteType::Single => write!(f, "\""), QuoteType::Double => write!(f, "\'"), QuoteType::Backtick => write!(f, "`"), } } } #[derive(Clone, Debug, Arbitrary, PartialEq)] pub enum Delimiter { Paren, Bracket, Brace, Angle, } impl Delimiter { fn fmt_start(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Delimiter::Paren => write!(f, "("), Delimiter::Bracket => write!(f, "["), Delimiter::Brace => write!(f, "{{"), Delimiter::Angle => write!(f, "<"), } } fn fmt_end(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Delimiter::Paren => write!(f, ")"), Delimiter::Bracket => write!(f, "]"), Delimiter::Brace => write!(f, "}}"), Delimiter::Angle => write!(f, ">"), } } } #[derive(Clone, Debug, PartialEq)] pub struct AnyString<'a> { prefix: Cow<'a, str>, ty: QuoteType, contents: Cow<'a, str>, num_hashtags: usize, suffix: Cow<'a, str>, } impl AnyString<'static> { #[cfg(test)] fn arb() -> impl Strategy { let prefix = "\\w*"; let ty = any::(); let contents = "\\w*"; let num_hashtags = 0usize..3; let suffix = "\\w*"; (prefix, ty, contents, num_hashtags, suffix).prop_map( |(prefix, ty, contents, num_hashtags, suffix)| Self { prefix: prefix.into(), ty, contents: contents.into(), num_hashtags, suffix: suffix.into(), }, ) } } impl<'a> AnyString<'a> { fn parse>() -> impl Parser<&'a str, Self, E> { use winnow::{combinator::*, prelude::*, token::*}; // let (prefix, num_hashtags, quote) = let preamble = ( take_while(0.., |b: char| !b.is_whitespace()), take_while(0.., |c| c == '#').map(|i: &'a str| i.len()), alt(( '`'.value(QuoteType::Backtick), '\''.value(QuoteType::Single), '\"'.value(QuoteType::Double), )), ); preamble.flat_map( |(prefix, num_hashtags, quote): (&'a str, usize, QuoteType)| { let end = ( match quote { QuoteType::Single => '\'', QuoteType::Double => '\"', QuoteType::Backtick => '`', }, repeat::<_, _, Cow<'a, str>, _, _>(num_hashtags..=num_hashtags, literal("#")), ); let contents = repeat_till(0.., any, end).map(|(contents, _)| contents); let suffix = take_while(0.., |b: char| !b.is_whitespace()); (contents, suffix).map(move |(contents, suffix): (Cow<'a, str>, &'a str)| Self { prefix: prefix.into(), ty: quote, contents, num_hashtags, suffix: suffix.into(), }) }, ) } } impl<'a> Display for AnyString<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.prefix)?; for _ in 0..self.num_hashtags { write!(f, "#")?; } write!(f, "{}", self.ty)?; write!(f, "{}", self.contents)?; for _ in 0..self.num_hashtags { write!(f, "#")?; } write!(f, "{}", self.suffix) } } #[derive(Clone, Debug, PartialEq)] pub struct Space<'a>(Cow<'a, str>); impl Space<'static> { #[cfg(test)] fn arb() -> impl Strategy { " *".prop_map(|spaces| Self(spaces.into())) } } impl<'a> Space<'a> { fn parse>() -> impl Parser<&'a str, Self, E> { use winnow::{prelude::*, token::*}; take_while(0.., |b: char| b.is_whitespace()).map(|i: &'a str| Self(i.into())) } } impl<'a> Display for Space<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } #[derive(Copy, Clone, Debug, PartialEq, Arbitrary)] pub enum PathSep { Slash, Backslash, } impl PathSep { fn parse<'a, E: ParserError<&'a str>>() -> impl Parser<&'a str, Self, E> { use winnow::{combinator::*, prelude::*}; alt(('/'.value(Self::Slash), '\\'.value(Self::Backslash))) } } impl Display for PathSep { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { PathSep::Slash => write!(f, "/"), PathSep::Backslash => write!(f, "\\"), } } } #[derive(Clone, Debug, PartialEq)] pub struct PathSegment<'a> { leading_separator: PathSep, segment: Cow<'a, str>, } impl PathSegment<'static> { #[cfg(test)] fn arb() -> impl Strategy { (any::(), "\\w*").prop_map(|(leading_separator, segment)| Self { leading_separator, segment: segment.into(), }) } } impl<'a> Display for PathSegment<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.leading_separator)?; write!(f, "{}", self.segment) } } #[derive(Clone, Debug, PartialEq)] pub struct FileLocation<'a> { line: Cow<'a, str>, offset: Option>, } impl FileLocation<'static> { #[cfg(test)] fn arb() -> impl Strategy { use proptest::option::*; ("[0-9]{0,4}", of("[0-9]{0,4}")).prop_map(|(line, offset)| Self { line: line.into(), offset: offset.map(Into::into), }) } } impl<'a> Display for FileLocation<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.line)?; if let Some(offset) = &self.offset { write!(f, "{offset}")?; } Ok(()) } } #[derive(Clone, Debug, PartialEq)] pub struct FileName<'a> { leading_separator: PathSep, segment: Cow<'a, str>, ext_excluding_dot: Option>, location: Option>, } impl FileName<'static> { #[cfg(test)] fn arb() -> impl Strategy { use proptest::option::*; ( any::(), "\\w*", of(".{0,3}"), of(FileLocation::arb()), ) .prop_map( |(leading_separator, segment, ext_excluding_dot, location)| Self { leading_separator, segment: segment.into(), ext_excluding_dot: ext_excluding_dot.map(Into::into), location, }, ) } } impl<'a> FileName<'a> { fn parse(segment: PathSegment<'a>) -> Self { fn rsplit<'a>( input: Cow<'a, str>, delimiter: char, ) -> Option<(Cow<'a, str>, Cow<'a, str>)> { match input { Cow::Borrowed(s) => s .rsplit_once(delimiter) .map(|(a, b)| (Cow::Borrowed(a), Cow::Borrowed(b))), Cow::Owned(s) => s .rsplit_once(delimiter) .map(|(a, b)| (Cow::Owned(a.to_string()), Cow::Owned(b.to_string()))), } } let (rest, location) = if let Some((before, offset)) = rsplit(segment.segment.clone(), ':') { if let Some((before, line)) = rsplit(before.clone(), ':') { ( before, Some(FileLocation { line, offset: Some(offset), }), ) } else { ( before, Some(FileLocation { line: offset, offset: None, }), ) } } else { (segment.segment, None) }; let (new_segment, ext_excluding_dot) = if let Some((segment, ext_excluding_dot)) = rsplit(rest.clone(), '.') { (segment, Some(ext_excluding_dot)) } else { (rest, None) }; Self { leading_separator: segment.leading_separator, segment: new_segment, ext_excluding_dot, location, } } } impl<'a> Display for FileName<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.leading_separator)?; write!(f, "{}", self.segment)?; if let Some(ext) = &self.ext_excluding_dot { write!(f, ".{ext}")?; } if let Some(loc) = &self.location { write!(f, "{loc}")?; } Ok(()) } } #[derive(Clone, Debug, PartialEq)] pub struct Path<'a> { drive_excluding_colon: Option, segments: Vec>, filename: FileName<'a>, } impl Path<'static> { #[cfg(test)] fn arb() -> impl Strategy { use proptest::{char::*, collection::*, option::*}; ( of(range('A', 'Z')), vec(PathSegment::arb(), 0..3), FileName::arb(), ) .prop_map(|(drive_excluding_colon, segments, filename)| Self { drive_excluding_colon, segments, filename, }) } } impl<'a> Path<'a> { fn parse>() -> impl Parser<&'a str, Self, E> { use winnow::{combinator::*, prelude::*, token::*}; let till_next_sep = repeat_till( 0.., any::<&'a str, E>.verify(|i: &char| !(*i).is_whitespace()), PathSep::parse(), ) .map(|(segment, _)| segment); let sep_and_next = (PathSep::parse(), till_next_sep).map(|(leading_separator, segment)| PathSegment { leading_separator, segment, }); let drive = opt(( any::<&'a str, E>.verify(|x: &char| matches!(*x, 'A'..='Z')), ':', )) .map(|i| i.map(|(letter, _): (char, char)| letter)); let drive_and_segments = ( drive, repeat_till( 1.., sep_and_next, any.verify(|i: &char| (*i).is_whitespace()), ) .map(|(segments, _): (Vec, _)| { let (rest, last) = { let mut segments = segments; let last = segments.pop().unwrap(); (segments, last) }; (rest, FileName::parse(last)) }), ); drive_and_segments.map(|(drive, (segments, filename))| Self { drive_excluding_colon: drive, segments, filename, }) } } impl<'a> Display for Path<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(drive) = &self.drive_excluding_colon { write!(f, "{drive}:")?; } for segment in &self.segments { write!(f, "{segment}:")?; } write!(f, "{}", self.filename) } } #[derive(Clone, Debug, PartialEq)] pub struct Number<'a>(Cow<'a, str>); impl Number<'static> { #[cfg(test)] fn arb() -> impl Strategy { prop_oneof![ any::().prop_map(|number| Self(number.to_string().into())), any::().prop_map(|number| Self(number.to_string().into())) ] } } impl<'a> Number<'a> { fn parse>() -> impl Parser<&'a str, Self, E> { use winnow::{ascii::*, combinator::*, prelude::*}; alt((float::<_, f64, _>.take(), dec_int::<_, i64, _>.take())).map(|i: &str| Self(i.into())) } } impl<'a> Display for Number<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } /// Anything that doesn't contain spaces, and that can be a prefix of `Delimited`. /// i.e. an english word, or rust `::`-separated Path #[derive(Clone, Debug, PartialEq)] pub enum Atom<'a> { Text(Cow<'a, str>), } impl<'a> Display for Atom<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Atom::Text(text) => write!(f, "{text}"), } } } impl Atom<'static> { #[cfg(test)] fn arb() -> impl Strategy { "\\w*".prop_map(|i| Self::Text(i.into())) } } impl<'a> Atom<'a> { fn parse>() -> impl Parser<&'a str, Self, E> { use winnow::{combinator::*, prelude::*, token::*}; alt((repeat(0.., any.verify(|i: &char| !(*i).is_whitespace())).map(Self::Text),)) } } #[derive(Clone, Debug, PartialEq)] pub enum Token<'a> { True, False, None, Path(Path<'a>), String(AnyString<'a>), Number(Number<'a>), // TODO: RustPath Separated { before: Box>, space_before: Space<'a>, separator: Separator, after: Box>, }, Delimited(Delimited<'a>), Atom(Atom<'a>), } impl Token<'static> { #[cfg(test)] fn arb() -> impl Strategy { let leaf = prop_oneof![ Just(Self::True), Just(Self::False), Just(Self::None), Path::arb().prop_map(Self::Path), AnyString::arb().prop_map(Self::String), Number::arb().prop_map(Self::Number), Atom::arb().prop_map(Self::Atom), ]; leaf.prop_recursive(4, 64, 16, |token| { Delimited::arb(token).prop_map(Self::Delimited).boxed() }) } } impl<'a> Token<'a> { fn parse + 'a>() -> impl Parser<&'a str, Self, E> { use winnow::{combinator::*, prelude::*}; let delimited: Box> = Box::new(Delimited::parse().map(Self::Delimited)); alt(( "true".value(Self::True), "false".value(Self::False), "None".value(Self::None), Path::parse().map(Self::Path), AnyString::parse().map(Self::String), Number::parse().map(Self::Number), delimited, Atom::parse().map(Self::Atom), )) } } impl<'a> Display for Token<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Token::Number(number) => write!(f, "{number}"), Token::True => write!(f, "true"), Token::False => write!(f, "false"), Token::None => write!(f, "None"), Token::Atom(atom) => write!(f, "{atom}"), Token::Path(path) => write!(f, "{path}"), Token::Separated { before, space_before, separator, after, } => write!(f, "{before}{space_before}{separator}{after}"), Token::Delimited(delimited) => write!(f, "{delimited}"), Token::String(s) => write!(f, "{s}"), } } } #[derive(Clone, Debug, PartialEq)] pub struct Delimited<'a> { prefix: Atom<'a>, delimiter: Delimiter, contents: Segments<'a>, } impl Delimited<'static> { #[cfg(test)] fn arb(token: impl Strategy>) -> impl Strategy { (Atom::arb(), any::(), Segments::arb(token)).prop_map( |(prefix, delimiter, contents)| Self { prefix, delimiter, contents, }, ) } } impl<'a> Delimited<'a> { fn parse + 'a>() -> impl Parser<&'a str, Self, E> { use winnow::{combinator::*, prelude::*, token::*}; ( Atom::parse(), alt(( literal('(').map(|_| literal(')').value(Delimiter::Paren)), literal('[').map(|_| literal(']').value(Delimiter::Bracket)), literal('{').map(|_| literal('}').value(Delimiter::Brace)), )) .flat_map(|end| Segments::parse(end)), ) .map(|(prefix, (contents, delimiter))| Self { prefix, delimiter, contents, }) } } impl<'a> Display for Delimited<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.prefix)?; self.delimiter.fmt_start(f)?; write!(f, "{}", self.contents)?; self.delimiter.fmt_end(f) } } #[derive(Clone, Debug, PartialEq)] pub struct Segment<'a> { leading_space: Space<'a>, token: Token<'a>, } impl Segment<'static> { #[cfg(test)] fn arb(token: impl Strategy>) -> impl Strategy { (Space::arb(), token).prop_map(|(leading_space, token)| Self { leading_space, token, }) } } impl<'a> Segment<'a> { fn parse + 'a>() -> impl Parser<&'a str, Self, E> { use winnow::prelude::*; (Space::parse(), Token::parse()).map(|(leading_space, token)| Self { leading_space, token, }) } } impl<'a> Display for Segment<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.leading_space)?; write!(f, "{}", self.token) } } #[derive(Clone, Debug, PartialEq)] pub struct Segments<'a> { segments: Vec>, trailing_space: Space<'a>, } impl Segments<'static> { #[cfg(test)] fn arb(token: impl Strategy>) -> impl Strategy { use proptest::collection::*; (vec(Segment::arb(token), 1..10), Space::arb()).prop_map(|(segments, trailing_space)| { Self { segments, trailing_space, } }) } } impl<'a> Segments<'a> { fn parse + 'a, End: 'a>( end: impl Parser<&'a str, End, E>, ) -> impl Parser<&'a str, (Self, End), E> { use winnow::{combinator::*, prelude::*}; repeat_till(0.., Segment::parse(), (Space::parse(), end)).map( |(segments, (trailing_space, end)): (Vec<_>, _)| { ( Self { segments, trailing_space, }, end, ) }, ) } } impl<'a> Display for Segments<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for segment in &self.segments { write!(f, "{segment}")?; } write!(f, "{}", self.trailing_space) } } fn parse_input<'a>(i: &'a str) -> Result, String> { use winnow::combinator::eof; Segments::parse(eof::<&str, winnow::error::EmptyError>) .map(|(segments, _)| segments) .parse(i) .map_err(|e| e.to_string()) } #[cfg(test)] mod tests { use proptest::proptest; use crate::format_debug_output::{Segments, Token, parse_input}; proptest! { #[test] fn test_some_function(original in Segments::arb(Token::arb())) { let stringified = original.to_string(); let parsed = parse_input(&stringified).unwrap(); assert_eq!(parsed, original, "left:\n `{parsed}`\nright: `{stringified}`"); } } }