use std::path::Path;
use afterburner_core::{AfterburnerError, Result, ScriptOutcome};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Language {
Js,
Ts,
Rust,
Go,
C,
Cpp,
Python,
Ruby,
}
impl Language {
pub fn from_extension(ext: &str) -> Option<Self> {
match ext.trim().to_ascii_lowercase().as_str() {
"rb" => Some(Self::Ruby),
"py" => Some(Self::Python),
"js" => Some(Self::Js),
"ts" => Some(Self::Ts),
"rs" => Some(Self::Rust),
"go" => Some(Self::Go),
"c" => Some(Self::C),
"cc" | "cpp" => Some(Self::Cpp),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Outcome {
pub stdout: String,
pub stderr: String,
pub value: Option<Value>,
pub ok: bool,
}
impl From<ScriptOutcome> for Outcome {
fn from(s: ScriptOutcome) -> Self {
Self {
stdout: String::from_utf8_lossy(&s.stdout).into_owned(),
stderr: String::from_utf8_lossy(&s.stderr).into_owned(),
value: None,
ok: s.exit_code == 0,
}
}
}
#[cfg(feature = "wasm")]
impl From<afterburner_wasi::pyodide_runner::PyodideRunOutput> for Outcome {
fn from(p: afterburner_wasi::pyodide_runner::PyodideRunOutput) -> Self {
Self {
stdout: String::from_utf8_lossy(&p.stdout).into_owned(),
stderr: String::new(),
value: None,
ok: p.exit_code == 0,
}
}
}
#[cfg(feature = "wasm")]
impl From<afterburner_wasi::ruby_runner::RubyRunOutput> for Outcome {
fn from(r: afterburner_wasi::ruby_runner::RubyRunOutput) -> Self {
Self {
stdout: String::from_utf8_lossy(&r.stdout).into_owned(),
stderr: String::from_utf8_lossy(&r.stderr).into_owned(),
value: None,
ok: r.exit_code == 0,
}
}
}
pub(crate) fn dispatch_run_source(
ab: &crate::Afterburner,
lang: Language,
source: &str,
) -> Result<Outcome> {
match lang {
Language::Js => {
let outcome = ab.run_script(source)?;
Ok(Outcome::from(outcome))
}
#[cfg(feature = "ts")]
Language::Ts => {
let js = crate::ts::transpile(source, Path::new("<run_source>.ts"))
.map_err(|e| AfterburnerError::Engine(format!("TypeScript transpile: {e}")))?;
let outcome = ab.run_script(&js)?;
Ok(Outcome::from(outcome))
}
#[cfg(not(feature = "ts"))]
Language::Ts => Err(AfterburnerError::Engine(
"TypeScript requires the `ts` cargo feature \
(rebuild with `--features ts`)."
.into(),
)),
#[cfg(feature = "wasm")]
Language::Python => {
let out = afterburner_wasi::pyodide_runner::run_python(source)?;
Ok(Outcome::from(out))
}
#[cfg(not(feature = "wasm"))]
Language::Python => Err(AfterburnerError::Engine(
"Python requires the `wasm` feature to be enabled".into(),
)),
#[cfg(feature = "wasm")]
Language::Ruby => {
let out = afterburner_wasi::ruby_runner::run_ruby(source)?;
Ok(Outcome::from(out))
}
#[cfg(not(feature = "wasm"))]
Language::Ruby => Err(AfterburnerError::Engine(
"Ruby requires the `wasm` feature to be enabled".into(),
)),
Language::Rust | Language::Go | Language::C | Language::Cpp => {
Err(AfterburnerError::Engine(format!(
"{lang:?} does not have a source-interpreter path. \
Compile the source to a wasm32-wasip1 module with the \
appropriate toolchain, then use \
`Afterburner::register_precompiled` to register and run it."
)))
}
}
}
pub(crate) fn language_for_path(path: &Path) -> Result<Language> {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
Language::from_extension(ext).ok_or_else(|| {
AfterburnerError::Engine(format!(
"cannot detect language for {:?}: unsupported extension {:?}. \
Supported extensions: rb, py, js, ts, rs, go, c, cc, cpp.",
path.display(),
ext
))
})
}