use std::collections::BTreeMap;
use std::fmt;
use anyhow::Result;
use oxdock_fs::{GuardedPath, PathResolver, WorkspaceFs};
use oxdock_parser::{Step, Value};
use oxdock_process::{DefaultProcessManager, ProcessManager, default_process_manager};
use super::io::ExecIo;
use super::native::{HostModule, HostRegistration, std_module_table};
use super::typing::{OxDockType, TypeDescriptor};
pub struct EngineOutput {
pub cwd: GuardedPath,
pub fs: Box<dyn WorkspaceFs>,
pub bindings: BTreeMap<String, Value>,
}
impl fmt::Debug for EngineOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EngineOutput")
.field("cwd", &self.cwd.as_path().display().to_string())
.field("bindings", &self.bindings)
.finish_non_exhaustive()
}
}
pub struct Engine<P: ProcessManager = DefaultProcessManager> {
modules: Vec<HostModule<P>>,
types: Vec<&'static TypeDescriptor>,
io: ExecIo,
}
impl<P: ProcessManager> Engine<P> {
pub fn new_custom() -> Self {
Self {
modules: Vec::new(),
types: Vec::new(),
io: ExecIo::new(),
}
}
pub fn with_io(mut self, io: ExecIo) -> Self {
self.io = io;
self
}
pub fn register_module(&mut self, module: HostModule<P>) -> &mut Self {
let mut seen: std::collections::HashSet<String> = super::builtin_function_names();
for staged in self.modules.iter().chain(std::iter::once(&module)) {
for registration in &staged.funcs {
let base = match registration {
HostRegistration::Stateful { name, .. }
| HostRegistration::Pure { name, .. } => name,
};
let qualified = format!("{}::{base}", staged.name);
if !seen.insert(qualified.clone()) {
panic!("duplicate function registration `{qualified}`");
}
}
}
self.modules.push(module);
self
}
pub fn register_type<T>(&mut self) -> &mut Self
where
T: OxDockType,
{
self.types.push(T::descriptor());
self
}
pub fn module_table(&self) -> oxdock_parser::ModuleTable {
let mut table = std_module_table();
for module in &self.modules {
let mut functions = std::collections::HashSet::new();
for registration in &module.funcs {
let name = match registration {
HostRegistration::Stateful { name, .. } => name,
HostRegistration::Pure { name, .. } => name,
};
functions.insert(name.clone());
}
table.modules.insert(
module.name.clone(),
Some(oxdock_parser::ModuleFuncs { functions }),
);
}
table
}
pub fn run_script_on(
&self,
fs: Box<dyn WorkspaceFs>,
script: &str,
process: P,
) -> Result<EngineOutput> {
let steps = crate::parse_script_with_modules(script, self.module_table())?;
self.run_steps_on(fs, &steps, process)
}
pub fn run_steps_on(
&self,
fs: Box<dyn WorkspaceFs>,
steps: &[Step],
process: P,
) -> Result<EngineOutput> {
let (cwd, fs, bindings) = super::run_steps_with_manager_with_modules(
fs,
steps,
process,
self.io.clone(),
self.modules.clone(),
self.types.clone(),
)?;
Ok(EngineOutput { cwd, fs, bindings })
}
}
impl Engine<DefaultProcessManager> {
pub fn new() -> Self {
Self {
modules: Vec::new(),
types: Vec::new(),
io: ExecIo::new(),
}
}
pub fn run_script(&self, root: &GuardedPath, script: &str) -> Result<EngineOutput> {
let steps = crate::parse_script_with_modules(script, self.module_table())?;
self.run_steps(root, &steps)
}
pub fn run_steps(&self, root: &GuardedPath, steps: &[Step]) -> Result<EngineOutput> {
let resolver = PathResolver::new_guarded(root.clone(), root.clone())?;
let fs: Box<dyn WorkspaceFs> = Box::new(resolver);
self.run_steps_on(fs, steps, default_process_manager())
}
}
impl<P: ProcessManager> Default for Engine<P> {
fn default() -> Self {
Self::new_custom()
}
}