use std::cell::RefCell;
use std::sync::Arc;
use rquickjs::class::{Class, Trace};
use rquickjs::function::{Args, Constructor, Func, Opt, Rest};
use rquickjs::{
ArrayBuffer, AsyncContext, CatchResultExt, Ctx, Function, JsLifetime, Object, Persistent, TypedArray, Value,
async_with,
};
use crate::bindings::convert::{serde_from_js, serde_to_js};
use crate::bindings::{install_browser_context_on, install_browser_on, install_page_on, install_request_on};
use crate::engine::caught_to_script_error;
use crate::error::ScriptError;
const SKIP_SENTINEL: &str = "__ferri_skip__";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepKind {
Given,
When,
Then,
Step,
}
impl StepKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Given => "Given",
Self::When => "When",
Self::Then => "Then",
Self::Step => "Step",
}
}
}
struct StepReg {
kind: StepKind,
pattern: String,
is_regex: bool,
func: Persistent<Function<'static>>,
timeout_ms: Option<u64>,
}
struct HookReg {
kind: String,
tags: Option<String>,
func: Persistent<Function<'static>>,
timeout_ms: Option<u64>,
}
struct ParamTypeReg {
name: String,
regexp: String,
transformer: Option<Persistent<Function<'static>>>,
}
struct ToolReg {
name: String,
description: Option<String>,
input_schema: Option<serde_json::Value>,
expose_as_tool: bool,
allowed_commands: std::collections::BTreeMap<String, crate::command_spec::CommandSpec>,
allowed_net: Vec<String>,
timeout_ms: Option<u64>,
handler: Persistent<Function<'static>>,
}
#[derive(Debug, Clone)]
pub struct ScriptAttachment {
pub media_type: String,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone, Default)]
pub struct HookArg {
pub name: String,
pub tags: Vec<String>,
pub status: String,
pub message: Option<String>,
}
#[derive(Default)]
struct ExtensionRegistry {
steps: Vec<StepReg>,
hooks: Vec<HookReg>,
param_types: Vec<ParamTypeReg>,
tools: Vec<ToolReg>,
attachments: Vec<ScriptAttachment>,
default_timeout_ms: u64,
def_fn_wrapper: Option<Persistent<Function<'static>>>,
world_ctor: Option<Persistent<Constructor<'static>>>,
current_world: Option<Persistent<Object<'static>>>,
}
fn timeout_from_opts(args: &[Value<'_>]) -> Option<u64> {
args.iter().find_map(|v| {
let o = v.as_object()?;
if v.as_function().is_some() {
return None;
}
o.get::<_, f64>("timeout").ok().map(|ms| ms.max(0.0) as u64)
})
}
struct BddUserData(RefCell<ExtensionRegistry>);
#[allow(unsafe_code)]
unsafe impl JsLifetime<'_> for BddUserData {
type Changed<'to> = BddUserData;
}
fn with_registry<R>(ctx: &Ctx<'_>, f: impl FnOnce(&mut ExtensionRegistry) -> R) -> Result<R, ScriptError> {
let ud = ctx
.userdata::<BddUserData>()
.ok_or_else(|| ScriptError::internal("bdd registry not installed".to_string()))?;
let mut reg = ud.0.borrow_mut();
Ok(f(&mut reg))
}
#[derive(Trace, JsLifetime)]
#[rquickjs::class(rename = "DataTable")]
pub struct DataTableJs {
#[qjs(skip_trace)]
rows: Vec<Vec<String>>,
}
#[rquickjs::methods]
impl DataTableJs {
#[qjs(rename = "raw")]
pub fn raw(&self) -> Vec<Vec<String>> {
self.rows.clone()
}
#[qjs(rename = "rows")]
pub fn data_rows(&self) -> Vec<Vec<String>> {
self.rows.iter().skip(1).cloned().collect()
}
#[qjs(rename = "hashes")]
pub fn hashes<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
let header = self.rows.first().cloned().unwrap_or_default();
let out: Vec<serde_json::Map<String, serde_json::Value>> = self
.rows
.iter()
.skip(1)
.map(|row| {
header
.iter()
.zip(row.iter())
.map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
.collect()
})
.collect();
serde_to_js(&ctx, &out)
}
#[qjs(rename = "rowsHash")]
pub fn rows_hash<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
let map: serde_json::Map<String, serde_json::Value> = self
.rows
.iter()
.filter(|r| r.len() >= 2)
.map(|r| (r[0].clone(), serde_json::Value::String(r[1].clone())))
.collect();
serde_to_js(&ctx, &map)
}
#[qjs(rename = "transpose")]
pub fn transpose<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Class<'js, DataTableJs>> {
let cols = self.rows.iter().map(Vec::len).max().unwrap_or(0);
let rows = (0..cols)
.map(|c| {
self
.rows
.iter()
.map(|r| r.get(c).cloned().unwrap_or_default())
.collect()
})
.collect();
Class::instance(ctx, DataTableJs { rows })
}
}
fn as_function<'js>(v: &Value<'js>) -> Option<Function<'js>> {
v.as_function().cloned()
}
fn pattern_of(a: &Value<'_>) -> Result<(String, bool), ScriptError> {
if let Some(s) = a.as_string() {
return Ok((s.to_string().map_err(|e| ScriptError::internal(e.to_string()))?, false));
}
if let Some(o) = a.as_object() {
if let Ok(src) = o.get::<_, String>("source") {
return Ok((src, true));
}
}
Err(ScriptError::internal(
"step pattern must be a string or RegExp".to_string(),
))
}
fn rq(e: &ScriptError) -> rquickjs::Error {
rquickjs::Error::new_from_js_message("bdd", "Error", e.message.clone())
}
fn ctx_of<'js>(args: &[Value<'js>]) -> Result<Ctx<'js>, rquickjs::Error> {
args
.first()
.map(|v| v.ctx().clone())
.ok_or_else(|| rq(&ScriptError::internal("missing arguments".to_string())))
}
fn register_step(kind: StepKind, args: &[Value<'_>]) -> rquickjs::Result<()> {
let ctx = ctx_of(args)?;
let pattern = args
.first()
.ok_or_else(|| rq(&ScriptError::internal("step pattern missing".to_string())))?;
let (pat, is_regex) = pattern_of(pattern).map_err(|e| rq(&e))?;
let func = args
.iter()
.skip(1)
.rev()
.find_map(as_function)
.ok_or_else(|| rq(&ScriptError::internal(format!("step `{pat}` has no function body"))))?;
let timeout_ms = timeout_from_opts(&args[1..]);
let saved = Persistent::save(&ctx, func);
with_registry(&ctx, |reg| {
reg.steps.push(StepReg {
kind,
pattern: pat,
is_regex,
func: saved,
timeout_ms,
});
})
.map_err(|e| rq(&e))
}
fn register_hook(kind: &str, args: &[Value<'_>]) -> rquickjs::Result<()> {
let ctx = ctx_of(args)?;
let first = args
.first()
.ok_or_else(|| rq(&ScriptError::internal(format!("{kind} hook missing"))))?;
let (tags, func) = if let Some(f) = as_function(first) {
(None, f)
} else {
let tags = if let Some(s) = first.as_string() {
Some(s.to_string().map_err(|e| rq(&ScriptError::internal(e.to_string())))?)
} else if let Some(o) = first.as_object() {
o.get::<_, String>("tags").ok()
} else {
None
};
let f = args
.iter()
.skip(1)
.find_map(as_function)
.ok_or_else(|| rq(&ScriptError::internal(format!("{kind} hook has no function body"))))?;
(tags, f)
};
let timeout_ms = timeout_from_opts(args);
let saved = Persistent::save(&ctx, func);
with_registry(&ctx, |reg| {
reg.hooks.push(HookReg {
kind: kind.to_string(),
tags,
func: saved,
timeout_ms,
});
})
.map_err(|e| rq(&e))
}
fn value_bytes(v: &Value<'_>) -> Option<Vec<u8>> {
if let Ok(ta) = TypedArray::<u8>::from_value(v.clone())
&& let Some(b) = ta.as_bytes()
{
return Some(b.to_vec());
}
if let Some(obj) = v.as_object()
&& let Some(buf) = ArrayBuffer::from_object(obj.clone())
&& let Some(b) = buf.as_bytes()
{
return Some(b.to_vec());
}
None
}
fn register_attachment(args: &[Value<'_>], is_log: bool) -> rquickjs::Result<()> {
let ctx = ctx_of(args)?;
let media_arg = args.get(1).and_then(Value::as_string).and_then(|s| s.to_string().ok());
let (bytes, media): (Vec<u8>, String) = if is_log {
let text = args
.iter()
.map(|v| {
v.as_string().and_then(|s| s.to_string().ok()).unwrap_or_else(|| {
serde_from_js::<serde_json::Value>(&ctx, v.clone())
.map(|j| j.to_string())
.unwrap_or_default()
})
})
.collect::<Vec<_>>()
.join(" ");
(text.into_bytes(), "text/x.cucumber.log+plain".to_string())
} else {
let data = args
.first()
.cloned()
.unwrap_or_else(|| Value::new_undefined(ctx.clone()));
if let Some(s) = data.as_string() {
let s = s.to_string().map_err(|e| rq(&ScriptError::internal(e.to_string())))?;
(s.into_bytes(), media_arg.unwrap_or_else(|| "text/plain".to_string()))
} else if let Some(b) = value_bytes(&data) {
(b, media_arg.unwrap_or_else(|| "application/octet-stream".to_string()))
} else {
let j: serde_json::Value = serde_from_js(&ctx, data).map_err(|e| rq(&ScriptError::internal(e.to_string())))?;
(
serde_json::to_vec(&j).unwrap_or_default(),
media_arg.unwrap_or_else(|| "application/json".to_string()),
)
}
};
with_registry(&ctx, |reg| {
reg.attachments.push(ScriptAttachment {
media_type: media,
bytes,
});
})
.map_err(|e| rq(&e))
}
pub async fn drain_attachments(actx: &AsyncContext) -> Result<Vec<ScriptAttachment>, ScriptError> {
async_with!(actx => |ctx| {
with_registry(&ctx, |reg| std::mem::take(&mut reg.attachments))
})
.await
}
fn register_tool<'js>(ctx: &Ctx<'js>, m: &Object<'js>, handler: Function<'js>) -> Result<(), ScriptError> {
let name: String = m
.get("name")
.map_err(|e| ScriptError::internal(format!("tool manifest missing string `name`: {e}")))?;
if name.trim().is_empty() {
return Err(ScriptError::internal(
"defineTool: `name` must be a non-empty string".to_string(),
));
}
let description = m
.get::<_, Value<'_>>("description")
.ok()
.and_then(|v| v.as_string().and_then(|s| s.to_string().ok()));
let input_schema = match m.get::<_, Value<'_>>("inputSchema") {
Ok(v) if !v.is_undefined() && !v.is_null() => {
Some(serde_from_js::<serde_json::Value>(ctx, v).map_err(|e| ScriptError::internal(e.to_string()))?)
},
_ => None,
};
let expose_as_tool = m.get::<_, bool>("exposeAsTool").unwrap_or(false);
let timeout_ms = m
.get::<_, f64>("timeoutMs")
.ok()
.map(|ms| ms.max(0.0) as u64)
.filter(|&v| v > 0);
let (allowed_commands, allowed_net) = match m.get::<_, Value<'_>>("allow") {
Ok(v) => {
if let Some(allow) = v.as_object() {
let commands = ["exec", "commands"]
.into_iter()
.find_map(|k| match allow.get::<_, Value<'_>>(k) {
Ok(c) if c.is_object() => serde_from_js(ctx, c).ok(),
_ => None,
})
.unwrap_or_default();
let net = match allow.get::<_, Value<'_>>("net") {
Ok(n) if !n.is_undefined() && !n.is_null() => serde_from_js(ctx, n).unwrap_or_default(),
_ => Vec::new(),
};
(commands, net)
} else {
(std::collections::BTreeMap::new(), Vec::new())
}
},
Err(_) => (std::collections::BTreeMap::new(), Vec::new()),
};
let saved = Persistent::save(ctx, handler);
with_registry(ctx, |reg| {
if reg.tools.iter().any(|t| t.name == name) {
return Err(ScriptError::internal(format!(
"defineTool: duplicate tool name `{name}` — names must be unique across all loaded extensions"
)));
}
reg.tools.push(ToolReg {
name,
description,
input_schema,
expose_as_tool,
allowed_commands,
allowed_net,
timeout_ms,
handler: saved,
});
Ok(())
})?
}
fn register_tool_args(args: &[Value<'_>]) -> rquickjs::Result<()> {
let ctx = ctx_of(args)?;
let manifest = args.first().and_then(Value::as_object).ok_or_else(|| {
rq(&ScriptError::internal(
"defineTool: first arg must be a tool/manifest object".to_string(),
))
})?;
let handler = args
.iter()
.skip(1)
.find_map(as_function)
.or_else(|| {
manifest
.get::<_, Value<'_>>("handler")
.ok()
.and_then(|v| v.as_function().cloned())
})
.ok_or_else(|| {
rq(&ScriptError::internal(
"defineTool: no handler — pass defineTool(tool) with a `handler` method or defineTool(manifest, fn)"
.to_string(),
))
})?;
register_tool(&ctx, manifest, handler).map_err(|e| rq(&e))
}
pub fn install_bdd(ctx: &Ctx<'_>) -> rquickjs::Result<()> {
if ctx.userdata::<BddUserData>().is_some() {
return Ok(());
}
let _ = ctx.store_userdata(BddUserData(RefCell::new(ExtensionRegistry {
default_timeout_ms: 5000,
..ExtensionRegistry::default()
})));
let g = ctx.globals();
Class::<DataTableJs>::define(&g)?;
for (name, kind) in [
("Given", StepKind::Given),
("When", StepKind::When),
("Then", StepKind::Then),
("defineStep", StepKind::Step),
("And", StepKind::Step),
("But", StepKind::Step),
] {
g.set(
name,
Func::from(move |args: Rest<Value<'_>>| register_step(kind, &args.0)),
)?;
}
for hook in ["Before", "After", "BeforeAll", "AfterAll", "BeforeStep", "AfterStep"] {
g.set(
hook,
Func::from(move |args: Rest<Value<'_>>| register_hook(hook, &args.0)),
)?;
}
g.set(
"defineParameterType",
Func::from(|def: Object<'_>| -> rquickjs::Result<()> {
let ctx = def.ctx().clone();
let name: String = def.get("name").map_err(|e| rq(&ScriptError::internal(e.to_string())))?;
let rx_val: Value<'_> = def
.get("regexp")
.map_err(|e| rq(&ScriptError::internal(e.to_string())))?;
let regexp = if let Some(s) = rx_val.as_string() {
s.to_string().map_err(|e| rq(&ScriptError::internal(e.to_string())))?
} else if let Some(o) = rx_val.as_object() {
o.get::<_, String>("source")
.map_err(|e| rq(&ScriptError::internal(e.to_string())))?
} else {
return Err(rq(&ScriptError::internal(
"parameter type regexp must be string or RegExp".to_string(),
)));
};
let transformer = def
.get::<_, Value<'_>>("transformer")
.ok()
.and_then(|v| v.as_function().cloned())
.map(|f| Persistent::save(&ctx, f));
with_registry(&ctx, |reg| {
reg.param_types.push(ParamTypeReg {
name,
regexp,
transformer,
});
})
.map_err(|e| rq(&e))
}),
)?;
g.set(
"setDefaultTimeout",
Func::from(|ctx: Ctx<'_>, ms: f64| -> rquickjs::Result<()> {
with_registry(&ctx, |reg| reg.default_timeout_ms = ms.max(0.0) as u64).map_err(|e| rq(&e))
}),
)?;
g.set(
"setDefinitionFunctionWrapper",
Func::from(|w: Function<'_>| -> rquickjs::Result<()> {
let ctx = w.ctx().clone();
let saved = Persistent::save(&ctx, w);
with_registry(&ctx, |reg| reg.def_fn_wrapper = Some(saved)).map_err(|e| rq(&e))
}),
)?;
g.set(
"setWorldConstructor",
Func::from(|c: Constructor<'_>| -> rquickjs::Result<()> {
let ctx = c.ctx().clone();
let saved = Persistent::save(&ctx, c);
with_registry(&ctx, |reg| reg.world_ctor = Some(saved)).map_err(|e| rq(&e))
}),
)?;
g.set("setParallelCanAssign", Func::from(|_: Opt<Value<'_>>| {}))?;
g.set(
"defineTool",
Func::from(|args: Rest<Value<'_>>| register_tool_args(&args.0)),
)?;
Ok(())
}
#[derive(Debug, Clone)]
pub struct CollectedStep {
pub kind: String,
pub pattern: String,
pub is_regex: bool,
}
#[derive(Debug, Clone)]
pub struct CollectedHook {
pub hook_type: String,
pub tags: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CollectedParamType {
pub name: String,
pub regexp: String,
}
#[derive(Debug, Clone)]
pub struct CollectedRegistry {
pub default_timeout_ms: u64,
pub steps: Vec<CollectedStep>,
pub hooks: Vec<CollectedHook>,
pub param_types: Vec<CollectedParamType>,
}
pub async fn collect_registry(actx: &AsyncContext) -> Result<CollectedRegistry, ScriptError> {
async_with!(actx => |ctx| {
with_registry(&ctx, |reg| CollectedRegistry {
default_timeout_ms: reg.default_timeout_ms,
steps: reg
.steps
.iter()
.map(|s| CollectedStep {
kind: s.kind.as_str().to_string(),
pattern: s.pattern.clone(),
is_regex: s.is_regex,
})
.collect(),
hooks: reg
.hooks
.iter()
.map(|h| CollectedHook {
hook_type: h.kind.clone(),
tags: h.tags.clone(),
})
.collect(),
param_types: reg
.param_types
.iter()
.map(|p| CollectedParamType {
name: p.name.clone(),
regexp: p.regexp.clone(),
})
.collect(),
})
})
.await
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct CollectedAllow {
pub commands: std::collections::BTreeMap<String, crate::command_spec::CommandSpec>,
pub net: Vec<String>,
}
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CollectedTool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_schema: Option<serde_json::Value>,
pub allow: CollectedAllow,
pub expose_as_tool: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout_ms: Option<u64>,
}
pub fn tools_snapshot(ctx: &Ctx<'_>) -> Result<Vec<CollectedTool>, ScriptError> {
with_registry(ctx, |reg| {
reg
.tools
.iter()
.map(|t| CollectedTool {
name: t.name.clone(),
description: t.description.clone(),
input_schema: t.input_schema.clone(),
allow: CollectedAllow {
commands: t.allowed_commands.clone(),
net: t.allowed_net.clone(),
},
expose_as_tool: t.expose_as_tool,
timeout_ms: t.timeout_ms,
})
.collect()
})
}
pub fn tools_len(ctx: &Ctx<'_>) -> Result<usize, ScriptError> {
with_registry(ctx, |reg| reg.tools.len())
}
pub fn tool_names(ctx: &Ctx<'_>) -> Result<Vec<String>, ScriptError> {
with_registry(ctx, |reg| reg.tools.iter().map(|t| t.name.clone()).collect())
}
pub(crate) struct ToolDispatch<'js> {
pub handler: Function<'js>,
pub allowed_commands: std::collections::BTreeMap<String, crate::command_spec::CommandSpec>,
pub allowed_net: Vec<String>,
pub timeout_ms: Option<u64>,
}
pub(crate) fn tool_dispatch<'js>(ctx: &Ctx<'js>, idx: usize) -> Result<ToolDispatch<'js>, ScriptError> {
let (saved, allowed_commands, allowed_net, timeout_ms) = with_registry(ctx, |reg| {
reg
.tools
.get(idx)
.map(|t| {
(
t.handler.clone(),
t.allowed_commands.clone(),
t.allowed_net.clone(),
t.timeout_ms,
)
})
.ok_or_else(|| ScriptError::internal(format!("tool index {idx} out of range")))
})??;
let handler = saved.restore(ctx).map_err(|e| ScriptError::internal(e.to_string()))?;
Ok(ToolDispatch {
handler,
allowed_commands,
allowed_net,
timeout_ms,
})
}
#[derive(Clone, Default)]
pub struct ScenarioWorld {
pub page: Option<Arc<ferridriver::Page>>,
pub context: Option<Arc<ferridriver::context::ContextRef>>,
pub request: Option<Arc<ferridriver::http_client::HttpClient>>,
pub browser: Option<Arc<ferridriver::Browser>>,
pub parameters: Option<serde_json::Value>,
}
pub async fn set_scenario_world(actx: &AsyncContext, world: &ScenarioWorld) -> Result<(), ScriptError> {
let world = world.clone();
let route_ctx = actx.clone();
async_with!(actx => |ctx| {
let ctor = with_registry(&ctx, |reg| reg.world_ctor.clone())?;
let params_val: Value<'_> = match &world.parameters {
Some(v) if !v.is_null() => serde_to_js(&ctx, v).map_err(|e| ScriptError::internal(e.to_string()))?,
_ => Object::new(ctx.clone())
.map_err(|e| ScriptError::internal(e.to_string()))?
.into_value(),
};
let obj: Object<'_> = if let Some(ctor) = ctor {
let ctor = ctor.restore(&ctx).map_err(|e| ScriptError::internal(e.to_string()))?;
let opts = Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
opts
.set("parameters", params_val.clone())
.map_err(|e| ScriptError::internal(e.to_string()))?;
ctor
.construct::<_, Object<'_>>((opts,))
.map_err(|e| ScriptError::internal(format!("World constructor: {e}")))?
} else {
Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?
};
obj
.set("parameters", params_val)
.map_err(|e| ScriptError::internal(e.to_string()))?;
let attach = Function::new(ctx.clone(), |args: Rest<Value<'_>>| register_attachment(&args.0, false))
.map_err(|e| ScriptError::internal(e.to_string()))?;
let log = Function::new(ctx.clone(), |args: Rest<Value<'_>>| register_attachment(&args.0, true))
.map_err(|e| ScriptError::internal(e.to_string()))?;
obj.set("attach", attach).map_err(|e| ScriptError::internal(e.to_string()))?;
obj.set("log", log).map_err(|e| ScriptError::internal(e.to_string()))?;
let skip = Function::new(ctx.clone(), || -> rquickjs::Result<()> {
Err(rquickjs::Error::new_from_js_message("World", "Error", SKIP_SENTINEL.to_string()))
})
.map_err(|e| ScriptError::internal(e.to_string()))?;
obj.set("skip", skip).map_err(|e| ScriptError::internal(e.to_string()))?;
if let Some(page) = world.page {
install_page_on(&ctx, &obj, page, route_ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
}
if let Some(c) = world.context {
install_browser_context_on(&ctx, &obj, c).map_err(|e| ScriptError::internal(e.to_string()))?;
}
if let Some(r) = world.request {
install_request_on(&ctx, &obj, r).map_err(|e| ScriptError::internal(e.to_string()))?;
}
if let Some(b) = world.browser {
install_browser_on(&ctx, &obj, b).map_err(|e| ScriptError::internal(e.to_string()))?;
}
let saved = Persistent::save(&ctx, obj);
with_registry(&ctx, |reg| reg.current_world = Some(saved))
})
.await
}
pub async fn reset_world(actx: &AsyncContext) -> Result<(), ScriptError> {
async_with!(actx => |ctx| {
with_registry(&ctx, |reg| {
reg.current_world = None;
reg.attachments.clear();
})
})
.await
}
#[derive(Debug, Clone)]
pub enum JsArg {
Str(String),
Int(i64),
Float(f64),
Custom {
type_name: String,
raw: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepOutcome {
Passed,
Pending,
Skipped,
}
pub async fn invoke_step(
actx: &AsyncContext,
idx: usize,
params: &[JsArg],
data_table: Option<&[Vec<String>]>,
doc_string: Option<&str>,
source: &str,
) -> Result<StepOutcome, ScriptError> {
let params = params.to_vec();
let data_table = data_table.map(<[Vec<String>]>::to_vec);
let doc_string = doc_string.map(str::to_string);
let source = source.to_string();
async_with!(actx => |ctx| {
let (func, world, wrapper, timeout_ms) = with_registry(&ctx, |reg| {
let step = reg
.steps
.get(idx)
.ok_or_else(|| ScriptError::internal(format!("step index {idx} out of range")))?;
let t = step.timeout_ms.or(Some(reg.default_timeout_ms)).filter(|&v| v > 0);
Ok::<_, ScriptError>((step.func.clone(), reg.current_world.clone(), reg.def_fn_wrapper.clone(), t))
})??;
let mut func = func.restore(&ctx).map_err(|e| ScriptError::internal(e.to_string()))?;
if let Some(w) = wrapper {
let w = w.restore(&ctx).map_err(|e| ScriptError::internal(e.to_string()))?;
func = w
.call::<_, Function<'_>>((func.clone(),))
.catch(&ctx)
.map_err(|e| caught_to_script_error(e, &source))?;
}
let world_obj = match world {
Some(w) => w.restore(&ctx).map_err(|e| ScriptError::internal(e.to_string()))?,
None => Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?,
};
let n = 1 + params.len() + usize::from(data_table.is_some()) + usize::from(doc_string.is_some());
let mut args = Args::new(ctx.clone(), n);
args
.this(world_obj.clone())
.map_err(|e| ScriptError::internal(e.to_string()))?;
args
.push_arg(world_obj)
.map_err(|e| ScriptError::internal(e.to_string()))?;
for p in ¶ms {
match p {
JsArg::Str(s) => args.push_arg(s.as_str()).map_err(|e| ScriptError::internal(e.to_string()))?,
JsArg::Int(i) => args.push_arg(*i).map_err(|e| ScriptError::internal(e.to_string()))?,
JsArg::Float(f) => args.push_arg(*f).map_err(|e| ScriptError::internal(e.to_string()))?,
JsArg::Custom { type_name, raw } => {
let tx = with_registry(&ctx, |reg| {
reg
.param_types
.iter()
.find(|pt| &pt.name == type_name)
.and_then(|pt| pt.transformer.clone())
})?;
match tx {
Some(saved) => {
let f = saved.restore(&ctx).map_err(|e| ScriptError::internal(e.to_string()))?;
let call: rquickjs::Result<rquickjs::promise::MaybePromise<'_>> = f.call((raw.as_str(),));
let mp = call.catch(&ctx).map_err(|e| caught_to_script_error(e, &source))?;
let v: Value<'_> = mp
.into_future::<Value<'_>>()
.await
.catch(&ctx)
.map_err(|e| caught_to_script_error(e, &source))?;
args.push_arg(v).map_err(|e| ScriptError::internal(e.to_string()))?;
},
None => args
.push_arg(raw.as_str())
.map_err(|e| ScriptError::internal(e.to_string()))?,
}
},
}
}
if let Some(rows) = data_table {
let dt = Class::instance(ctx.clone(), DataTableJs { rows }).map_err(|e| ScriptError::internal(e.to_string()))?;
args.push_arg(dt).map_err(|e| ScriptError::internal(e.to_string()))?;
}
if let Some(s) = doc_string {
args.push_arg(s).map_err(|e| ScriptError::internal(e.to_string()))?;
}
let called: rquickjs::Result<rquickjs::promise::MaybePromise<'_>> = args.apply(&func);
let mp = match called.catch(&ctx) {
Ok(v) => v,
Err(e) => return Err(caught_to_script_error(e, &source)),
};
let fut = mp.into_future::<Value<'_>>();
let awaited = match timeout_ms {
Some(t) => match tokio::time::timeout(std::time::Duration::from_millis(t), fut).await {
Ok(r) => r,
Err(_) => return Err(ScriptError::timeout(t, t)),
},
None => fut.await,
};
let resolved: Value<'_> = match awaited.catch(&ctx) {
Ok(v) => v,
Err(e) => {
let se = caught_to_script_error(e, &source);
if se.message.contains(SKIP_SENTINEL) {
return Ok(StepOutcome::Skipped);
}
return Err(se);
},
};
let marker = resolved.as_string().and_then(|s| s.to_string().ok());
Ok(match marker.as_deref() {
Some("pending") => StepOutcome::Pending,
Some("skipped") => StepOutcome::Skipped,
_ => StepOutcome::Passed,
})
})
.await
}
pub async fn invoke_hook(
actx: &AsyncContext,
idx: usize,
arg: Option<&HookArg>,
source: &str,
) -> Result<StepOutcome, ScriptError> {
let source = source.to_string();
let arg = arg.cloned();
async_with!(actx => |ctx| {
let (func, world, timeout_ms) = with_registry(&ctx, |reg| {
let hook = reg
.hooks
.get(idx)
.ok_or_else(|| ScriptError::internal(format!("hook index {idx} out of range")))?;
let t = hook.timeout_ms.or(Some(reg.default_timeout_ms)).filter(|&v| v > 0);
Ok::<_, ScriptError>((hook.func.clone(), reg.current_world.clone(), t))
})??;
let func = func.restore(&ctx).map_err(|e| ScriptError::internal(e.to_string()))?;
let world_obj = match world {
Some(w) => w.restore(&ctx).map_err(|e| ScriptError::internal(e.to_string()))?,
None => Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?,
};
let n_args = 1 + usize::from(arg.is_some());
let mut args = Args::new(ctx.clone(), n_args);
args
.this(world_obj.clone())
.map_err(|e| ScriptError::internal(e.to_string()))?;
args
.push_arg(world_obj)
.map_err(|e| ScriptError::internal(e.to_string()))?;
if let Some(a) = arg {
let param = Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
let pickle = Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
pickle.set("name", a.name).map_err(|e| ScriptError::internal(e.to_string()))?;
let tags = rquickjs::Array::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
for (i, t) in a.tags.iter().enumerate() {
let to = Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
to.set("name", t.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
tags.set(i, to).map_err(|e| ScriptError::internal(e.to_string()))?;
}
pickle.set("tags", tags).map_err(|e| ScriptError::internal(e.to_string()))?;
let result = Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
result.set("status", a.status).map_err(|e| ScriptError::internal(e.to_string()))?;
if let Some(m) = a.message {
result.set("message", m).map_err(|e| ScriptError::internal(e.to_string()))?;
}
param.set("pickle", pickle).map_err(|e| ScriptError::internal(e.to_string()))?;
param.set("result", result).map_err(|e| ScriptError::internal(e.to_string()))?;
args.push_arg(param).map_err(|e| ScriptError::internal(e.to_string()))?;
}
let called: rquickjs::Result<rquickjs::promise::MaybePromise<'_>> = args.apply(&func);
let mp = match called.catch(&ctx) {
Ok(v) => v,
Err(e) => return Err(caught_to_script_error(e, &source)),
};
let fut = mp.into_future::<Value<'_>>();
let awaited = match timeout_ms {
Some(t) => match tokio::time::timeout(std::time::Duration::from_millis(t), fut).await {
Ok(r) => r,
Err(_) => return Err(ScriptError::timeout(t, t)),
},
None => fut.await,
};
if let Err(e) = awaited.catch(&ctx) {
return Err(caught_to_script_error(e, &source));
}
Ok(StepOutcome::Passed)
})
.await
}