trixy 0.4.0

A rust crate used to generate multi-language apis for your application
Documentation
/*
* Copyright (C) 2023 - 2024:
* The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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 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 GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/

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 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);
    }
}