feat(macros/config/file_tree): Add support for parsing a FileTree from file

This makes storing expected `FileTree`s as markdown possible (useful in
tests).
This commit is contained in:
Benedikt Peetz 2024-03-26 15:56:55 +01:00
parent ec929dabe5
commit 89fd67c45e
Signed by: bpeetz
GPG Key ID: A5E94010C3A642AD
8 changed files with 273 additions and 63 deletions

View File

@ -29,7 +29,7 @@ proc-macro2 = {version = "1.0.79", optional = true}
quote = {version = "1.0.35", optional = true} quote = {version = "1.0.35", optional = true}
syn = { version = "2.0.55", features = ["extra-traits", "full", "parsing"], optional = true } syn = { version = "2.0.55", features = ["extra-traits", "full", "parsing"], optional = true }
thiserror = { version = "1.0.58", optional = true} thiserror = { version = "1.0.58", optional = true}
pulldown-cmark = {version = "0.10.0", optional = true}
# macros # macros
prettyplease = {version = "0.2.17", optional = true} prettyplease = {version = "0.2.17", optional = true}
@ -41,14 +41,13 @@ libc ={ version = "0.2.153", optional = true}
log = { version = "0.4.21", optional = true} log = { version = "0.4.21", optional = true}
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.81"
# parser # parser
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
[features] [features]
default = ["parser", "types", "macros", "build-binary"] # default = ["parser", "types", "macros", "build-binary"]
# default = ["parser", "types", "macros"] default = ["parser", "types", "macros"]
build-binary = ["clap", "parser", "types", "macros"] build-binary = ["parser", "types", "macros", "clap", "pulldown-cmark"]
parser = [ "regex", "thiserror", "convert_case" ] parser = [ "regex", "thiserror", "convert_case" ]
types = [ "parser", "libc", "log", "proc-macro2", "quote", "syn", "thiserror", "convert_case" ] types = [ "parser", "libc", "log", "proc-macro2", "quote", "syn", "thiserror", "convert_case" ]

View File

@ -0,0 +1,212 @@
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", self.path.display()))?;
f.write_fmt(format_args!("```{}\n", self.language))?;
f.write_fmt(format_args!("{}", &self.value))?;
f.write_str("```\n\n")
}
}
impl Display for FileTree {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if !self.host_files.is_empty() {
f.write_str("# Host files\n")?;
self.host_files
.iter()
.map(|file| -> std::fmt::Result { f.write_str(&file.to_string()) })
.collect::<std::fmt::Result>()?;
}
if !self.auxiliary_files.is_empty() {
f.write_str("# Auxiliary files\n")?;
self.auxiliary_files
.iter()
.map(|file| -> std::fmt::Result { 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 the extra heading close node
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 the Start(Paragraph) (already removed in the calling function)
// remove_event(iter, Event::Start(Tag::Paragraph))?;
// Remove the Text(Borrowed("File path: "))
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 the End(Paragraph)
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 the End(CodeBlock)
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| {
// Improves the error readability
panic!("{}", err);
})
.unwrap();
assert_eq!(output, file_tree);
}
}

View File

@ -2,16 +2,17 @@
//! you. //! you.
use std::{ use std::{
fmt::Display,
fs, io, fs, io,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use super::trixy::Language; use super::trixy::Language;
pub mod markdown_representation;
/// A file tree containing all files that were generated. These are separated into host and /// A file tree containing all files that were generated. These are separated into host and
/// auxiliary files. See their respective descriptions about what differentiates them. /// auxiliary files. See their respective descriptions about what differentiates them.
#[derive(Default, Debug)] #[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct FileTree { pub struct FileTree {
/// Files, that are supposed to be included in the compiled crate. /// Files, that are supposed to be included in the compiled crate.
pub host_files: Vec<GeneratedFile>, pub host_files: Vec<GeneratedFile>,
@ -21,7 +22,7 @@ pub struct FileTree {
} }
/// A generated files /// A generated files
#[derive(Default, Debug)] #[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct GeneratedFile { pub struct GeneratedFile {
/// The path this generated file would like to be placed at. /// The path this generated file would like to be placed at.
/// This path is relative to the crate root. /// This path is relative to the crate root.
@ -77,6 +78,16 @@ impl FileTree {
self.auxiliary_files.push(file) self.auxiliary_files.push(file)
} }
pub fn extend_host(&mut self, files: Vec<GeneratedFile>) {
files.into_iter().for_each(|file| self.add_host_file(file));
}
pub fn extend_auxiliary(&mut self, files: Vec<GeneratedFile>) {
files
.into_iter()
.for_each(|file| self.add_auxiliary_file(file));
}
pub fn materialize(self) -> io::Result<()> { pub fn materialize(self) -> io::Result<()> {
self.host_files self.host_files
.into_iter() .into_iter()
@ -89,39 +100,3 @@ impl FileTree {
Ok(()) Ok(())
} }
} }
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", self.path.display()))?;
f.write_fmt(format_args!("```{}\n", self.language))?;
f.write_fmt(format_args!("{}\r", &self.value))?;
f.write_str("```\n\n")
}
}
impl Display for FileTree {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("# Host files\n")?;
self.host_files
.iter()
.map(|file| -> std::fmt::Result { f.write_str(&file.to_string()) })
.collect::<std::fmt::Result>()?;
f.write_str("# Auxiliary files\n")?;
self.auxiliary_files
.iter()
.map(|file| -> std::fmt::Result { f.write_str(&file.to_string()) })
.collect::<std::fmt::Result>()
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2023 - 2024:
* The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* This file is part of the Trixy crate for Trinitrix.
*
* Trixy is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* and the Lesser GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/
fn print(message: String);
mod trinitrix {
fn hi(name: String) -> String;
}
// That's a flat out lie, but it results in a rather nice syntax highlight compared to nothing:
// vim: syntax=rust

View File

@ -39,9 +39,9 @@ pub fn generate(trixy: &CommandSpec, config: &TrixyConfig) -> String {
let c_code = format_c(c_code); let c_code = format_c(c_code);
format!( format!(
" "\
{}\n{} {}\n\
", {}",
c_code, VIM_LINE_C c_code, VIM_LINE_C
) )
} }

View File

@ -32,10 +32,9 @@ pub fn generate(trixy: &CommandSpec, config: &TrixyConfig) -> String {
let rust_code = format_rust(host_rust_code); let rust_code = format_rust(host_rust_code);
format!( format!(
" "\
/* c API */ /* C API */\n\
{} {}",
",
rust_code rust_code
) )
} }

View File

@ -21,14 +21,11 @@ pub fn generate(trixy: &CommandSpec, config: &TrixyConfig) -> String {
let c_host = c::generate(trixy, config); let c_host = c::generate(trixy, config);
format!( format!(
" "\
// Host code {{{{{{ // Host code\n\
{} {}\
/* C API */ {}\
{} {}",
// }}}}}}
{}
",
rust_host, c_host, VIM_LINE_RUST rust_host, c_host, VIM_LINE_RUST
) )
} }

View File

@ -31,10 +31,9 @@ pub fn generate(trixy: &CommandSpec, config: &TrixyConfig) -> String {
let rust_code = format_rust(host_rust_code); let rust_code = format_rust(host_rust_code);
format!( format!(
" "\
/* Rust API */ /* Rust API */\n\
{} {}",
",
rust_code rust_code
) )
} }