rustyscript/
module.rs

1use maybe_path::MaybePathBuf;
2use serde::{Deserialize, Serialize};
3use std::borrow::Cow;
4use std::ffi::OsStr;
5use std::fmt::Display;
6use std::fs::{read_dir, read_to_string};
7use std::path::{Path, PathBuf};
8
9/// Creates a static module
10///
11/// This is just a macro around [`Module::new_static`]
12///
13/// # Arguments
14/// * `filename` - A string representing the filename of the module.
15/// * `contents` - A string containing the contents of the module.
16///
17/// Note that the contents argument is optional;
18/// if not provided, the macro will attempt to include the file at the given path.
19///
20/// # Example
21///
22/// ```rust
23/// use rustyscript::{ module, Module };
24///
25/// const MY_SCRIPT: Module = module!(
26///     "filename.js",
27///     "export const myValue = 42;"
28/// );
29/// ```
30#[macro_export]
31macro_rules! module {
32    ($filename:literal, $contents:literal) => {
33        $crate::Module::new_static($filename, $contents)
34    };
35
36    ($filename:literal) => {
37        Module::new_static($filename, include_str!($filename))
38    };
39}
40
41/// Creates a static module based on a statically included file
42///
43/// # Arguments
44/// * `filename` - A string representing the filename of the module.
45///
46/// See [module] for an example
47#[macro_export]
48macro_rules! include_module {
49    ($filename:literal) => {
50        Module::new_static($filename, include_str!($filename))
51    };
52}
53
54#[derive(Clone, Debug, Eq, PartialEq, Serialize, Default)]
55/// Represents a piece of javascript for execution.
56///
57/// Can be loaded from data at runtime, with `Module::new`, or from a file with `Module::load`.
58///
59/// It can also be loaded statically with `Module::new_static` or `module!`
60pub struct Module {
61    filename: MaybePathBuf<'static>,
62    contents: Cow<'static, str>,
63}
64
65impl<'de> Deserialize<'de> for Module {
66    fn deserialize<D>(deserializer: D) -> Result<Module, D::Error>
67    where
68        D: serde::Deserializer<'de>,
69    {
70        #[derive(Deserialize)]
71        struct OwnedModule {
72            filename: PathBuf,
73            contents: String,
74        }
75
76        let OwnedModule { filename, contents } = OwnedModule::deserialize(deserializer)?;
77        Ok(Module::new(filename, contents))
78    }
79}
80
81impl Display for Module {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        write!(f, "{}", self.filename().display())
84    }
85}
86
87impl Module {
88    /// Creates a new `Module` instance with the given filename and contents.
89    ///
90    /// If filename is relative it will be resolved to the current working dir at runtime
91    ///
92    /// # Arguments
93    /// * `filename` - A string representing the filename of the module.
94    /// * `contents` - A string containing the contents of the module.
95    ///
96    /// # Returns
97    /// A new `Module` instance.
98    ///
99    /// # Example
100    ///
101    /// ```rust
102    /// use rustyscript::Module;
103    ///
104    /// let module = Module::new("module.js", "console.log('Hello, World!');");
105    /// ```
106    #[must_use]
107    pub fn new(filename: impl AsRef<Path>, contents: impl ToString) -> Self {
108        let filename = MaybePathBuf::Owned(filename.as_ref().to_path_buf());
109        let contents = Cow::Owned(contents.to_string());
110
111        Self { filename, contents }
112    }
113
114    /// Creates a new `Module` instance with the given filename and contents.  
115    /// The function is const, and the filename and contents are static strings.
116    ///
117    /// If filename is relative it will be resolved to the current working dir at runtime
118    ///
119    /// # Arguments
120    /// * `filename` - A string representing the filename of the module.
121    /// * `contents` - A string containing the contents of the module.
122    ///
123    /// # Returns
124    /// A new `Module` instance.
125    ///
126    /// # Example
127    ///
128    /// ```rust
129    /// use rustyscript::Module;
130    ///
131    /// let module = Module::new("module.js", "console.log('Hello, World!');");
132    /// ```
133    #[must_use]
134    pub const fn new_static(filename: &'static str, contents: &'static str) -> Self {
135        Self {
136            filename: MaybePathBuf::new_str(filename),
137            contents: Cow::Borrowed(contents),
138        }
139    }
140
141    /// Loads a `Module` instance from a file with the given filename.
142    ///
143    /// # Arguments
144    /// * `filename` - A string representing the filename of the module file.
145    ///
146    /// # Returns
147    /// A `Result` containing the loaded `Module` instance or an `std::io::Error` if there
148    /// are issues reading the file.
149    ///
150    /// # Errors
151    /// Will return an error if the file cannot be read.
152    ///
153    /// # Example
154    ///
155    /// ```rust
156    /// use rustyscript::Module;
157    ///
158    /// # fn main() -> Result<(), rustyscript::Error> {
159    /// let module = Module::load("src/ext/rustyscript/rustyscript.js")?;
160    /// # Ok(())
161    /// # }
162    /// ```
163    pub fn load(filename: impl AsRef<Path>) -> Result<Self, std::io::Error> {
164        let contents = read_to_string(filename.as_ref())?;
165        Ok(Self::new(filename, &contents))
166    }
167
168    /// Attempt to load all `.js`/`.ts` files in a given directory
169    ///
170    /// Fails if any of the files cannot be loaded
171    ///
172    /// # Arguments
173    /// * `directory` - A string representing the target directory
174    ///
175    /// # Returns
176    /// A `Result` containing a vec of loaded `Module` instances or an `std::io::Error` if there
177    /// are issues reading a file.
178    ///
179    /// # Errors
180    /// Will return an error if the directory cannot be read, or if any contained file cannot be read.
181    ///
182    /// # Example
183    ///
184    /// ```rust
185    /// use rustyscript::Module;
186    ///
187    /// # fn main() -> Result<(), rustyscript::Error> {
188    /// let all_modules = Module::load_dir("src/ext/rustyscript")?;
189    /// # Ok(())
190    /// # }
191    /// ```
192    pub fn load_dir(directory: impl AsRef<Path>) -> Result<Vec<Self>, std::io::Error> {
193        let mut files: Vec<Self> = Vec::new();
194        for file in read_dir(directory)? {
195            let file = file?;
196            if let Some(filename) = file.path().to_str() {
197                // Skip non-js files
198                let extension = Path::new(&filename)
199                    .extension()
200                    .and_then(OsStr::to_str)
201                    .unwrap_or_default();
202                if !["js", "ts"].contains(&extension) {
203                    continue;
204                }
205
206                files.push(Self::load(filename)?);
207            }
208        }
209
210        Ok(files)
211    }
212
213    /// Returns the filename of the module.
214    ///
215    /// # Returns
216    /// A reference to a string containing the filename.
217    ///
218    /// # Example
219    ///
220    /// ```rust
221    /// use rustyscript::Module;
222    ///
223    /// let module = Module::new("module.js", "console.log('Hello, World!');");
224    /// println!("Filename: {:?}", module.filename());
225    /// ```
226    #[must_use]
227    pub fn filename(&self) -> &Path {
228        self.filename.as_ref()
229    }
230
231    /// Returns the contents of the module.
232    ///
233    /// # Returns
234    /// A reference to a string containing the module contents.
235    ///
236    /// # Example
237    ///
238    /// ```rust
239    /// use rustyscript::Module;
240    ///
241    /// let module = Module::new("module.js", "console.log('Hello, World!');");
242    /// println!("Module Contents: {}", module.contents());
243    /// ```
244    #[must_use]
245    pub fn contents(&self) -> &str {
246        &self.contents
247    }
248}
249
250#[cfg(test)]
251mod test_module {
252    use super::*;
253
254    #[test]
255    fn test_new_module() {
256        let module = Module::new("module.js", "console.log('Hello, World!');");
257        assert_eq!(module.filename().to_str().unwrap(), "module.js");
258        assert_eq!(module.contents(), "console.log('Hello, World!');");
259    }
260
261    #[test]
262    fn test_load_module() {
263        let module =
264            Module::load("src/ext/rustyscript/rustyscript.js").expect("Failed to load module");
265        assert_eq!(
266            module.filename().to_str().unwrap(),
267            "src/ext/rustyscript/rustyscript.js"
268        );
269    }
270
271    #[test]
272    fn test_load_dir() {
273        let modules =
274            Module::load_dir("src/ext/rustyscript").expect("Failed to load modules from directory");
275        assert!(!modules.is_empty());
276    }
277}