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}