pub fn is_snake_case(s: &str) -> bool {
!s.is_empty()
&& s.starts_with(|c: char| c.is_ascii_lowercase())
&& s.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
&& !s.contains("__")
&& !s.ends_with('_')
}
pub fn to_snake_case(s: &str) -> String {
let chars: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(s.len() + 4);
let mut pending_break = false;
for (i, &c) in chars.iter().enumerate() {
if c.is_ascii_alphanumeric() {
let starts_word = c.is_ascii_uppercase()
&& i > 0
&& (chars[i - 1].is_ascii_lowercase()
|| chars[i - 1].is_ascii_digit()
|| (chars[i - 1].is_ascii_uppercase()
&& chars.get(i + 1).is_some_and(char::is_ascii_lowercase)));
if (pending_break || starts_word) && !out.is_empty() {
out.push('_');
}
out.push(c.to_ascii_lowercase());
pending_break = false;
} else {
pending_break = !out.is_empty();
}
}
let out = out.trim_start_matches(|c: char| c.is_ascii_digit() || c == '_');
out.trim_end_matches('_').to_owned()
}
pub fn map_file_name(name: &str) -> String {
format!("{name}.map.ron")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn already_snake_is_left_alone() {
for s in ["site_67", "a", "wall_doorway_wide", "kit_b2"] {
assert!(is_snake_case(s), "{s} should be valid");
assert_eq!(to_snake_case(s), s);
}
}
#[test]
fn the_invalid_shapes_are_named() {
for s in ["", "Site", "site 67", "site-67", "_site", "site_", "site__67", "67"] {
assert!(!is_snake_case(s), "{s} should be invalid");
}
}
#[test]
fn spaces_and_punctuation_become_one_underscore() {
assert_eq!(to_snake_case("Site 67"), "site_67");
assert_eq!(to_snake_case("my map (final) v2"), "my_map_final_v2");
assert_eq!(to_snake_case(" lots of space "), "lots_of_space");
assert_eq!(to_snake_case("dash-and_underscore"), "dash_and_underscore");
}
#[test]
fn case_boundaries_are_word_boundaries() {
assert_eq!(to_snake_case("SiteSixtySeven"), "site_sixty_seven");
assert_eq!(to_snake_case("camelCase"), "camel_case");
assert_eq!(to_snake_case("HTTPServer"), "http_server");
}
#[test]
fn leading_digits_are_dropped_not_prefixed() {
assert_eq!(to_snake_case("2nd floor"), "nd_floor");
assert_eq!(to_snake_case("67"), "");
}
#[test]
fn anything_non_empty_out_is_valid_in() {
for s in [
"Site 67",
"---",
"a",
"ALLCAPS",
"trailing___",
"2nd floor",
"m i x e d 99",
"café",
] {
let out = to_snake_case(s);
if !out.is_empty() {
assert!(is_snake_case(&out), "{s:?} produced invalid {out:?}");
}
}
}
#[test]
fn nothing_survivable_yields_no_name() {
assert_eq!(to_snake_case(""), "");
assert_eq!(to_snake_case("!!! ???"), "");
}
#[test]
fn the_file_name_is_derived_from_the_name() {
assert_eq!(map_file_name("site_67"), "site_67.map.ron");
}
}
pub fn is_id(s: &str) -> bool {
!s.is_empty() && s.split('/').all(is_id_segment)
}
fn is_id_segment(s: &str) -> bool {
s.starts_with(|c: char| c.is_ascii_lowercase())
&& !s.ends_with('_')
&& !s.contains("__")
&& s.chars().all(|c: char| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
}
pub fn to_id(s: &str) -> String {
s.split('/')
.filter(|segment| !segment.is_empty())
.map(to_snake_case)
.filter(|segment| !segment.is_empty())
.collect::<Vec<_>>()
.join("/")
}
#[cfg(test)]
mod id_tests {
use super::*;
#[test]
fn a_kit_namespaced_id_is_a_valid_id() {
for ok in ["lamp_tall", "site/wall_corner", "crt_a", "barrel_acid_b", "tv", "is"] {
assert!(is_id(ok), "`{ok}` should be a valid id");
}
}
#[test]
fn a_name_that_is_not_snake_case_is_refused() {
for bad in ["", "Wall Corner", "siteWall", "_leading", "trailing_", "double__bar", "9lives", "site//wall", "/wall"] {
assert!(!is_id(bad), "`{bad}` should not be a valid id");
}
}
#[test]
fn the_namespace_survives_the_forcing() {
assert_eq!(to_id("site/wall_corner"), "site/wall_corner");
assert_eq!(to_id("town/house_cottage_plaster"), "town/house_cottage_plaster");
assert_eq!(to_id("Town/House Cottage Plaster"), "town/house_cottage_plaster");
assert_eq!(to_id("Site/Wall-Corner"), "site/wall_corner");
}
#[test]
fn a_stray_separator_is_dropped() {
assert_eq!(to_id("site//wall"), "site/wall");
assert_eq!(to_id("/wall"), "wall");
assert_eq!(to_id("site/"), "site");
}
#[test]
fn anything_non_empty_out_is_a_valid_id() {
for s in [
"site/wall",
"Site 67",
"---",
"a",
"ALLCAPS",
"trailing___",
"2nd floor",
"site//wall",
"/wall",
"site/",
] {
let out = to_id(s);
if !out.is_empty() {
assert!(is_id(&out), "{s:?} produced invalid {out:?}");
}
}
}
#[test]
fn nothing_survivable_yields_no_id() {
assert_eq!(to_id(""), "");
assert_eq!(to_id("!!! ???"), "");
}
}