use beet_core::prelude::*;
use heck::ToSnakeCase;
use quote::ToTokens;
use syn::Expr;
use syn::Item;
pub fn export_codegen(
query: Populated<&CodegenFile, Changed<CodegenFile>>,
) -> bevy::prelude::Result {
let num_files = query.iter().count();
info!("Exporting {} codegen files...", num_files);
for codegen_file in query.iter() {
codegen_file.build_and_write()?;
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Reflect, Component)]
#[reflect(Default, Component)]
pub struct CodegenFile {
output: AbsPathBuf,
pkg_name: Option<String>,
imports: Vec<String>,
items: Vec<String>,
}
impl Default for CodegenFile {
fn default() -> Self {
Self {
output: WsPathBuf::new("src/codegen/mod.rs").into_abs(),
pkg_name: None,
imports: default(),
items: default(),
}
.with_import(syn::parse_quote!(
#[allow(unused_imports)]
use beet::prelude::*;
))
.with_import(syn::parse_quote!(
#[allow(unused_imports)]
use crate::prelude::*;
))
}
}
impl CodegenFile {
pub fn new(output: AbsPathBuf) -> Self {
Self {
output,
..Default::default()
}
}
pub fn output(&self) -> &AbsPathBuf { &self.output }
pub fn pkg_name(&self) -> Option<&String> { self.pkg_name.as_ref() }
pub fn name(&self) -> String {
match self
.output
.file_stem()
.expect("codegen output must have a file stem")
.to_str()
.expect("file stem must be valid UTF-8")
{
"mod" => self
.output
.parent()
.expect("mod files must have a parent")
.file_name()
.expect("parent must have a file name")
.to_str()
.expect("file name must be valid UTF-8")
.to_owned(),
other => other.to_owned(),
}
.to_snake_case()
}
pub fn clone_info(&self, output: AbsPathBuf) -> Self {
Self {
output,
imports: self.imports.clone(),
pkg_name: self.pkg_name.clone(),
items: Vec::new(),
}
}
pub fn with_pkg_name(mut self, pkg_name: impl Into<String>) -> Self {
self.pkg_name = Some(pkg_name.into());
self
}
pub fn with_import(mut self, item: Item) -> Self {
self.imports.push(item.into_token_stream().to_string());
self
}
pub fn set_imports(mut self, items: Vec<Item>) -> Self {
self.imports = items
.iter()
.map(|item| item.into_token_stream().to_string())
.collect();
self
}
pub fn output_dir(&self) -> Result<AbsPathBuf> {
self.output
.parent()
.ok_or_else(|| bevyhow!("Output path must have a parent directory"))
}
pub fn clear_items(&mut self) { self.items.clear(); }
pub fn add_item<T: Into<Item>>(&mut self, item: T) {
self.items.push(item.into().into_token_stream().to_string());
}
fn imports_to_tokens(&self) -> Result<Vec<Item>, syn::Error> {
self.imports
.iter()
.map(|s| syn::parse_str::<Item>(s))
.collect::<Result<_, _>>()
}
fn items_to_tokens(&self) -> Result<Vec<Item>, syn::Error> {
self.items
.iter()
.map(|s| syn::parse_str::<Item>(s))
.collect::<Result<_, _>>()
}
pub fn build_output(&self) -> Result<syn::File> {
let imports = self.imports_to_tokens()?;
let crate_alias = self.crate_alias()?;
let items = self.items_to_tokens()?;
Ok(syn::parse_quote! {
#(#imports)*
#crate_alias
#(#items)*
})
}
pub fn build_and_write(&self) -> Result<()> {
let output_tokens = self.build_output()?;
let output_str = prettyplease::unparse(&output_tokens);
trace!("Exporting codegen file:\n{}", self.output.to_string_lossy());
fs_ext::write_if_diff(&self.output, &output_str)?;
Ok(())
}
fn crate_alias(&self) -> Result<Option<syn::Item>> {
if let Some(pkg_name) = &self.pkg_name {
let pkg_name: Expr = syn::parse_str(pkg_name)?;
Ok(Some(syn::parse_quote! {
#[allow(unused_imports)]
use crate as #pkg_name;
}))
} else {
Ok(None)
}
}
}
#[cfg(test)]
mod test {
use crate::prelude::*;
use quote::ToTokens;
use beet_core::prelude::*;
use syn::ItemFn;
#[test]
fn works() {
let mut file = CodegenFile::default();
file.add_item::<ItemFn>(syn::parse_quote! {
fn test() {}
});
(&file.build_output().unwrap().to_token_stream().to_string())
.xpect_contains("fn test () { }");
}
}