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}