mod elm_type;
mod error;
#[cfg(feature = "quickjs")]
mod quickjs;
#[cfg(feature = "quickjs")]
pub use quickjs::ElmFunctionHandle;
use std::{convert::identity, fs, path::PathBuf, process::Command};
pub use error::{Error, Result};
use serde::de::DeserializeOwned;
use uuid::Uuid;
pub struct ElmRoot {
root_path: PathBuf,
debug: bool,
}
macro_rules! log {
($self: expr, $($arg:tt)*) => {
if $self.debug {
println!($($arg)*)
}
};
}
impl ElmRoot {
pub fn new<P>(path: P) -> Result<Self>
where
PathBuf: From<P>,
{
Ok(Self {
root_path: PathBuf::from(path),
debug: false,
})
}
pub fn debug(self) -> Self {
Self {
debug: true,
..self
}
}
#[cfg(feature = "quickjs")]
pub async fn prepare<I, O>(
&self,
fully_qualified_function: &str,
) -> Result<ElmFunctionHandle<I, O>>
where
I: DeserializeOwned,
O: DeserializeOwned,
{
let elm_binding = self.prepare_shared::<I, O>(fully_qualified_function)?;
quickjs::prepare(self, elm_binding).await
}
fn prepare_shared<I, O>(&self, fully_qualified_function: &str) -> Result<ElmBinding>
where
I: DeserializeOwned,
O: DeserializeOwned,
{
let seed = Uuid::now_v7().as_u128();
log!(self, "Running with seed: {seed}");
let input_type = elm_type::convert::<I>(elm_type::wrap_in_round_brackets)?;
log!(self, "Inferred input type: {input_type}");
let output_type = elm_type::convert::<O>(identity)?;
log!(self, "Inferred output type: {output_type}");
let qualified_segments = fully_qualified_function.split('.').collect::<Vec<_>>();
let Some((function_name, module_path_segments)) = qualified_segments.split_last() else {
return Err(Box::new(Error::InvalidElmCall(
fully_qualified_function.to_owned(),
)));
};
log!(self, "Inferred function name: {function_name}");
let module_name = module_path_segments.join(".");
log!(self, "Inferred module name: {module_name}");
let mut binding_module_name = qualified_segments.join("_");
binding_module_name.push_str("_Binding");
binding_module_name.push_str(&seed.to_string());
log!(self, "Inferred binding module name: {binding_module_name}");
let binding_elm = BINDING_TEMPLATE
.replace("{{ module_path }}", &module_name)
.replace("{{ function_name }}", function_name)
.replace("{{ file_name }}", &binding_module_name)
.replace("{{ input_type }}", &input_type)
.replace("{{ output_type }}", &output_type);
let file_name = binding_module_name.clone() + ".elm";
let file_path = self.root_path.join(&file_name);
fs::write(&file_path, binding_elm).map_err(Error::map_disk_error(file_path.clone()))?;
let binding_js_file_name = binding_module_name.clone() + ".js";
let elm_compile_result = Command::new("elm")
.current_dir(&self.root_path)
.arg("make")
.arg(&file_name)
.arg(format!("--output={binding_js_file_name}"))
.arg("--optimize")
.output();
if !self.debug {
fs::remove_file(&file_path).map_err(Error::map_disk_error(file_path.clone()))?;
}
match elm_compile_result {
Ok(ok) => {
if !ok.stderr.is_empty() {
return Err(Box::new(Error::InvalidElmCall(format!(
"The elm binding failed to compile: {}",
String::from_utf8_lossy(&ok.stderr)
))));
}
}
Err(error) => {
return Err(Box::new(Error::InvalidElmCall(format!(
"Failed to invoke elm compiler: {error}"
))))
}
}
let compiled_binding_file_path = self.root_path.join(binding_js_file_name);
let compiled_binding_result = fs::read_to_string(&compiled_binding_file_path);
if !self.debug {
fs::remove_file(&compiled_binding_file_path)
.map_err(Error::map_disk_error(compiled_binding_file_path.clone()))?;
}
let compiled_binding = compiled_binding_result
.map_err(Error::map_disk_error(compiled_binding_file_path.clone()))?;
Ok(ElmBinding {
compiled_binding,
binding_module_name,
})
}
fn write_esm_binding(
&self,
binding_module_name: &str,
esm_compiled_binding: &str,
) -> Result<()> {
if self.debug {
let esm_binding_path = self
.root_path
.join(format!("{binding_module_name}-esm.mjs"));
fs::write(&esm_binding_path, esm_compiled_binding)
.map_err(Error::map_disk_error(esm_binding_path))?;
}
Ok(())
}
}
struct ElmBinding {
compiled_binding: String,
binding_module_name: String,
}
const BINDING_TEMPLATE: &str = include_str!("./templates/Binding.elm.template");
const TO_ESM_JS: &str = include_str!("./templates/to-esm.mjs");
#[doc = include_str!("../README.md")]
struct _ReadMe;