Skip to main content

factorio_frontend/
paths.rs

1use std::path::{Path, PathBuf};
2
3/// Returns the dotted module name for a source file relative to a source root, e.g.
4/// - `src/player/extra_info.rs` -> `"player.extra_info"`
5/// - `src/player/mod.rs` -> `"player"`
6#[must_use]
7pub fn module_name_from_source(source_dir: &Path, source_path: &Path) -> Option<String> {
8    let relative = source_path.strip_prefix(source_dir).ok()?;
9
10    if relative.file_name().is_some_and(|name| name == "lib.rs") {
11        return None;
12    }
13
14    if relative.file_name().is_some_and(|name| name == "mod.rs") {
15        let parent = relative.parent()?;
16        if parent.as_os_str().is_empty() {
17            return None;
18        }
19        return Some(path_to_module_name(parent));
20    }
21
22    Some(path_to_module_name(&relative.with_extension("")))
23}
24
25fn path_to_module_name(path: &Path) -> String {
26    path.iter()
27        .map(|component| component.to_string_lossy().into_owned())
28        .collect::<Vec<_>>()
29        .join(".")
30}
31
32/// Returns the output Lua path for a dotted module name.
33#[must_use]
34pub fn lua_output_path(output_dir: &Path, module_name: &str) -> PathBuf {
35    let mut path = output_dir.to_path_buf();
36    for segment in module_name.split('.') {
37        path.push(segment);
38    }
39    path.set_extension("lua");
40    path
41}
42
43/// Returns a valid Lua identifier for a required module path.
44#[must_use]
45pub fn require_local_name(module_path: &str) -> String {
46    module_path.replace('.', "_")
47}
48
49/// Splits a crate-relative path into a dotted module path and remaining item segments.
50#[allow(clippy::indexing_slicing)]
51pub fn split_crate_path(segments: &[String]) -> (String, Vec<String>) {
52    if segments.is_empty() {
53        return (String::new(), Vec::new());
54    }
55
56    let mut module_parts = Vec::new();
57    let mut item_start = segments.len();
58
59    for (index, segment) in segments.iter().enumerate() {
60        if starts_with_uppercase(segment) {
61            item_start = index;
62            break;
63        }
64        module_parts.push(segment.as_str());
65    }
66
67    if module_parts.is_empty() {
68        module_parts.push(segments[0].as_str());
69        item_start = 1;
70    }
71
72    (module_parts.join("."), segments[item_start..].to_vec())
73}
74
75fn starts_with_uppercase(value: &str) -> bool {
76    value.chars().next().is_some_and(char::is_uppercase)
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn maps_nested_source_paths_to_module_names() {
85        let source_dir = Path::new("/project/src");
86
87        assert_eq!(
88            module_name_from_source(source_dir, Path::new("/project/src/player/extra_info.rs")),
89            Some("player.extra_info".to_string())
90        );
91        assert_eq!(
92            module_name_from_source(source_dir, Path::new("/project/src/player/mod.rs")),
93            Some("player".to_string())
94        );
95    }
96
97    #[test]
98    fn splits_crate_paths_into_module_and_item_segments() {
99        assert_eq!(
100            split_crate_path(&[
101                "player".to_string(),
102                "extra_info".to_string(),
103                "MyType".to_string(),
104            ]),
105            ("player.extra_info".to_string(), vec!["MyType".to_string()])
106        );
107        assert_eq!(
108            split_crate_path(&[
109                "player".to_string(),
110                "MyPlayer".to_string(),
111                "new".to_string(),
112            ]),
113            (
114                "player".to_string(),
115                vec!["MyPlayer".to_string(), "new".to_string()]
116            )
117        );
118        assert_eq!(
119            split_crate_path(&["player".to_string(), "extra_info".to_string()]),
120            ("player.extra_info".to_string(), Vec::new())
121        );
122    }
123}