mrapids 0.1.31

Your OpenAPI, but executable
Documentation
// Identifier Splitter - Token normalization for camelCase/snake_case/kebab-case identifiers
// Splits API identifiers into lowercase tokens for improved search matching

/// Split camelCase/snake_case/kebab-case/path identifiers into lowercase tokens.
///
/// Examples:
///   "findPetsByStatus" → ["find", "pets", "by", "status"]
///   "get_user_by_id"   → ["get", "user", "by", "id"]
///   "x-api-key"        → ["api", "key"]
///   "/users/{userId}"  → ["users", "user", "id"]
///   "getHTTPResponse"  → ["get", "http", "response"]
///   "API_BASE_URL"     → ["api", "base", "url"]
pub fn split_identifier(input: &str) -> Vec<String> {
    if input.is_empty() {
        return Vec::new();
    }

    // Step 1: Replace path separators and braces with spaces
    let cleaned: String = input
        .chars()
        .map(|c| match c {
            '/' | '{' | '}' => ' ',
            _ => c,
        })
        .collect();

    // Step 2: Split on delimiters (underscore, hyphen, whitespace)
    let segments: Vec<&str> = cleaned
        .split(|c: char| c == '_' || c == '-' || c.is_whitespace())
        .filter(|s| !s.is_empty())
        .collect();

    let mut tokens = Vec::new();

    for segment in segments {
        // Step 3: Split camelCase / PascalCase / consecutive caps
        let sub_tokens = split_camel_case(segment);
        tokens.extend(sub_tokens);
    }

    // Step 4: Lowercase all, filter single-char tokens
    tokens
        .into_iter()
        .map(|t| t.to_lowercase())
        .filter(|t| t.len() > 1)
        .collect()
}

/// Split and join into space-separated string (for embedding text).
pub fn split_to_text(input: &str) -> String {
    split_identifier(input).join(" ")
}

/// Split a segment on camelCase boundaries.
///
/// Handles:
/// - lowerUpper: "findPets" → ["find", "Pets"]
/// - consecutive caps: "getHTTPResponse" → ["get", "HTTP", "Response"]
/// - all caps: "API" → ["API"]
fn split_camel_case(input: &str) -> Vec<String> {
    let chars: Vec<char> = input.chars().collect();
    if chars.is_empty() {
        return Vec::new();
    }

    let mut tokens = Vec::new();
    let mut current = String::new();
    current.push(chars[0]);

    for i in 1..chars.len() {
        let prev = chars[i - 1];
        let curr = chars[i];
        let next = chars.get(i + 1);

        if curr.is_uppercase() {
            if prev.is_lowercase() {
                // lowerUpper boundary: "findP" → ["find", "P..."]
                tokens.push(current.clone());
                current.clear();
                current.push(curr);
            } else if prev.is_uppercase() {
                // Check if this is the end of a consecutive-caps run
                // e.g., "HTTPResponse" — when we see 'R' (upper) after 'P' (upper),
                // and next char 'e' is lowercase, split before 'R'
                if let Some(&next_char) = next {
                    if next_char.is_lowercase() {
                        tokens.push(current.clone());
                        current.clear();
                        current.push(curr);
                    } else {
                        current.push(curr);
                    }
                } else {
                    // Last char, just append
                    current.push(curr);
                }
            } else {
                // prev is not a letter (digit, etc.)
                tokens.push(current.clone());
                current.clear();
                current.push(curr);
            }
        } else {
            current.push(curr);
        }
    }

    if !current.is_empty() {
        tokens.push(current);
    }

    tokens
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_camel_case() {
        assert_eq!(
            split_identifier("findPetsByStatus"),
            vec!["find", "pets", "by", "status"]
        );
    }

    #[test]
    fn test_snake_case() {
        assert_eq!(
            split_identifier("get_user_by_id"),
            vec!["get", "user", "by", "id"]
        );
    }

    #[test]
    fn test_kebab_case() {
        // "x-" prefix gets stripped (single char 'x' filtered)
        assert_eq!(split_identifier("x-api-key"), vec!["api", "key"]);
    }

    #[test]
    fn test_pascal_case() {
        assert_eq!(
            split_identifier("GetUserById"),
            vec!["get", "user", "by", "id"]
        );
    }

    #[test]
    fn test_screaming_snake() {
        assert_eq!(split_identifier("API_BASE_URL"), vec!["api", "base", "url"]);
    }

    #[test]
    fn test_consecutive_caps() {
        assert_eq!(
            split_identifier("getHTTPResponse"),
            vec!["get", "http", "response"]
        );
    }

    #[test]
    fn test_path_segments() {
        assert_eq!(
            split_identifier("/users/{userId}"),
            vec!["users", "user", "id"]
        );
    }

    #[test]
    fn test_short_tokens_filtered() {
        // All single-char tokens filtered out
        assert_eq!(split_identifier("a_b_c"), Vec::<String>::new());
    }

    #[test]
    fn test_empty_input() {
        assert_eq!(split_identifier(""), Vec::<String>::new());
    }

    // Additional edge case tests
    #[test]
    fn test_split_to_text() {
        assert_eq!(split_to_text("findPetsByStatus"), "find pets by status");
    }

    #[test]
    fn test_mixed_delimiters() {
        assert_eq!(
            split_identifier("get-user_byId"),
            vec!["get", "user", "by", "id"]
        );
    }

    #[test]
    fn test_numbers_in_identifier() {
        assert_eq!(split_identifier("getV2Users"), vec!["get", "v2", "users"]);
    }
}