use crate::menu_action;
use cloud_terrastodon_user_input::prompt_line;
use eyre::Context;
use eyre::bail;
use quote::quote;
use std::path::PathBuf;
use syn::Arm;
use syn::Expr;
use syn::ImplItem;
use syn::Item;
use syn::ItemEnum;
use syn::ItemUse;
use syn::Stmt;
use syn::Type;
use syn::Variant;
use syn::parse_file;
use syn::parse_str;
use tokio::process::Command;
use tracing::info;
pub async fn create_new_action_variant() -> eyre::Result<()> {
let new_variant_decl =
prompt_line("Enter the new enum variant name, e.g., \"BuildPolicyImports\":").await?;
let new_variant_display =
prompt_line("Enter the display name for the new variant, e.g., \"build policy imports\":")
.await?;
let function_name =
prompt_line("Enter the function name, e.g., \"build_policy_imports\":").await?;
update_menu_action_rs_file(&new_variant_decl, &new_variant_display, &function_name).await?;
create_new_function_file(&function_name).await?;
update_interactive_entrypoint_mod_rs_file(&function_name).await?;
add_import_statement_to_menu_action_rs(&function_name).await?;
Ok(())
}
async fn mutate_file<T>(path: &str, mut mutator: T) -> eyre::Result<()>
where
T: FnMut(&mut syn::File) -> eyre::Result<()>,
{
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
#[cfg(not(test))]
let manifest_dir = PathBuf::from(&manifest_dir);
#[cfg(test)]
let mut manifest_dir = PathBuf::from(&manifest_dir);
#[cfg(test)]
{
manifest_dir.pop();
manifest_dir.pop();
}
let full_path = manifest_dir.join(path);
let content = tokio::fs::read_to_string(&full_path)
.await
.wrap_err(format!(
"Failed to find {} in manifest dir {} given path {}",
full_path.display(),
manifest_dir.display(),
&path
))?;
let mut ast = parse_file(&content).wrap_err_with(|| {
format!(
"Parsing file content with syn failed for {}",
full_path.display()
)
})?;
info!("Modifying {}", full_path.display());
mutator(&mut ast).wrap_err_with(|| format!("Failed mutating {}", full_path.display()))?;
let modified_code = quote! {
#ast
};
let pretty_code =
prettyplease::unparse(&syn::parse2(modified_code).wrap_err_with(|| {
format!("Parsing file with syn failed for {}", full_path.display())
})?);
tokio::fs::write(&full_path, pretty_code)
.await
.wrap_err_with(|| format!("Writing file contents failed for {}", full_path.display()))?;
Command::new("rustfmt")
.arg(full_path.as_os_str())
.args(["--edition", "2021"])
.status()
.await
.wrap_err("Error applying rustfmt")?;
Ok(())
}
async fn update_menu_action_rs_file(
new_variant_decl: &str,
new_variant_display: &str,
function_name: &str,
) -> eyre::Result<()> {
fn add_name_match_arm(
method: &mut syn::ImplItemFn,
variant_ident: &syn::Ident,
display_name: &str,
) -> eyre::Result<()> {
for stmt in &mut method.block.stmts {
if let Stmt::Expr(Expr::Match(match_expr), _) = stmt {
let new_arm: Arm = parse_str(&format!(
"MenuAction::{variant_ident} => \"{display_name}\","
))?;
match_expr.arms.push(new_arm);
break;
}
}
Ok(())
}
fn add_invoke_match_arm(
method: &mut syn::ImplItemFn,
variant_ident: &syn::Ident,
function_name: &str,
) -> eyre::Result<()> {
for stmt in &mut method.block.stmts {
if let Stmt::Expr(Expr::Match(match_expr), _) = stmt {
let new_arm: Arm = parse_str(&format!(
"MenuAction::{variant_ident} => {function_name}().await?,"
))?;
match_expr.arms.push(new_arm);
break;
}
}
Ok(())
}
mutate_file(menu_action::THIS_FILE, |ast| {
let new_variant: Variant = syn::parse_str(new_variant_decl)?;
let new_variant_ident = new_variant.ident.clone();
let mut variant_added = false;
for item in &mut ast.items {
if let Item::Enum(ItemEnum {
ident, variants, ..
}) = item
&& ident == "MenuAction"
{
variants.push(new_variant);
variant_added = true;
break; }
}
if !variant_added {
bail!("MenuAction enum not found");
}
for item in &mut ast.items {
if let Item::Impl(impl_item) = item {
if impl_item.trait_.is_none()
&& let Type::Path(ref type_path) = *impl_item.self_ty
&& type_path.path.is_ident("MenuAction")
{
for impl_item in &mut impl_item.items {
if let ImplItem::Fn(method) = impl_item {
if method.sig.ident == "name" {
add_name_match_arm(
method,
&new_variant_ident,
new_variant_display,
)?;
}
else if method.sig.ident == "invoke" {
add_invoke_match_arm(method, &new_variant_ident, function_name)?;
}
}
}
}
}
}
Ok(())
})
.await?;
Ok(())
}
async fn update_interactive_entrypoint_mod_rs_file(function_name: &str) -> eyre::Result<()> {
mutate_file(crate::interactive::THIS_FILE, |ast| {
let new_mod_code = format!("mod {function_name};");
let new_mod: syn::ItemMod =
parse_str(&new_mod_code).wrap_err("Failed to parse new mod statement")?;
ast.items.insert(0, Item::Mod(new_mod));
for item in &mut ast.items {
if let Item::Mod(item_mod) = item
&& item_mod.ident == "prelude"
{
let (_, body) = item_mod
.content
.as_mut()
.ok_or_else(|| eyre::eyre!("prelude module has no inline content"))?;
let new_use_code = format!("pub use crate::interactive::{function_name}::*;");
let new_use: ItemUse =
parse_str(&new_use_code).wrap_err("Failed to parse new use statement")?;
body.push(Item::Use(new_use));
break;
}
}
Ok(())
})
.await
}
async fn add_import_statement_to_menu_action_rs(function_name: &str) -> eyre::Result<()> {
mutate_file(crate::menu_action::THIS_FILE, |ast| {
let use_statement = format!("use crate::interactive::{function_name};");
let use_statement = parse_str(&use_statement)?;
ast.items.insert(0, use_statement);
Ok(())
})
.await
}
async fn create_new_function_file(function_name: &str) -> eyre::Result<()> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?;
let mut crate_dir = PathBuf::from(&manifest_dir).join(crate::interactive::THIS_FILE);
crate_dir.pop();
let file_path = crate_dir.join(format!("{function_name}.rs"));
let boilerplate = format!(
r#"use eyre::Result;
pub async fn {function_name}() -> Result<()> {{
Ok(())
}}
"#
);
tokio::fs::write(&file_path, boilerplate)
.await
.wrap_err_with(|| format!("Failed to write to {}", file_path.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore]
async fn it_works() -> eyre::Result<()> {
update_interactive_entrypoint_mod_rs_file("hehe_testing").await?;
Ok(())
}
}