use std::borrow::Cow;
use itertools::Itertools;
use lazy_regex::regex;
use crate::generate::core::get_package_info;
use crate::prelude::{NamingScheme, PluginConfig, PluginName};
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct TemplateHeader<'a> {
pub template: Vec<&'a str>,
pub comments_block: Vec<&'a str>,
pub info_block: Vec<String>,
}
trait Unquote {
fn unquoted(&self) -> Self;
}
impl Unquote for &str {
fn unquoted(&self) -> Self {
self.strip_prefix("\"")
.and_then(|s| s.strip_suffix("\""))
.unwrap_or(self)
}
}
pub(crate) const SUPPORTED_VARIABLES: &[&str] = &[
"root_state_name",
"naming_scheme",
"plugin_name",
"states_module_name",
"additional_derives",
];
pub(crate) fn parse_template_header<'a>(
source: &'a str,
plugin_config: &mut PluginConfig,
) -> TemplateHeader<'a> {
let leading_comments = source
.lines()
.take_while(|line| line.starts_with("//"))
.collect_vec();
let mut info_block = Vec::new();
info_block.push(format!("generated by {}", get_package_info()));
macro_rules! emit_warning {
($($exprs:expr),*) => {
let warning = format!($($exprs),*);
let warning = format!("WARN: {warning}");
eprintln!("{warning}");
info_block.push(warning);
};
}
let mut template_source = Vec::new();
let mut in_template = false;
let leading_comments = leading_comments
.iter()
.filter(|line| {
if in_template {
if let Some(line) = line.strip_prefix("//") {
template_source.push(line.trim());
true
} else {
false
}
} else if let Some(captures) =
regex!(r#"^\s*//\s*bspg:(\w+)\s+(\w+|"\w+\")\s*$"#).captures(line)
{
let (_, [name, value]) = captures.extract();
emit_warning!("unknown setting: '{name}'");
if !SUPPORTED_VARIABLES.contains(&name) {
emit_warning!("unknown setting: '{name}'");
} else {
match name {
"root_state_name" => {
plugin_config.root_state_name = if value == "None" {
None
} else {
Some(Cow::Owned(value.unquoted().to_string()))
};
}
"naming_scheme" => {
if let Some(naming_scheme) = NamingScheme::try_parse(value) {
plugin_config.naming_scheme = naming_scheme;
} else {
emit_warning!(
"invalid naming scheme '{value}' (expected [none, short, full])"
);
}
}
"plugin_name" => {
let value = value.to_string();
plugin_config.plugin_name =
if value.starts_with(|c: char| c.is_uppercase()) {
PluginName::new_struct(value)
} else {
PluginName::new_function(value)
}
}
"states_module_name" => {
plugin_config.states_module_name = Cow::from(value.to_string());
}
"additional_derives" => {
let mut to_add = value
.split_terminator(",")
.map(ToString::to_string)
.map(Cow::Owned)
.collect_vec();
plugin_config.additional_derives.append(&mut to_add);
}
_ => {
unreachable!()
}
}
}
true
} else if regex!(r#"^\s*//\s*bspg:\s*$"#).is_match(line) {
in_template = true;
true
} else {
eprintln!("dropping: '{line:?}'");
false
}
})
.copied()
.collect_vec();
TemplateHeader {
template: template_source,
comments_block: leading_comments,
info_block,
}
}
#[cfg(test)]
mod tests {
use bevy_reflect::Struct;
use bevy_utils::default;
use indoc::formatdoc;
use rstest::rstest;
use speculoos::assert_that;
use speculoos::prelude::{ContainingIntoIterAssertions, VecAssertions};
use crate::parsing::header::{SUPPORTED_VARIABLES, parse_template_header};
use crate::prelude::{NamingScheme, PluginConfig};
#[rstest]
#[case(String::new())]
#[case::ignore_generated_lines(formatdoc! {"
// generated by v[CARGO_PKG_VERSION]
// WARN: some warning
"})]
fn test_parse_template_header(#[case] header: String) {
let mut config: PluginConfig = default();
let header = parse_template_header(&header, &mut config);
assert_that!(header.template).is_empty();
assert_that!(header.comments_block).is_empty();
}
#[rstest]
fn test_parse_template_header_keep_variables() {
let header = formatdoc! {"
// generated by v[CARGO_PKG_VERSION]
// WARN: some warning
// bspg:some setting
"};
let mut config: PluginConfig = default();
let header = parse_template_header(&header, &mut config);
assert_that!(header.comments_block).contains("// bspg:some setting");
assert_that!(header.info_block).contains("WARN: unknown setting: 'some'".to_string());
}
#[rstest]
fn test_parse_template_header_variable_values(
#[values(NamingScheme::Full, NamingScheme::None, NamingScheme::Short)]
naming_scheme: NamingScheme,
#[values(None, Some("MyStateRoot"))] name: Option<&str>,
) {
let mut config: PluginConfig = default();
let header = formatdoc! {"
// generated by v[CARGO_PKG_VERSION]
// bspg:root_state_name {}
// bspg:naming_scheme {}
// bspg:
",
name.map(|name| format!("\"{name}\"")).unwrap_or("None".to_string()),
naming_scheme
};
let _ = parse_template_header(&header, &mut config);
assert_that!(config.root_state_name.as_ref().map(ToString::to_string))
.is_equal_to(name.map(String::from));
assert_that!(config.naming_scheme).is_equal_to(naming_scheme);
}
#[rstest]
fn test_plugin_config_all_fields_supported_as_variables() {
let config = PluginConfig::default();
let info = config.get_represented_struct_info().unwrap();
assert_that!(info.field_names().to_vec()).contains_all_of(&SUPPORTED_VARIABLES);
}
#[rstest]
fn test_parse_template_header_all_variables_supported() {}
}