cal-core 0.2.158

Callable core lib
Documentation
use crate::device::device::{DeviceTag, TagType};
use ip_in_subnet::iface_in_subnet;
use lazy_static::lazy_static;
use log::{debug, error, trace};
use phonenumber::country;
use phonenumber::country::Id;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

lazy_static! {
    // Compile regex patterns once at program start
    static ref PHONE_CLEANER: Regex = Regex::new(r"(?:\+|<sip:)?([^>:]+)(?::.*|>.*)?").unwrap();
    static ref REQUEST_PLUS: Regex = Regex::new(r"\+\[request\.(\w+)\]").unwrap();
    static ref APP_PLUS: Regex = Regex::new(r"\+\[app\.(\w+)\]").unwrap();
    static ref REQUEST: Regex = Regex::new(r"\[request\.(\w+)\]").unwrap();
    static ref APP: Regex = Regex::new(r"\[app\.(\w+)\]").unwrap();
}

#[derive(Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum NumberFormat {
    #[serde(rename = "national")]
    National,
    #[serde(rename = "e164")]
    E164,
    #[serde(rename = "e164p")]
    E164Plus,
}

/// Clean a phone number string and parse it based on country code
///
/// # Arguments
/// * `country_code` - Country code as string (e.g., "1" for US)
/// * `str` - Input phone number to clean
pub fn clean_str_parse_country(country_code: &str, str: &str) -> String {
    trace!("Cleaning phone number '{}' with country code '{}'", str, country_code);

    let id = match country_code.parse() {
        Ok(id) => id,
        Err(e) => {
            debug!("Invalid country code '{}', defaulting to GB: {}", country_code, e);
            country::GB
        }
    };

    clean_str(id, str)
}

/// Clean a phone number string for a specific country using regex
///
/// Removes SIP formatting, plus signs, and extracts the main number part
pub fn clean_str(country: Id, str: &str) -> String {
    trace!("Cleaning phone number '{}' for country {:?}", str, country);

    // Extract the phone number using regex
    let clean = match PHONE_CLEANER.captures(str) {
        Some(caps) => {
            // Use the captured group which contains just the number
            caps.get(1).map_or(str, |m| m.as_str())
        },
        None => {
            debug!("Regex pattern didn't match for '{}', using as-is", str);
            str
        }
    };

    trace!("Extracted number part: '{}'", clean);
    parse_number(country, clean.to_string())
}

/// Format a phone number according to the specified format
///
/// # Arguments
/// * `str` - Input phone number
/// * `country_code` - Country code as string
/// * `format` - Desired output format
pub fn format_number(str: &str, country_code: &str, format: &NumberFormat) -> String {
    trace!("Formatting '{}' with country code '{}' using format {:?}", str, country_code, format);

    let clean_str = clean_str_parse_country(country_code, str);

    match format {
        NumberFormat::National => {
            let result = national_number(&clean_str, country_code);
            debug!("Formatted '{}' to national format: '{}'", str, result);
            result
        },
        NumberFormat::E164 => {
            debug!("Formatted '{}' to E164 format: '{}'", str, clean_str);
            clean_str
        },
        NumberFormat::E164Plus => {
            let result = format!("+{}", clean_str);
            debug!("Formatted '{}' to E164Plus format: '{}'", str, result);
            result
        },
    }
}

/// Format a phone number in national format
fn national_number(str: &str, country_code: &str) -> String {
    trace!("Converting '{}' to national format for country '{}'", str, country_code);

    let id = match country_code.parse() {
        Ok(id) => id,
        Err(e) => {
            debug!("Invalid country code '{}', defaulting to GB: {}", country_code, e);
            country::GB
        }
    };

    parse_number(id, str.to_string())
}

/// Parse a phone number using the phonenumber library
fn parse_number(country: Id, clean: String) -> String {
    trace!("Parsing '{}' for country {:?}", clean, country);

    match phonenumber::parse(Some(country), &clean) {
        Ok(number) => {
            let valid = phonenumber::is_valid(&number);
            if valid {
                let result = format!("{}{}", number.code().value(), number.national().value());
                trace!("Successfully parsed number: '{}'", result);
                result
            } else {
                debug!("Number '{}' parsed but not valid, returning as-is", clean);
                clean
            }
        }
        Err(e) => {
            debug!("Failed to parse '{}': {}", clean, e);
            clean
        }
    }
}

/// Replaces placeholders in a string with values from device tags
/// and applies number formatting when needed
///
/// # Arguments
/// * `input` - Input string with placeholders
/// * `device_tags` - Map of device tags
/// * `country_code` - Country code for number formatting
/// * `format` - Number format to apply to tag values
pub fn replace_placeholders_with_formatting(
    input: &str,
    device_tags: &HashMap<String, DeviceTag>,
    country_code: &str,
    format: &NumberFormat
) -> String {
    debug!("Replacing placeholders in '{}' with country_code '{}', format {:?}",
           input, country_code, format);

    let mut result = input.to_string();

    // Process all plus-prefixed placeholders first
    let replacements = find_tag_replacements(
        &result,
        device_tags,
        country_code,
        format,
        true
    );

    for (placeholder, value) in replacements {
        result = result.replace(&placeholder, &value);
    }

    // Then process regular placeholders
    let replacements = find_tag_replacements(
        &result,
        device_tags,
        country_code,
        format,
        false
    );

    for (placeholder, value) in replacements {
        result = result.replace(&placeholder, &value);
    }

    debug!("Replacement result: '{}'", result);
    result
}

/// Find all placeholder replacements in a string
fn find_tag_replacements(
    input: &str,
    device_tags: &HashMap<String, DeviceTag>,
    country_code: &str,
    format: &NumberFormat,
    with_plus: bool
) -> Vec<(String, String)> {
    let placeholder_type = if with_plus { "plus-prefixed" } else { "standard" };
    trace!("Finding {} placeholders in '{}'", placeholder_type, input);

    let mut replacements = Vec::new();

    // Process request.* placeholders
    if with_plus {
        for cap in REQUEST_PLUS.captures_iter(input) {
            let placeholder = cap[0].to_string();
            let tag_name = &cap[1];

            let key = format!("request.{}", tag_name);
            if let Some(tag) = device_tags.get(&key) {
                if tag.tag_type == TagType::Session {
                    let formatted_value = format_number(&tag.value, country_code, format);
                    trace!("Found request+ tag: '{}' → '{}'", placeholder, formatted_value);
                    replacements.push((placeholder, formatted_value));
                }
            }
        }
    } else {
        for cap in REQUEST.captures_iter(input) {
            let placeholder = cap[0].to_string();
            let tag_name = &cap[1];

            let key = format!("request.{}", tag_name);
            if let Some(tag) = device_tags.get(&key) {
                if tag.tag_type == TagType::Session {
                    let formatted_value = format_number(&tag.value, country_code, format);
                    trace!("Found request tag: '{}' → '{}'", placeholder, formatted_value);
                    replacements.push((placeholder, formatted_value));
                }
            }
        }
    }

    // Process app.* placeholders
    if with_plus {
        for cap in APP_PLUS.captures_iter(input) {
            let placeholder = cap[0].to_string();
            let tag_name = &cap[1];

            let key = format!("app.{}", tag_name);
            if let Some(tag) = device_tags.get(&key) {
                if tag.tag_type == TagType::Global {
                    let formatted_value = format_number(&tag.value, country_code, format);
                    trace!("Found app+ tag: '{}' → '{}'", placeholder, formatted_value);
                    replacements.push((placeholder, formatted_value));
                }
            }
        }
    } else {
        for cap in APP.captures_iter(input) {
            let placeholder = cap[0].to_string();
            let tag_name = &cap[1];

            let key = format!("app.{}", tag_name);
            if let Some(tag) = device_tags.get(&key) {
                if tag.tag_type == TagType::Global {
                    let formatted_value = format_number(&tag.value, country_code, format);
                    trace!("Found app tag: '{}' → '{}'", placeholder, formatted_value);
                    replacements.push((placeholder, formatted_value));
                }
            }
        }
    }

    replacements
}

/// Check if an IP address is within a subnet
///
/// # Arguments
/// * `ip` - IP address to check
/// * `subnet` - Subnet in CIDR notation (e.g., "192.168.1.0/24")
///
/// # Returns
/// `true` if the IP is in the subnet, `false` otherwise
pub fn ip_in_subnet(ip: &str, subnet: &str) -> bool {
    trace!("Checking if IP '{}' is in subnet '{}'", ip, subnet);

    // Direct match for identical values
    if ip == subnet {
        debug!("IP '{}' matches subnet '{}' directly", ip, subnet);
        return true;
    }

    // Check using the iface_in_subnet function
    match iface_in_subnet(ip, subnet) {
        Ok(result) => {
            debug!("IP '{}' in subnet '{}': {}", ip, subnet, result);
            result
        },
        Err(e) => {
            error!("Failed to check if IP '{}' is in subnet '{}': {}", ip, subnet, e);
            false
        }
    }
}