1mod elm_type;
2mod error;
3
4use std::{
5 cell::RefCell, convert::identity, fs, marker::PhantomData, path::PathBuf, process::Command,
6};
7
8pub use error::{Error, Result};
9use rustyscript::{
10 deno_core::serde::{de::DeserializeOwned, Serialize},
11 Module, ModuleHandle, Runtime,
12};
13use uuid::Uuid;
14
15thread_local! {
16 static RUNTIME: RefCell<Runtime> = RefCell::new(Runtime::new(Default::default())
17 .expect("V8 Javascript Runtime initialization failed"));
18}
19
20pub struct ElmRoot {
24 root_path: PathBuf,
25 debug: bool,
26}
27
28macro_rules! log {
29 ($self: expr, $($arg:tt)*) => {
30 if $self.debug {
31 println!($($arg)*)
32 }
33 };
34}
35
36impl ElmRoot {
37 pub fn new<P>(path: P) -> Result<Self>
44 where
45 PathBuf: From<P>,
46 {
47 Ok(Self {
48 root_path: PathBuf::from(path),
49 debug: false,
50 })
51 }
52
53 pub fn debug(self) -> Self {
59 Self {
60 debug: true,
61 ..self
62 }
63 }
64
65 pub fn prepare<I, O>(&self, fully_qualified_function: &str) -> Result<ElmFunctionHandle<I, O>>
76 where
77 I: DeserializeOwned,
78 O: DeserializeOwned,
79 {
80 let seed = Uuid::now_v7().as_u128();
82 log!(self, "Running with seed: {seed}");
83 let input_type = elm_type::convert::<I>(elm_type::wrap_in_round_brackets)?;
85 log!(self, "Inferred input type: {input_type}");
86
87 let output_type = elm_type::convert::<O>(identity)?;
88 log!(self, "Inferred output type: {output_type}");
89
90 let qualified_segments = fully_qualified_function.split('.').collect::<Vec<_>>();
91 let Some((function_name, module_path_segments)) = qualified_segments.split_last() else {
92 return Err(Error::InvalidElmCall(fully_qualified_function.to_owned()));
93 };
94 log!(self, "Inferred function name: {function_name}");
95
96 let module_name = module_path_segments.join(".");
97 log!(self, "Inferred module name: {module_name}");
98
99 let mut binding_module_name = qualified_segments.join("_");
100 binding_module_name.push_str("_Binding");
101 binding_module_name.push_str(&seed.to_string());
102 log!(self, "Inferred binding module name: {binding_module_name}");
103
104 let binding_elm = BINDING_TEMPLATE
105 .replace("{{ module_path }}", &module_name)
106 .replace("{{ function_name }}", function_name)
107 .replace("{{ file_name }}", &binding_module_name)
108 .replace("{{ input_type }}", &input_type)
109 .replace("{{ output_type }}", &output_type);
110
111 let file_name = binding_module_name.clone() + ".elm";
112 let file_path = self.root_path.join(&file_name);
113
114 fs::write(&file_path, binding_elm).map_err(Error::map_disk_error(file_path.clone()))?;
115
116 let binding_js_file_name = binding_module_name.clone() + ".js";
118 let elm_compile_result = Command::new("elm")
119 .current_dir(&self.root_path)
120 .arg("make")
121 .arg(&file_name)
122 .arg(format!("--output={binding_js_file_name}"))
123 .arg("--optimize")
124 .output();
125 if !self.debug {
126 fs::remove_file(&file_path).map_err(Error::map_disk_error(file_path.clone()))?;
127 }
128 match elm_compile_result {
129 Ok(ok) => {
130 if !ok.stderr.is_empty() {
131 return Err(Error::InvalidElmCall(format!(
132 "The elm binding failed to compile: {}",
133 String::from_utf8_lossy(&ok.stderr)
134 )));
135 }
136 }
137 Err(error) => {
138 return Err(Error::InvalidElmCall(format!(
139 "Failed to invoke elm compiler: {error}"
140 )))
141 }
142 }
143
144 let compiled_binding_file_path = self.root_path.join(binding_js_file_name);
145 let compiled_binding_result = fs::read_to_string(&compiled_binding_file_path);
146 if !self.debug {
147 fs::remove_file(&compiled_binding_file_path)
148 .map_err(Error::map_disk_error(compiled_binding_file_path.clone()))?;
149 }
150 let compiled_binding = compiled_binding_result
151 .map_err(Error::map_disk_error(compiled_binding_file_path.clone()))?;
152
153 let to_esm = Module::new("to-esm.js", TO_ESM_JS);
155 let esm_compiled_binding: String = RUNTIME.with_borrow_mut(|runtime| {
156 let handle = runtime.load_module(&to_esm)?;
157 let result: String = runtime.call_entrypoint(&handle, &[compiled_binding])?;
158 Ok::<_, Error>(result)
159 })?;
160 if self.debug {
161 let esm_binding_path = self
162 .root_path
163 .join(format!("{binding_module_name}-esm.mjs"));
164 fs::write(&esm_binding_path, esm_compiled_binding.clone())
165 .map_err(Error::map_disk_error(esm_binding_path))?;
166 }
167 let debug_extras = if self.debug {
169 "console.log('Calling elm binding with', flags);"
170 } else {
171 ""
172 };
173 let wrapper = Module::new(
174 "run.js",
175 &RUN_JS_TEMPLATE
176 .replace("{{ binding_module_name }}", &binding_module_name)
177 .replace("{{ debug_extras }}", debug_extras),
178 );
179 let binding_module = Module::new("./binding.js", &esm_compiled_binding);
180 let module_handle = RUNTIME
181 .with_borrow_mut(|runtime| runtime.load_modules(&wrapper, vec![&binding_module]))?;
182
183 Ok(ElmFunctionHandle {
184 module: module_handle,
185 _type: Default::default(),
186 })
187 }
188}
189
190pub struct ElmFunctionHandle<I, O> {
193 module: ModuleHandle,
194 _type: PhantomData<(I, O)>,
195}
196
197impl<I, O> ElmFunctionHandle<I, O>
198where
199 I: Serialize,
200 O: DeserializeOwned,
201{
202 pub fn call(&self, input: I) -> Result<O> {
204 let output =
205 RUNTIME.with_borrow_mut(|runtime| runtime.call_entrypoint(&self.module, &[input]))?;
206 Ok(output)
207 }
208}
209
210const BINDING_TEMPLATE: &str = include_str!("./templates/Binding.elm.template");
211const RUN_JS_TEMPLATE: &str = include_str!("./templates/run.js.template");
212const TO_ESM_JS: &str = include_str!("./templates/to-esm.mjs");
213
214#[doc = include_str!("../README.md")]
215struct _ReadMe;