rustyscript/
module.rs

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