Skip to main content

pipi/generator/
mod.rs

1//! This module defines the `Generator` struct, which is responsible for
2//! executing scripted commands
3
4use std::path::Path;
5pub mod executer;
6pub mod template;
7use std::sync::Arc;
8
9use include_dir::{include_dir, Dir};
10use rhai::{
11    export_module, exported_module,
12    plugin::{
13        Dynamic, FnNamespace, FuncRegistration, Module, NativeCallContext, PluginFunc, RhaiResult,
14        TypeId,
15    },
16    Engine, Scope,
17};
18
19use crate::wizard::AssetsOption;
20use crate::{settings, OS};
21
22static APP_TEMPLATE: Dir<'_> = include_dir!("base_template");
23
24/// Extracts a default template to a temporary directory for use by the
25/// application.
26///
27/// # Errors
28/// when could not extract the the base template
29pub fn extract_default_template() -> std::io::Result<tree_fs::Tree> {
30    let generator_tmp_folder = tree_fs::TreeBuilder::default().create()?;
31
32    APP_TEMPLATE.extract(&generator_tmp_folder.root)?;
33    Ok(generator_tmp_folder)
34}
35
36/// The `Generator` struct provides functionality to execute scripted
37/// operations, such as copying files and templates, based on the current
38/// settings.
39#[derive(Clone)]
40pub struct Generator {
41    pub executer: Arc<dyn executer::Executer>,
42    pub settings: settings::Settings,
43}
44impl Generator {
45    /// Creates a new [`Generator`] with a given executor and settings.
46    pub fn new(executer: Arc<dyn executer::Executer>, settings: settings::Settings) -> Self {
47        Self { executer, settings }
48    }
49
50    /// Runs the default script.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if the script execution fails.
55    pub fn run(&self) -> crate::Result<()> {
56        self.run_from_script(include_str!("../../setup.rhai"))
57    }
58
59    /// Runs a custom script provided as a string.
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if the script execution fails.
64    pub fn run_from_script(&self, script: &str) -> crate::Result<()> {
65        let mut engine = Engine::new();
66
67        tracing::debug!(
68            settings = format!("{:?}", self.settings),
69            script,
70            "prepare installation script"
71        );
72        engine
73            .build_type::<settings::Settings>()
74            .build_type::<settings::Initializers>()
75            .build_type::<settings::Db>()
76            .build_type::<settings::Asset>()
77            .build_type::<settings::Background>()
78            .register_static_module(
79                "rhai_settings_extensions",
80                exported_module!(rhai_settings_extensions).into(),
81            )
82            .register_fn("copy_file", Self::copy_file)
83            .register_fn("create_file", Self::create_file)
84            .register_fn("copy_files", Self::copy_files)
85            .register_fn("copy_dir", Self::copy_dir)
86            .register_fn("copy_dirs", Self::copy_dirs)
87            .register_fn("copy_template", Self::copy_template)
88            .register_fn("copy_template_dir", Self::copy_template_dir);
89
90        let settings_dynamic = rhai::Dynamic::from(self.settings.clone());
91
92        let mut scope = Scope::new();
93        scope.set_value("settings", settings_dynamic);
94        scope.push("gen", self.clone());
95        // TODO:: move it as part of the settings?
96        scope.push("db", self.settings.db.is_some());
97        scope.push("background", self.settings.background.is_some());
98        scope.push("initializers", self.settings.initializers.is_some());
99        scope.push("asset", self.settings.asset.is_some());
100        scope.push("windows", self.settings.os == OS::Windows);
101
102        engine.run_with_scope(&mut scope, script)?;
103        Ok(())
104    }
105
106    /// Copies a single file from the specified path.
107    ///
108    /// # Errors
109    ///
110    /// Returns an error if the file copy operation fails.
111    pub fn copy_file(&mut self, path: &str) -> Result<(), Box<rhai::EvalAltResult>> {
112        let span = tracing::info_span!("copy_file", path);
113        let _guard = span.enter();
114
115        self.executer.copy_file(Path::new(path)).map_err(|err| {
116            Box::new(rhai::EvalAltResult::ErrorSystem(
117                "copy_file".to_string(),
118                err.into(),
119            ))
120        })?;
121        Ok(())
122    }
123
124    /// Creates a single file in the specified path.
125    ///
126    /// # Errors
127    ///
128    /// Returns an error if the file copy operation fails.
129    pub fn create_file(
130        &mut self,
131        path: &str,
132        content: &str,
133    ) -> Result<(), Box<rhai::EvalAltResult>> {
134        let span = tracing::info_span!("create_file", path);
135        let _guard = span.enter();
136
137        self.executer
138            .create_file(Path::new(path), content.to_string())
139            .map_err(|err| {
140                Box::new(rhai::EvalAltResult::ErrorSystem(
141                    "create_file".to_string(),
142                    err.into(),
143                ))
144            })?;
145        Ok(())
146    }
147
148    /// Copies list of files from the specified path.
149    ///
150    /// # Errors
151    ///
152    /// Returns an error if the file copy operation fails.
153    pub fn copy_files(&mut self, paths: rhai::Array) -> Result<(), Box<rhai::EvalAltResult>> {
154        let span = tracing::info_span!("copy_files");
155        let _guard = span.enter();
156        for path in paths {
157            self.executer
158                .copy_file(Path::new(&path.to_string()))
159                .map_err(|err| {
160                    Box::new(rhai::EvalAltResult::ErrorSystem(
161                        "copy_files".to_string(),
162                        err.into(),
163                    ))
164                })?;
165        }
166
167        Ok(())
168    }
169
170    /// Copies an entire directory from the specified path.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if the directory copy operation fails.
175    pub fn copy_dir(&mut self, path: &str) -> Result<(), Box<rhai::EvalAltResult>> {
176        let span = tracing::info_span!("copy_dir", path);
177        let _guard = span.enter();
178        self.executer.copy_dir(Path::new(path)).map_err(|err| {
179            Box::new(rhai::EvalAltResult::ErrorSystem(
180                "copy_dir".to_string(),
181                err.into(),
182            ))
183        })
184    }
185
186    /// Copies list of directories from the specified path.
187    ///
188    /// # Errors
189    ///
190    /// Returns an error if the directory copy operation fails.
191    pub fn copy_dirs(&mut self, paths: rhai::Array) -> Result<(), Box<rhai::EvalAltResult>> {
192        let span = tracing::info_span!("copy_dirs");
193        let _guard = span.enter();
194        for path in paths {
195            self.executer
196                .copy_dir(Path::new(&path.to_string()))
197                .map_err(|err| {
198                    Box::new(rhai::EvalAltResult::ErrorSystem(
199                        "copy_dirs".to_string(),
200                        err.into(),
201                    ))
202                })?;
203        }
204        Ok(())
205    }
206
207    /// Copies a template file from the specified path, applying settings.
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if the template copy operation fails.
212    pub fn copy_template(&mut self, path: &str) -> Result<(), Box<rhai::EvalAltResult>> {
213        let span = tracing::info_span!("copy_template", path);
214        let _guard = span.enter();
215        self.executer
216            .copy_template(Path::new(path), &self.settings)
217            .map_err(|err| {
218                Box::new(rhai::EvalAltResult::ErrorSystem(
219                    "copy_template".to_string(),
220                    err.into(),
221                ))
222            })
223    }
224
225    /// Copies an entire template directory from the specified path, applying
226    /// settings.
227    ///
228    /// # Errors
229    ///
230    /// Returns an error if the template directory copy operation fails.
231    pub fn copy_template_dir(&mut self, path: &str) -> Result<(), Box<rhai::EvalAltResult>> {
232        let span = tracing::info_span!("copy_template_dir", path);
233        let _guard = span.enter();
234        self.executer
235            .copy_template_dir(Path::new(path), &self.settings)
236            .map_err(|err| {
237                Box::new(rhai::EvalAltResult::ErrorSystem(
238                    "copy_template_dir".to_string(),
239                    err.into(),
240                ))
241            })
242    }
243}
244
245/// This module provides extensions to the [`rhai`] scripting language,
246/// enabling ergonomic access to specific.
247/// These extensions allow scripts to interact with internal settings
248/// in a controlled and expressive way.
249#[export_module]
250mod rhai_settings_extensions {
251    /// Checks if the rendering method is set to client-side rendering.
252    #[rhai_fn(global, get = "is_client_side", pure)]
253    pub const fn is_client_side(rendering_method: &mut settings::Asset) -> bool {
254        matches!(rendering_method.kind, AssetsOption::Clientside)
255    }
256
257    /// Checks if the rendering method is set to server-side rendering.
258    #[rhai_fn(global, get = "is_server_side", pure)]
259    pub const fn is_server_side(rendering_method: &mut settings::Asset) -> bool {
260        matches!(rendering_method.kind, AssetsOption::Serverside)
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use executer::MockExecuter;
267    use mockall::predicate::*;
268
269    use super::*;
270
271    #[test]
272    pub fn can_copy_file() {
273        let mut executor = MockExecuter::new();
274
275        executor
276            .expect_copy_file()
277            .with(eq(Path::new("test.rs")))
278            .times(1)
279            .returning(|p| Ok(p.to_path_buf()));
280
281        let g = Generator::new(Arc::new(executor), settings::Settings::default());
282        let script_res = g.run_from_script(r#"gen.copy_file("test.rs");"#);
283
284        assert!(script_res.is_ok());
285    }
286
287    #[test]
288    pub fn can_copy_files() {
289        let mut executor = MockExecuter::new();
290
291        executor
292            .expect_copy_file()
293            .with(eq(Path::new(".gitignore")))
294            .times(1)
295            .returning(|p| Ok(p.to_path_buf()));
296
297        executor
298            .expect_copy_file()
299            .with(eq(Path::new(".rustfmt.toml")))
300            .times(1)
301            .returning(|p| Ok(p.to_path_buf()));
302
303        executor
304            .expect_copy_file()
305            .with(eq(Path::new("README.md")))
306            .times(1)
307            .returning(|p| Ok(p.to_path_buf()));
308
309        let g = Generator::new(Arc::new(executor), settings::Settings::default());
310        let script_res =
311            g.run_from_script(r#"gen.copy_files([".gitignore", ".rustfmt.toml", "README.md"]);"#);
312
313        assert!(script_res.is_ok());
314    }
315
316    #[test]
317    pub fn can_copy_dir() {
318        let mut executor = MockExecuter::new();
319
320        executor
321            .expect_copy_dir()
322            .with(eq(Path::new("test")))
323            .times(1)
324            .returning(|_| Ok(()));
325
326        let g = Generator::new(Arc::new(executor), settings::Settings::default());
327        let script_res = g.run_from_script(r#"gen.copy_dir("test");"#);
328
329        assert!(script_res.is_ok());
330    }
331
332    #[test]
333    pub fn can_copy_dirs() {
334        let mut executor = MockExecuter::new();
335
336        executor
337            .expect_copy_dir()
338            .with(eq(Path::new("src")))
339            .times(1)
340            .returning(|_| Ok(()));
341
342        executor
343            .expect_copy_dir()
344            .with(eq(Path::new("example")))
345            .times(1)
346            .returning(|_| Ok(()));
347
348        executor
349            .expect_copy_dir()
350            .with(eq(Path::new(".github")))
351            .times(1)
352            .returning(|_| Ok(()));
353
354        let g = Generator::new(Arc::new(executor), settings::Settings::default());
355        let script_res = g.run_from_script(r#"gen.copy_dirs(["src", "example", ".github"]);"#);
356
357        assert!(script_res.is_ok());
358    }
359
360    #[test]
361    pub fn can_copy_template() {
362        let mut executor = MockExecuter::new();
363
364        executor
365            .expect_copy_template()
366            .with(eq(Path::new("src/lib.rs.t")), always())
367            .times(1)
368            .returning(|_, _| Ok(()));
369
370        let g = Generator::new(Arc::new(executor), settings::Settings::default());
371        let script_res = g.run_from_script(r#"gen.copy_template("src/lib.rs.t");"#);
372
373        assert!(script_res.is_ok());
374    }
375
376    #[test]
377    pub fn can_copy_template_dir() {
378        let mut executor = MockExecuter::new();
379
380        executor
381            .expect_copy_template_dir()
382            .with(eq(Path::new("src/examples")), always())
383            .times(1)
384            .returning(|_, _| Ok(()));
385
386        let g = Generator::new(Arc::new(executor), settings::Settings::default());
387        let script_res = g.run_from_script(r#"gen.copy_template_dir("src/examples");"#);
388
389        assert!(script_res.is_ok());
390    }
391}