arete-macros 0.1.2

Proc-macros for defining Arete streams
Documentation
#![allow(dead_code)]

use crate::utils::to_pascal_case;

/// Extract the base name from a potentially scoped event type.
/// "ore::RoundState" -> "RoundState"
/// "RoundState" -> "RoundState"  (backwards compat)
pub fn event_type_base_name(event_type: &str) -> &str {
    event_type.rsplit("::").next().unwrap_or(event_type)
}

/// Extract the program name from a scoped event type.
/// "ore::RoundState" -> Some("ore")
/// "RoundState" -> None
pub fn event_type_program(event_type: &str) -> Option<&str> {
    event_type.rsplit_once("::").map(|(prefix, _)| prefix)
}

/// Strip the "State" or "IxState" suffix from a potentially scoped event type,
/// returning just the base account/instruction name.
/// "ore::RoundState" -> "Round"
/// "ore::DeployIxState" -> "Deploy"
/// "RoundState" -> "Round"
pub fn strip_event_type_suffix(event_type: &str) -> &str {
    let base = event_type_base_name(event_type);
    base.strip_suffix("IxState")
        .or_else(|| base.strip_suffix("State"))
        .unwrap_or(base)
}

/// Create a scoped event type name.
/// ("ore", "Round", false) -> "ore::RoundState"
/// ("ore", "Deploy", true) -> "ore::DeployIxState"
pub fn scoped_event_type(program_name: &str, type_name: &str, is_instruction: bool) -> String {
    let suffix = if is_instruction { "IxState" } else { "State" };
    format!("{}::{}{}", program_name, type_name, suffix)
}

/// Create a canonical instruction event type name.
///
/// Instruction names in the DSL often come from Rust SDK paths like
/// `pump_sdk::instructions::buy_exact_sol_in`. Runtime parser event names are
/// always PascalCase (`pump::BuyExactSolInIxState`), so AST generation must
/// normalize the raw path segment before appending the suffix.
pub fn scoped_instruction_event_type(program_name: Option<&str>, instruction_name: &str) -> String {
    let canonical_name = to_pascal_case(instruction_name);
    match program_name {
        Some(program_name) => format!("{}::{}IxState", program_name, canonical_name),
        None => format!("{}IxState", canonical_name),
    }
}

/// Create a scoped event type name for either an instruction or a CPI event.
///
/// CPI event identifiers are already emitted in their canonical case by the
/// parser, so only instruction names are normalized here.
pub fn scoped_instruction_or_cpi_event_type(
    program_name: Option<&str>,
    event_name: &str,
    is_cpi_event: bool,
) -> String {
    if is_cpi_event {
        match program_name {
            Some(program_name) => format!("{}::{}CpiEvent", program_name, event_name),
            None => format!("{}CpiEvent", event_name),
        }
    } else {
        scoped_instruction_event_type(program_name, event_name)
    }
}

/// Canonicalize an instruction event type while preserving any program scope.
pub fn canonicalize_instruction_event_type(event_type: &str) -> String {
    let (program_name, base_name) = match event_type.rsplit_once("::") {
        Some((program_name, base_name)) => (Some(program_name), base_name),
        None => (None, event_type),
    };

    let instruction_name = base_name.strip_suffix("IxState").unwrap_or(base_name);
    let canonical_name = to_pascal_case(instruction_name);

    match program_name {
        Some(program_name) => format!("{}::{}IxState", program_name, canonical_name),
        None => format!("{}IxState", canonical_name),
    }
}

pub fn canonicalize_event_type_name(event_type: &str) -> String {
    if event_type.ends_with("IxState") {
        canonicalize_instruction_event_type(event_type)
    } else {
        event_type.to_string()
    }
}

use crate::parse::idl::IdlSpec;

pub type IdlLookup<'a> = &'a [(String, &'a IdlSpec)];

pub fn find_idl_for_type<'a>(type_str: &str, idls: IdlLookup<'a>) -> Option<&'a IdlSpec> {
    if idls.is_empty() {
        return None;
    }
    let first_segment = type_str.split("::").next()?.trim();
    idls.iter()
        .find(|(sdk_name, _)| sdk_name == first_segment)
        .map(|(_, idl)| *idl)
        .or_else(|| Some(idls[0].1))
}

pub fn program_name_for_type<'a>(type_str: &str, idls: IdlLookup<'a>) -> Option<&'a str> {
    find_idl_for_type(type_str, idls).map(|idl| idl.get_name())
}

pub fn program_name_from_sdk_prefix(sdk_module: &str) -> &str {
    sdk_module.strip_suffix("_sdk").unwrap_or(sdk_module)
}

/// Convert a snake_case identifier to lowerCamelCase.
/// "bonding_curve" -> "bondingCurve"
/// "mint" -> "mint" (single word unchanged)
/// "associated_bonding_curve" -> "associatedBondingCurve"
pub fn snake_to_lower_camel(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    let mut capitalize_next = false;
    for ch in s.chars() {
        if ch == '_' {
            capitalize_next = true;
        } else if capitalize_next {
            result.extend(ch.to_uppercase());
            capitalize_next = false;
        } else {
            result.push(ch);
        }
    }
    result
}

#[cfg(test)]
mod tests {
    use super::{
        canonicalize_event_type_name, canonicalize_instruction_event_type,
        scoped_instruction_event_type, scoped_instruction_or_cpi_event_type,
    };

    #[test]
    fn scoped_instruction_event_type_normalizes_snake_case_names() {
        assert_eq!(
            scoped_instruction_event_type(Some("pump"), "buy_exact_sol_in"),
            "pump::BuyExactSolInIxState"
        );
        assert_eq!(
            scoped_instruction_event_type(Some("pump"), "buy"),
            "pump::BuyIxState"
        );
    }

    #[test]
    fn scoped_instruction_event_type_preserves_pascal_case_names() {
        assert_eq!(
            scoped_instruction_event_type(Some("ore"), "Deploy"),
            "ore::DeployIxState"
        );
    }

    #[test]
    fn scoped_instruction_or_cpi_event_type_keeps_cpi_names_verbatim() {
        assert_eq!(
            scoped_instruction_or_cpi_event_type(Some("pump"), "TradeExecuted", true),
            "pump::TradeExecutedCpiEvent"
        );
    }

    #[test]
    fn canonicalize_instruction_event_type_normalizes_scoped_snake_case() {
        assert_eq!(
            canonicalize_instruction_event_type("pump::create_v2IxState"),
            "pump::CreateV2IxState"
        );
    }

    #[test]
    fn canonicalize_event_type_name_leaves_accounts_unchanged() {
        assert_eq!(
            canonicalize_event_type_name("pump::BondingCurveState"),
            "pump::BondingCurveState"
        );
    }
}