logviewer/src/format_debug_output.rs
2026-04-01 13:59:56 +02:00

756 lines
20 KiB
Rust

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<Self, E> {
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<Self, E> {
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<Value = Self> {
let prefix = "\\w*";
let ty = any::<QuoteType>();
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<E: ParserError<&'a str>>() -> 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<Value = Self> {
" *".prop_map(|spaces| Self(spaces.into()))
}
}
impl<'a> Space<'a> {
fn parse<E: ParserError<&'a str>>() -> 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<Value = Self> {
(any::<PathSep>(), "\\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<Cow<'a, str>>,
}
impl FileLocation<'static> {
#[cfg(test)]
fn arb() -> impl Strategy<Value = Self> {
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<Cow<'a, str>>,
location: Option<FileLocation<'a>>,
}
impl FileName<'static> {
#[cfg(test)]
fn arb() -> impl Strategy<Value = Self> {
use proptest::option::*;
(
any::<PathSep>(),
"\\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<char>,
segments: Vec<PathSegment<'a>>,
filename: FileName<'a>,
}
impl Path<'static> {
#[cfg(test)]
fn arb() -> impl Strategy<Value = Self> {
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<E: ParserError<&'a str>>() -> 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<PathSegment>, _)| {
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<Value = Self> {
prop_oneof![
any::<i64>().prop_map(|number| Self(number.to_string().into())),
any::<f64>().prop_map(|number| Self(number.to_string().into()))
]
}
}
impl<'a> Number<'a> {
fn parse<E: ParserError<&'a str>>() -> 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<Value = Self> {
"\\w*".prop_map(|i| Self::Text(i.into()))
}
}
impl<'a> Atom<'a> {
fn parse<E: ParserError<&'a str>>() -> 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<Token<'a>>,
space_before: Space<'a>,
separator: Separator,
after: Box<Segment<'a>>,
},
Delimited(Delimited<'a>),
Atom(Atom<'a>),
}
impl Token<'static> {
#[cfg(test)]
fn arb() -> impl Strategy<Value = Self> {
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<E: ParserError<&'a str> + 'a>() -> impl Parser<&'a str, Self, E> {
use winnow::{combinator::*, prelude::*};
let delimited: Box<dyn Parser<&'a str, Self, E>> =
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<Value = Token<'static>>) -> impl Strategy<Value = Self> {
(Atom::arb(), any::<Delimiter>(), Segments::arb(token)).prop_map(
|(prefix, delimiter, contents)| Self {
prefix,
delimiter,
contents,
},
)
}
}
impl<'a> Delimited<'a> {
fn parse<E: ParserError<&'a str> + '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<Value = Token<'static>>) -> impl Strategy<Value = Self> {
(Space::arb(), token).prop_map(|(leading_space, token)| Self {
leading_space,
token,
})
}
}
impl<'a> Segment<'a> {
fn parse<E: ParserError<&'a str> + '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<Segment<'a>>,
trailing_space: Space<'a>,
}
impl Segments<'static> {
#[cfg(test)]
fn arb(token: impl Strategy<Value = Token<'static>>) -> impl Strategy<Value = Self> {
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<E: ParserError<&'a str> + '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<Segments<'a>, 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}`");
}
}
}