lune_utils/path/
luau.rs

1/*!
2    Utilities for working with Luau module paths.
3*/
4
5use std::{
6    ffi::OsStr,
7    fmt,
8    path::{Path, PathBuf},
9};
10
11use mlua::prelude::*;
12
13use super::constants::{FILE_EXTENSIONS, FILE_NAME_INIT};
14use super::std::append_extension;
15
16/**
17    A file path for Luau, which has been resolved to either a valid file or directory.
18
19    Not to be confused with [`LuauModulePath`]. This is the path
20    **on the filesystem**, and not the abstracted module path.
21*/
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum LuauFilePath {
24    /// A resolved and valid file path.
25    File(PathBuf),
26    /// A resolved and valid directory path.
27    Directory(PathBuf),
28}
29
30impl LuauFilePath {
31    fn resolve(module: impl AsRef<Path>) -> Result<Self, LuaNavigateError> {
32        let module = module.as_ref();
33
34        // Modules named "init" are ambiguous and not allowed
35        if module
36            .file_name()
37            .is_some_and(|n| n == OsStr::new(FILE_NAME_INIT))
38        {
39            return Err(LuaNavigateError::Ambiguous);
40        }
41
42        let mut found = None;
43
44        // Try files first
45        for ext in FILE_EXTENSIONS {
46            let candidate = append_extension(module, ext);
47            if candidate.is_file() && found.replace(candidate).is_some() {
48                return Err(LuaNavigateError::Ambiguous);
49            }
50        }
51
52        // Try directories with init files in them
53        if module.is_dir() {
54            let init = Path::new(FILE_NAME_INIT);
55            for ext in FILE_EXTENSIONS {
56                let candidate = module.join(append_extension(init, ext));
57                if candidate.is_file() && found.replace(candidate).is_some() {
58                    return Err(LuaNavigateError::Ambiguous);
59                }
60            }
61
62            // If we have not found any luau / lua files, and we also did not find
63            // any init files in this directory, we still found a valid directory
64            if found.is_none() {
65                return Ok(Self::Directory(module.to_path_buf()));
66            }
67        }
68
69        // We have now narrowed down our resulting module
70        // path to be exactly one valid path, or no path
71        found.map(Self::File).ok_or(LuaNavigateError::NotFound)
72    }
73
74    #[must_use]
75    pub const fn is_file(&self) -> bool {
76        matches!(self, Self::File(_))
77    }
78
79    #[must_use]
80    pub const fn is_dir(&self) -> bool {
81        matches!(self, Self::Directory(_))
82    }
83
84    #[must_use]
85    pub fn as_file(&self) -> Option<&Path> {
86        match self {
87            Self::File(path) => Some(path),
88            Self::Directory(_) => None,
89        }
90    }
91
92    #[must_use]
93    pub fn as_dir(&self) -> Option<&Path> {
94        match self {
95            Self::File(_) => None,
96            Self::Directory(path) => Some(path),
97        }
98    }
99}
100
101impl AsRef<Path> for LuauFilePath {
102    fn as_ref(&self) -> &Path {
103        match self {
104            Self::File(path) | Self::Directory(path) => path.as_ref(),
105        }
106    }
107}
108
109impl fmt::Display for LuauFilePath {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            Self::Directory(path) | Self::File(path) => path.display().fmt(f),
113        }
114    }
115}
116
117/**
118    A resolved module path for Luau, containing both:
119
120    - The **source** Luau module path.
121    - The **target** filesystem path.
122
123    Note the separation here - the source is not necessarily a valid filesystem path,
124    and the target is not necessarily a valid Luau module path for require-by-string.
125
126    See [`LuauFilePath`] and [`LuauModulePath::resolve`] for more information.
127*/
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct LuauModulePath {
130    // The originating module path
131    source: PathBuf,
132    // The target filesystem path
133    target: LuauFilePath,
134}
135
136impl LuauModulePath {
137    /**
138        Strips Luau file extensions and potential init segments from a given path.
139
140        This is the opposite operation of [`LuauModulePath::resolve`] and is generally
141        useful for converting between paths in a CLI or other similar use cases - but
142        should *never* be used to implement `require` resolution.
143
144        Does not use any filesystem calls and will not panic.
145    */
146    #[must_use]
147    pub fn strip(path: impl Into<PathBuf>) -> PathBuf {
148        let mut path: PathBuf = path.into();
149
150        if path
151            .extension()
152            .and_then(|e| e.to_str())
153            .is_some_and(|e| FILE_EXTENSIONS.contains(&e))
154        {
155            path = path.with_extension("");
156        }
157
158        if path
159            .file_name()
160            .and_then(|e| e.to_str())
161            .is_some_and(|f| f == FILE_NAME_INIT)
162        {
163            path.pop();
164        }
165
166        path
167    }
168
169    /**
170        Resolves an existing file or directory path for the given *module* path.
171
172        Given a *module* path "path/to/module", these files will be searched:
173
174        - `path/to/module.luau`
175        - `path/to/module.lua`
176        - `path/to/module/init.luau`
177        - `path/to/module/init.lua`
178
179        If the given path ("path/to/module") is a directory instead,
180        and it exists, it will be returned without any modifications.
181
182        # Errors
183
184        - If the given module path is ambiguous.
185        - If the given module path does not resolve to a valid file or directory.
186    */
187    pub fn resolve(module: impl Into<PathBuf>) -> Result<Self, LuaNavigateError> {
188        let source = module.into();
189        let target = LuauFilePath::resolve(&source)?;
190        Ok(Self { source, target })
191    }
192
193    /**
194        Returns the source Luau module path.
195    */
196    #[must_use]
197    pub fn source(&self) -> &Path {
198        &self.source
199    }
200
201    /**
202        Returns the target filesystem file path.
203    */
204    #[must_use]
205    pub fn target(&self) -> &LuauFilePath {
206        &self.target
207    }
208}
209
210impl fmt::Display for LuauModulePath {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        self.source().display().fmt(f)
213    }
214}