1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
use crate::MethodKind;

/// Change a CamelCase method name to snake case and prepends an optional
/// preamble depending on the kind of method.
pub fn method_to_snake(camel: &str, kind: MethodKind) -> String {
    debug_assert!(!camel.is_empty());
    let mut snake = String::with_capacity(camel.len());

    match kind {
        MethodKind::Set => snake.push_str("set_"),
        MethodKind::Remove => snake.push_str("remove_"),
        _ => {}
    };

    append_to_snake(snake, camel)
}

/// Change a CamelCase name to snake case.
pub fn to_snake(camel: &str) -> String {
    debug_assert!(!camel.is_empty());
    let snake = String::with_capacity(camel.len());
    append_to_snake(snake, camel)
}

fn append_to_snake(mut snake: String, camel: &str) -> String {
    // Add any manual fixups here, anything that isn't handle automatically by the algorithm below.
    if camel == "WinRT" {
        return "winrt".to_string();
    }

    let mut since_last_underscore = 0;
    let mut chars = camel.chars();
    // first character as lowercased
    for c in chars.next().unwrap().to_lowercase() {
        since_last_underscore += 1;
        snake.push(c);
    }

    // zip together iterator of previous characters and next characters
    for (previous, next) in camel.chars().zip(camel.chars().skip(2)) {
        // safe to unwrap since the iterator of next chars produced something
        let current = chars.next().unwrap();

        // If the current character isn't uppercase we can just push it and move on
        if !current.is_uppercase() {
            since_last_underscore += 1;
            snake.push(current);
            continue;
        }

        if previous.is_lowercase() || next.is_lowercase() && since_last_underscore > 1 {
            since_last_underscore = 0;

            if previous != '_' {
                snake.push('_');
            }
        }

        for c in current.to_lowercase() {
            since_last_underscore += 1;

            snake.push(c);
        }
    }

    if let Some(last) = chars.next() {
        snake.extend(last.to_lowercase());
    }

    snake
}

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

    #[test]
    fn to_snake_works() {
        assert_eq!(to_snake("Windows"), "windows".to_owned());

        assert_eq!(to_snake("ApplicationModel"), "application_model".to_owned());

        assert_eq!(method_to_snake("foo", MethodKind::Normal), "foo".to_owned());

        assert_eq!(
            method_to_snake("UIProgramming", MethodKind::Normal),
            "ui_programming".to_owned()
        );

        assert_eq!(
            method_to_snake("UIProgramming", MethodKind::Set),
            "set_ui_programming".to_owned()
        );

        assert_eq!(
            method_to_snake("CreateUInt8Array", MethodKind::Normal),
            "create_uint8_array".to_owned()
        );

        assert_eq!(
            method_to_snake("Socks", MethodKind::Remove),
            "remove_socks".to_owned()
        );

        assert_eq!(to_snake("appointmentId"), "appointment_id".to_owned());

        assert_eq!(method_to_snake("a", MethodKind::Normal), "a".to_owned());

        assert_eq!(
            method_to_snake("CreateField_Default", MethodKind::Normal),
            "create_field_default".to_owned()
        );

        assert!(to_snake("WinRT") == "winrt");
    }
}