1mod elm_type;
2mod error;
3#[cfg(feature = "quickjs")]
4mod quickjs;
5#[cfg(feature = "quickjs")]
6pub use quickjs::ElmFunctionHandle;
7
8use std::{convert::identity, fs, path::PathBuf, process::Command};
9
10pub use error::{Error, Result};
11use serde::de::DeserializeOwned;
12use uuid::Uuid;
13
14pub struct ElmRoot {
18 root_path: PathBuf,
19 debug: bool,
20}
21
22macro_rules! log {
23 ($self: expr, $($arg:tt)*) => {
24 if $self.debug {
25 println!($($arg)*)
26 }
27 };
28}
29
30impl ElmRoot {
31 pub fn new<P>(path: P) -> Result<Self>
38 where
39 PathBuf: From<P>,
40 {
41 Ok(Self {
42 root_path: PathBuf::from(path),
43 debug: false,
44 })
45 }
46
47 pub fn debug(self) -> Self {
53 Self {
54 debug: true,
55 ..self
56 }
57 }
58
59 #[cfg(feature = "quickjs")]
70 pub async fn prepare<I, O>(
71 &self,
72 fully_qualified_function: &str,
73 ) -> Result<ElmFunctionHandle<I, O>>
74 where
75 I: DeserializeOwned,
76 O: DeserializeOwned,
77 {
78 let elm_binding = self.prepare_shared::<I, O>(fully_qualified_function)?;
79 quickjs::prepare(self, elm_binding).await
80 }
81
82 fn prepare_shared<I, O>(&self, fully_qualified_function: &str) -> Result<ElmBinding>
83 where
84 I: DeserializeOwned,
85 O: DeserializeOwned,
86 {
87 let seed = Uuid::now_v7().as_u128();
89 log!(self, "Running with seed: {seed}");
90 let input_type = elm_type::convert::<I>(elm_type::wrap_in_round_brackets)?;
92 log!(self, "Inferred input type: {input_type}");
93
94 let output_type = elm_type::convert::<O>(identity)?;
95 log!(self, "Inferred output type: {output_type}");
96
97 let qualified_segments = fully_qualified_function.split('.').collect::<Vec<_>>();
98 let Some((function_name, module_path_segments)) = qualified_segments.split_last() else {
99 return Err(Box::new(Error::InvalidElmCall(
100 fully_qualified_function.to_owned(),
101 )));
102 };
103 log!(self, "Inferred function name: {function_name}");
104
105 let module_name = module_path_segments.join(".");
106 log!(self, "Inferred module name: {module_name}");
107
108 let mut binding_module_name = qualified_segments.join("_");
109 binding_module_name.push_str("_Binding");
110 binding_module_name.push_str(&seed.to_string());
111 log!(self, "Inferred binding module name: {binding_module_name}");
112
113 let binding_elm = BINDING_TEMPLATE
114 .replace("{{ module_path }}", &module_name)
115 .replace("{{ function_name }}", function_name)
116 .replace("{{ file_name }}", &binding_module_name)
117 .replace("{{ input_type }}", &input_type)
118 .replace("{{ output_type }}", &output_type);
119
120 let file_name = binding_module_name.clone() + ".elm";
121 let file_path = self.root_path.join(&file_name);
122
123 fs::write(&file_path, binding_elm).map_err(Error::map_disk_error(file_path.clone()))?;
124
125 let binding_js_file_name = binding_module_name.clone() + ".js";
127 let elm_compile_result = Command::new("elm")
128 .current_dir(&self.root_path)
129 .arg("make")
130 .arg(&file_name)
131 .arg(format!("--output={binding_js_file_name}"))
132 .arg("--optimize")
133 .output();
134 if !self.debug {
135 fs::remove_file(&file_path).map_err(Error::map_disk_error(file_path.clone()))?;
136 }
137 match elm_compile_result {
138 Ok(ok) => {
139 if !ok.stderr.is_empty() {
140 return Err(Box::new(Error::InvalidElmCall(format!(
141 "The elm binding failed to compile: {}",
142 String::from_utf8_lossy(&ok.stderr)
143 ))));
144 }
145 }
146 Err(error) => {
147 return Err(Box::new(Error::InvalidElmCall(format!(
148 "Failed to invoke elm compiler: {error}"
149 ))))
150 }
151 }
152
153 let compiled_binding_file_path = self.root_path.join(binding_js_file_name);
154 let compiled_binding_result = fs::read_to_string(&compiled_binding_file_path);
155 if !self.debug {
156 fs::remove_file(&compiled_binding_file_path)
157 .map_err(Error::map_disk_error(compiled_binding_file_path.clone()))?;
158 }
159 let compiled_binding = compiled_binding_result
160 .map_err(Error::map_disk_error(compiled_binding_file_path.clone()))?;
161 Ok(ElmBinding {
162 compiled_binding,
163 binding_module_name,
164 })
165 }
166
167 fn write_esm_binding(
168 &self,
169 binding_module_name: &str,
170 esm_compiled_binding: &str,
171 ) -> Result<()> {
172 if self.debug {
173 let esm_binding_path = self
174 .root_path
175 .join(format!("{binding_module_name}-esm.mjs"));
176 fs::write(&esm_binding_path, esm_compiled_binding)
177 .map_err(Error::map_disk_error(esm_binding_path))?;
178 }
179 Ok(())
180 }
181}
182
183struct ElmBinding {
184 compiled_binding: String,
185 binding_module_name: String,
186}
187
188const BINDING_TEMPLATE: &str = include_str!("./templates/Binding.elm.template");
189const TO_ESM_JS: &str = include_str!("./templates/to-esm.mjs");
190
191#[doc = include_str!("../README.md")]
192struct _ReadMe;