pinoc 0.3.3

A CLI tool for setting up pinocchio program project
use super::cpi::instruction_cpi_rs;
use super::types::{idl_type_to_rust, safe_ident};
use anyhow::Result;
use heck::{ToPascalCase, ToSnakeCase};
use shank_idl::idl_instruction::{IdlAccountItem, IdlInstruction};

pub fn instruction_rs(ix: &IdlInstruction, generate_cpi: bool) -> Result<String> {
    let name = ix.name.to_pascal_case();
    let snake_name = safe_ident(&ix.name.to_snake_case());

    let mut accounts = Vec::new();
    for item in &ix.accounts {
        match item {
            IdlAccountItem::IdlAccount(acc) => accounts.push(acc),
            IdlAccountItem::IdlAccounts(_) => anyhow::bail!(
                "pinoc client generate does not support nested account groups yet ({name})"
            ),
        }
    }

    let mut args_fields = String::new();
    let mut args_params = String::new();
    for field in &ix.args {
        let field_name = safe_ident(&field.name.to_snake_case());
        let field_ty = idl_type_to_rust(&field.ty)?;
        args_fields.push_str(&format!("    pub {field_name}: {field_ty},\n"));
        args_params.push_str(&format!("{field_name}: {field_ty}, "));
    }
    let args_names = ix
        .args
        .iter()
        .map(|f| safe_ident(&f.name.to_snake_case()))
        .collect::<Vec<_>>()
        .join(", ");

    let mut account_metas = String::new();
    let mut fn_params = String::new();
    for acc in &accounts {
        let acc_name = safe_ident(&acc.name.to_snake_case());
        fn_params.push_str(&format!("{acc_name}: solana_pubkey::Pubkey, "));
        let meta = if acc.is_mut {
            format!(
                "solana_instruction::AccountMeta::new({acc_name}, {})",
                acc.is_signer
            )
        } else {
            format!(
                "solana_instruction::AccountMeta::new_readonly({acc_name}, {})",
                acc.is_signer
            )
        };
        account_metas.push_str(&format!("        accounts.push({meta});\n"));
    }

    let discriminant = ix.discriminant.value;
    let account_count = accounts.len();

    let type_imports = super::types::type_imports(ix.args.iter().map(|f| &f.ty));

    let cpi_block = if generate_cpi {
        instruction_cpi_rs(ix, &accounts)?
    } else {
        String::new()
    };
    let accounts_mut = if accounts.is_empty() { "" } else { "mut " };

    Ok(format!(
        r#"//! Autogenerated by `pinoc client generate`. Do not edit by hand.

use borsh::BorshSerialize;
{type_imports}
pub const {upper_name}_DISCRIMINANT: u8 = {discriminant};

#[derive(BorshSerialize, Clone, Debug, PartialEq)]
pub struct {name}InstructionArgs {{
{args_fields}}}

/// Builds the `{name}` instruction.
///
/// ### Accounts:
{accounts_doc}
#[allow(clippy::too_many_arguments)]
pub fn {snake_name}(
    {fn_params}{args_params}
) -> solana_instruction::Instruction {{
    let {accounts_mut}accounts = Vec::with_capacity({account_count});
{account_metas}
    let args = {name}InstructionArgs {{ {args_names} }};
    let mut data = vec![{upper_name}_DISCRIMINANT];
    data.extend(borsh::to_vec(&args).expect("{name} args should always serialize"));

    solana_instruction::Instruction {{
        program_id: crate::ID,
        accounts,
        data,
    }}
}}
{cpi_block}"#,
        upper_name = ix.name.to_snake_case().to_uppercase(),
        accounts_doc = accounts
            .iter()
            .enumerate()
            .map(|(i, acc)| {
                let mut flags = Vec::new();
                if acc.is_mut {
                    flags.push("writable");
                }
                if acc.is_signer {
                    flags.push("signer");
                }
                format!(
                    "///   {i}. `[{}]` {}",
                    flags.join(", "),
                    acc.name.to_snake_case()
                )
            })
            .collect::<Vec<_>>()
            .join("\n"),
    ))
}