bevy_state_plugin_generator 1.4.4

A build-dependency that generates a Bevy State Plugin from a simple state definition.
Documentation
use indoc::formatdoc;
use itertools::Itertools;
use nom::AsChar;

use crate::parsing::parse_config;
use crate::prelude::PluginConfig;
use crate::processing::{ProcessingError, convert_nodes_into_plugin_source};

pub(crate) const REQUIRED_DERIVES: &[&str] =
    &["Hash", "Default", "Debug", "Clone", "PartialEq", "Eq"];

pub(crate) fn get_package_info() -> String {
    let pkg = env!("CARGO_PKG_NAME");
    #[cfg(not(test))]
    let version = env!("CARGO_PKG_VERSION");
    #[cfg(test)]
    let version = "[CARGO_PKG_VERSION]";
    format!("{pkg} v{version}")
}

pub(crate) fn generate_debug_info(src_path: &str, source: &str) -> String {
    let lines = source.lines().map(|line| format!("// {line}")).join("\n");
    let pkg_info = get_package_info();
    formatdoc! {"
        // generated by {pkg_info}
        // src: {src_path}
        {lines}
    "}
}

#[cfg(feature = "rustfmt")]
fn try_format_source(source: &str) -> std::io::Result<String> {
    duct::cmd!("rustfmt")
        .stdin_bytes(source)
        .stderr_to_stdout()
        .read()
}

pub(crate) fn format_source<S: AsRef<str>>(source: S) -> String {
    let source = source.as_ref();
    #[cfg(feature = "rustfmt")]
    let source = try_format_source(source).unwrap_or_else(|_| source.to_owned());
    #[cfg(not(feature = "rustfmt"))]
    let source = source.to_owned();

    if source.ends_with(|c: char| c.is_newline()) {
        source
    } else {
        source + "\n"
    }
}

pub(crate) fn generate_state_plugin_source(
    input_source: &str,
    plugin_config: PluginConfig,
    src_path: Option<&str>,
) -> Result<String, ProcessingError> {
    let (unparsed, nodes) = parse_config(input_source)?;
    let mut output = convert_nodes_into_plugin_source(nodes, plugin_config)?;

    #[cfg(test)]
    {
        use speculoos::assert_that;
        use speculoos::prelude::VecAssertions;

        assert_that!(output.matches(" mod ").collect_vec()).has_length(1);
    }

    // if we're writing to a file we add a header with some information
    output = if let Some(src_path) = src_path {
        if !unparsed.trim().is_empty() {
            return Err(ProcessingError::InvalidConfig(unparsed.to_string()));
        }
        let debug_info = generate_debug_info(src_path, input_source);
        [debug_info, output].join("\n")
    } else {
        [unparsed, &output].join("\n")
    };

    Ok(format_source(output))
}