nrcc_core 0.1.1

A code counter.
Documentation
use std::{
    collections::BTreeMap,
    env,
    fs::{self, File},
    path::Path,
};

use regex::Regex;
use serde::{Deserialize, Serialize};
use tera::{Context, Tera};

fn main() -> anyhow::Result<()> {
    let template = {
        let mut t = Tera::default();
        t.add_template_file("./templates/tests.tera.rs", Some("tests"))
            .expect("Error loading test template");
        t.add_template_file("./templates/language_type.tera.rs", Some("lang_type"))
            .expect("Error loading languages template");
        t.add_template_file("./templates/language_syntax.tera.rs", Some("syntax"))
            .expect("Error loading languages syntax template");
        t.add_template_file("./templates/language_file.tera.rs", Some("file"))
            .expect("Error loading languages file definition template");
        t
    };

    generate_language(&template)?;
    if Path::new("./tests/").exists() {
        generate_tests(&template)?;
    } else {
        fs::write(Path::new(&env::var("OUT_DIR")?).join("tests_tera.rs"), "")?;
    }
    Ok(())
}

fn generate_tests(template: &Tera) -> anyhow::Result<()> {
    let out_dir = env::var("OUT_DIR")?;
    let tests: BTreeMap<String, TestLang> = serde_yaml::from_reader(
        File::open("./tests/test_config.yaml").expect("Error loading test config"),
    )
    .expect("Error loading test config");

    #[derive(Serialize)]
    struct Language {
        name: String,
        ident: String,
        file: String,
        predict: String,
        detect: Vec<String>,
    }
    #[derive(Serialize)]
    struct TestContext {
        languages: Vec<Language>,
    }

    let context = {
        let mut v = Vec::new();
        for (k, d) in tests {
            v.push(Language {
                ident: k,
                name: d.name,
                file: d.file,
                predict: d.stats.ser(0),
                detect: d.file_detect
            })
        }
        v
    };

    let context = Context::from_serialize(TestContext { languages: context })?;

    let result = template
        .render("tests", &context)
        .expect("Error rendering test config");

    let output_path = Path::new(&out_dir).join("tests_tera.rs");
    fs::write(output_path, result).expect("Error writing test config");

    Ok(())
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct TestLang {
    name: String,
    file: String,
    stats: TestData,
    #[serde(default = "empty_vec")]
    file_detect: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct TestData {
    code: usize,
    blank: usize,
    all: usize,
    sub_language: BTreeMap<String, TestData>,
    comment: TestCommentData,
}

impl TestData {
    fn ser(&self, level: usize) -> String {
        format!(
            "ParseResult {{
    code: {},
    blank: {},
    all: {},
    comment: CommentResult {{
        doc: {},
        normal: {},
        doc_quote: {},
    }},
    sub_language: {{
        #[allow(unused_mut)]
        let mut m{} = std::collections::BTreeMap::new();
        {}
        m{}
    }}
}}",
            self.code,
            self.blank,
            self.all,
            self.comment.doc,
            self.comment.normal,
            self.comment.doc_quote,
            level,
            self.sub_language
                .iter()
                .map(|(k, v)| format!(
                    "m{}.insert(LanguageType::{}, {});",
                    level,
                    k,
                    v.ser(level + 1)
                ))
                .collect::<Vec<_>>()
                .join(""),
            level
        )
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct TestCommentData {
    doc: usize,
    normal: usize,
    doc_quote: usize,
}

fn generate_language(template: &Tera) -> anyhow::Result<()> {
    let out_dir = env::var("OUT_DIR").expect("Error loading output directory");
    let lang: BTreeMap<String, LanguageDefinition> = serde_yaml::from_reader(
        File::open("./languages.yaml").expect("Error loading languages config"),
    )?;

    #[derive(Serialize)]
    struct Lang {
        ident: String,
        name: String,
        aliases: Vec<String>,
        syntax: String,
        file: String,
    }
    #[derive(Serialize)]
    struct LangContext {
        languages: Vec<Lang>,
    }

    let context = {
        let mut v = Vec::new();
        for (ident, def) in lang {
            v.push(Lang {
                ident,
                aliases: {
                    let mut iv = def.alias;
                    iv.push(def.name.clone());
                    iv
                },
                name: def.name,
                syntax: generate_syntax(template, def.syntax)?,
                file: {
                    def.file.extension.check_regex()?;
                    def.file.file_name.check_regex()?;
                    generate_file_definition(template, def.file)?
                },
            })
        }
        v
    };

    let context = Context::from_serialize(LangContext { languages: context })
        .expect("Error loading languages context");
    let result = template
        .render("lang_type", &context)
        .expect("Error rendering languages template");

    let output_path = Path::new(&out_dir).join("language_syntax_tera.rs");
    fs::write(output_path, result).expect("Error writing languages config");
    Ok(())
}

fn generate_file_definition(template: &Tera, file: LanguageFile) -> anyhow::Result<String> {
    let context =
        Context::from_serialize(file).expect("Error loading languages file definition context");
    let result = template
        .render("file", &context)
        .expect("Error rendering languages file definition template");
    Ok(result)
}

fn generate_syntax(template: &Tera, syntax: LanguageSyntax) -> anyhow::Result<String> {
    let context = Context::from_serialize(syntax).expect("Error loading languages syntax context");
    let result = template
        .render("syntax", &context)
        .expect("Error rendering languages syntax template");
    Ok(result)
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct LanguageDefinition {
    name: String,
    #[serde(default = "empty_vec")]
    alias: Vec<String>,
    syntax: LanguageSyntax,
    file: LanguageFile,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct LanguageFile {
    extension: FileSetting,
    file_name: FileSetting,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct FileSetting {
    #[serde(default = "empty_vec")]
    regex: Vec<String>,
    #[serde(default = "empty_vec")]
    case_insensitive: Vec<String>,
    #[serde(default = "empty_vec")]
    plain: Vec<String>,
}

impl FileSetting {
    fn check_regex(&self) -> Result<(), regex::Error> {
        for regex in self.regex.iter() {
            match Regex::new(regex) {
                Ok(_) => {}
                Err(e) => return Err(e),
            }
        }
        Ok(())
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct LanguageSyntax {
    #[serde(default = "empty_vec")]
    block: Vec<(String, String)>,
    line_prefix: Option<String>,
    #[serde(default = "true_func")]
    ignore_prefix_space: bool,
    comment: LanguageComment,
    quote: LanguageQuote,
    #[serde(default = "empty_vec")]
    sub_language: Vec<(String, String, String)>,
}

fn true_func() -> bool {
    true
}

fn empty_vec<T>() -> Vec<T> {
    vec![]
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct LanguageComment {
    #[serde(default = "empty_vec")]
    multi: Vec<(String, String)>,
    #[serde(default = "empty_vec")]
    single: Vec<String>,
    #[serde(default = "empty_vec")]
    doc: Vec<String>,
    #[serde(default = "empty_vec")]
    doc_multi: Vec<(String, String)>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct LanguageQuote {
    #[serde(default = "empty_vec")]
    normal: Vec<(String, String)>,
    #[serde(default = "empty_vec")]
    literal: Vec<(String, String)>,
    #[serde(default = "empty_vec")]
    doc: Vec<(String, String)>,
}