device 0.0.4

A generative engine
use {
  quote::ToTokens,
  std::{collections::BTreeMap, env, fs, path::PathBuf},
  syn::{FnArg, Item, ItemFn, Pat, PatType, ReturnType, Type},
};

const PATH: &str = "src/commands.rs";

fn main() {
  println!("cargo:rerun-if-changed={PATH}");

  let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());

  let path = root.join(PATH);

  let src = fs::read_to_string(&path).unwrap();

  let ast = syn::parse_file(&src).unwrap();

  let functions: Vec<&ItemFn> = ast
    .items
    .iter()
    .filter_map(|item| {
      if let Item::Fn(function) = item {
        Some(function)
      } else {
        None
      }
    })
    .collect();

  let mut commands = BTreeMap::new();

  for function in functions {
    let name = function.sig.ident.to_string();

    let inputs = function
      .sig
      .inputs
      .iter()
      .map(|input| {
        let FnArg::Typed(PatType { pat, .. }) = input else {
          panic!(
            "command function {name} has self receiver: {}",
            input.to_token_stream(),
          );
        };

        let Pat::Ident(pat_ident) = pat.as_ref() else {
          panic!(
            "command function {name} has input without ident pattern: {}",
            input.to_token_stream(),
          );
        };

        pat_ident.ident.to_string()
      })
      .collect::<Vec<String>>();

    let fallible = match &function.sig.output {
      ReturnType::Default => false,
      ReturnType::Type(_, ty) => {
        let Type::Path(p) = ty.as_ref() else {
          panic!(
            "command function {name} has unexpected return type: {}",
            ty.to_token_stream(),
          );
        };

        assert!(
          p.qself.is_none(),
          "command function {name} has qualified return type",
        );

        let ident = p.path.get_ident().unwrap();

        assert_eq!(
          ident,
          "Result",
          "command function {name} has unexpected return type: {}",
          ty.to_token_stream(),
        );

        true
      }
    };

    let inputs = inputs.iter().map(String::as_str).collect::<Vec<&str>>();

    let variant = match (inputs.as_slice(), fallible) {
      (["app"], false) => "App",
      (["app"], true) => "AppFallible",
      (["app", "event_loop"], false) => "AppEventLoop",
      (["state"], false) => "State",
      _ => panic!(
        "unsupported combination of inputs and fallibility: ({}, {fallible}",
        inputs.join(" ")
      ),
    };

    commands.insert(name, variant);
  }

  let mut lines = Vec::new();

  lines.push("use {super::*, commands::*, Command::*};".into());

  lines.push(String::new());

  for (name, variant) in &commands {
    lines.push(format!(
      "pub(crate) const {}: (&str, Command) = (\"{}\", {variant}({name}));",
      name.to_uppercase(),
      name.replace('_', "-"),
    ));
  }

  lines.push(String::new());

  lines.push("pub(crate) const COMMANDS: &[(&str, Command)] = &[".into());

  for name in commands.keys() {
    lines.push(format!("  {},", name.to_uppercase()));
  }

  lines.push("];\n".into());

  fs::write(
    PathBuf::from(env::var("OUT_DIR").unwrap()).join("generated.rs"),
    lines.join("\n"),
  )
  .unwrap();
}