use std::{fmt::Display, path::PathBuf, str::FromStr};
use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Parser, Tag, TagEnd};
use thiserror::Error;
use crate::macros::config::trixy::Language;
use super::{FileTree, GeneratedFile};
#[derive(Debug, Error)]
pub enum FileTreeParseError {
#[error("Your Header has the wrong content: {0}")]
WrongHeader(String),
#[error("Your language is not recognized: {0}")]
WrongLanguage(String),
#[error("A path seems to be missing from your input data")]
NoPath,
#[error("A language attribute seems to be missing from your input data")]
NoLanguage,
#[error("A value seems to be missing from your input data")]
NoValue,
#[error("I exected: \n```\n{expected}\n```\nbut recieved:\n```\n{got}\n```")]
EventNotExpected { expected: String, got: String },
}
impl FromStr for Language {
type Err = FileTreeParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"rust" => Ok(Self::Rust),
"c" => Ok(Self::C),
"lua" => Ok(Self::Lua),
other => Err(Self::Err::WrongLanguage(other.to_owned())),
}
}
}
impl Display for Language {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self {
Language::Rust => f.write_str("rust"),
Language::C => f.write_str("c"),
Language::Lua => f.write_str("lua"),
Language::All => unreachable!("The `all` language variant should never be displayed"),
}
}
}
impl Display for GeneratedFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("File path: `{}`\n\n", self.path.display()))?;
f.write_fmt(format_args!("```{}\n", self.language))?;
f.write_fmt(format_args!("{}", &self.value))?;
f.write_str("```")
}
}
impl Display for FileTree {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut first = true;
if !self.host_files.is_empty() {
f.write_str("# Host files\n\n")?;
self.host_files
.iter()
.map(|file| -> std::fmt::Result {
if !first {
f.write_str("\n\n")?;
} else {
first = false;
}
f.write_str(&file.to_string())
})
.collect::<std::fmt::Result>()?;
}
first = true;
if !self.auxiliary_files.is_empty() {
if !self.host_files.is_empty() {
f.write_str("\n\n")?;
}
f.write_str("# Auxiliary files\n\n")?;
self.auxiliary_files
.iter()
.map(|file| -> std::fmt::Result {
if !first {
f.write_str("\n\n")?;
} else {
first = false;
}
f.write_str(&file.to_string())
})
.collect::<std::fmt::Result>()?;
}
Ok(())
}
}
impl FromStr for FileTree {
type Err = FileTreeParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parser = Parser::new(s);
let iter = parser.into_iter();
parse_start(iter)
}
}
fn parse_start(mut iter: Parser<'_>) -> Result<FileTree, FileTreeParseError> {
let mut file_tree = FileTree::new();
if let Some(Event::Start(Tag::Heading { .. })) = iter.next() {
while let Some(Event::Text(CowStr::Borrowed(text))) = iter.next() {
match text {
"Host files" => {
let files = parse_files(&mut iter)?;
file_tree.extend_host(files);
}
"Auxiliary files" => {
let files = parse_files(&mut iter)?;
file_tree.extend_auxiliary(files);
}
_ => return Err(FileTreeParseError::WrongHeader(text.to_owned())),
};
}
};
debug_assert_eq!(iter.next(), None, "Should be empty at this point");
Ok(file_tree)
}
fn parse_files(iter: &mut Parser<'_>) -> Result<Vec<GeneratedFile>, FileTreeParseError> {
remove_event(
iter,
Event::End(pulldown_cmark::TagEnd::Heading(HeadingLevel::H1)),
)?;
let mut files: Vec<GeneratedFile> = vec![];
while let Some(Event::Start(Tag::Paragraph)) = iter.next() {
files.push(make_generated_file(iter)?);
}
Ok(files)
}
fn make_generated_file(iter: &mut Parser<'_>) -> Result<GeneratedFile, FileTreeParseError> {
remove_event(iter, Event::Text(CowStr::Borrowed("File path: ")))?;
let file_path: PathBuf = if let Some(Event::Code(CowStr::Borrowed(path))) = iter.next() {
path.into()
} else {
return Err(FileTreeParseError::NoPath);
};
remove_event(iter, Event::End(TagEnd::Paragraph))?;
let file_language: Language = if let Some(Event::Start(Tag::CodeBlock(
CodeBlockKind::Fenced(CowStr::Borrowed(language)),
))) = iter.next()
{
language.parse()?
} else {
return Err(FileTreeParseError::NoLanguage);
};
let file_value: String = if let Some(Event::Text(CowStr::Borrowed(value))) = iter.next() {
value.into()
} else {
return Err(FileTreeParseError::NoValue);
};
remove_event(iter, Event::End(TagEnd::CodeBlock))?;
Ok(GeneratedFile {
path: file_path,
value: file_value,
language: file_language,
})
}
fn remove_event(iter: &mut Parser<'_>, event: Event) -> Result<(), FileTreeParseError> {
let a: Vec<Event> = iter.take(1).collect();
if a.first().expect("Should always contain a value") != &event {
let expected = format!("{:#?}", event);
let got = format!("{:#?}", a.first().unwrap());
return Err(FileTreeParseError::EventNotExpected { expected, got });
};
Ok(())
}
#[cfg(test)]
mod test {
use std::path::PathBuf;
use pretty_assertions::assert_eq;
use crate::macros::config::{file_tree::FileTree, trixy::TrixyConfig};
const API_FILE_PATH: &str = "./src/macros/config/file_tree/test_api.tri";
#[test]
fn test_round_trip() {
let base_config = TrixyConfig::new("callback_function")
.trixy_path(Into::<PathBuf>::into(API_FILE_PATH))
.dist_dir_path("dist")
.out_dir_path("out/dir");
let file_tree = base_config.generate();
let input = file_tree.to_string();
let output: FileTree = input
.parse()
.map_err(|err| {
panic!("{}", err);
})
.unwrap();
assert_eq!(output, file_tree);
}
}