Skip to content

Commit 84bfb97

Browse files
authored
Merge pull request #269 from obeli-sk/feat/activity-js-custom-params
feat(activity-js): Support custom parameters in JS activities
2 parents 50564bf + 95a6df3 commit 84bfb97

10 files changed

Lines changed: 584 additions & 45 deletions

File tree

crates/activity-js-runtime/src/activity_js_runtime.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ use std::rc::Rc;
2121

2222
/// Execute JavaScript code with the given parameters.
2323
///
24-
/// `params_json` is expected to be a JSON array string. The array is passed
25-
/// as the first and only argument to `fn_name`.
24+
/// `params_json` is a list of JSON-serialized parameter values.
25+
/// Each element is passed as a positional argument to the JS function `fn_name`.
2626
pub fn execute(
2727
fn_name: &str,
2828
js_code: &str,
29-
params_json: &str,
29+
params_json: &[String],
3030
) -> Result<Result<String, String>, JsRuntimeError> {
3131
// `fn_name` comes from trusted `activity_js_worker`, must be FFQN's fn name
3232
let fn_name = fn_name.replace('-', "_");
@@ -44,12 +44,16 @@ pub fn execute(
4444
setup_fetch(&mut context).expect("fetch setup must work");
4545

4646
// `params_json` is sent by trusted `activity_js_worker`, params were typechecked.
47-
// Store as global `__params__` array.
48-
// Direct interpolation, JSON array/object literals are valid JavaScript syntax.
49-
let params_code = format!("const __params__ = {params_json};");
47+
// Parse each JSON param and store as `__params__` array.
48+
let mut params_js_parts = Vec::with_capacity(params_json.len());
49+
for param in params_json {
50+
// Each param is a JSON value — valid JavaScript literal syntax.
51+
params_js_parts.push(param.as_str());
52+
}
53+
let params_array = format!("const __params__ = [{}];", params_js_parts.join(", "));
5054
context
51-
.eval(Source::from_bytes(&params_code))
52-
.expect("already verified that params_json is parseable");
55+
.eval(Source::from_bytes(&params_array))
56+
.expect("already verified that params_json elements are parseable");
5357

5458
// Add the function to the context, without running it.
5559
let bare_fn_eval = context.eval(Source::from_bytes(js_code));
@@ -69,7 +73,11 @@ pub fn execute(
6973
return Err(JsRuntimeError::FunctionNotFound);
7074
}
7175

72-
let call_fn = format!("{fn_name}(__params__);");
76+
// Spread params as positional arguments: fn_name(__params__[0], __params__[1], ...)
77+
let spread_args: Vec<String> = (0..params_json.len())
78+
.map(|i| format!("__params__[{i}]"))
79+
.collect();
80+
let call_fn = format!("{fn_name}({});", spread_args.join(", "));
7381

7482
let result = context.eval(Source::from_bytes(&call_fn));
7583

crates/activity-js-runtime/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ impl Guest for Component {
1717
fn run(
1818
fn_name: String,
1919
js_code: String,
20-
params_json: String,
20+
params_json: Vec<String>,
2121
) -> Result<Result<String, String>, JsRuntimeError> {
2222
activity_js_runtime::execute(&fn_name, &js_code, &params_json)
2323
}

crates/activity-js-runtime/wit/obelisk_js-runtime/execute.wit

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ interface execute {
1717
/// Execute JavaScript code as an activity.
1818
///
1919
/// The JS code should define a function named as declared in `fn-name` that receives
20-
/// the params as positional arguments (spread from the JSON array)
21-
/// and returns a JSON-serializable value.
20+
/// the params as positional arguments (each element is one JSON-serialized parameter value)
21+
/// and returns a string.
2222
///
23-
/// Returns Ok(json_result) on success, Err(error_message) on failure.
24-
run: func(fn-name: string, js-code: string, params-json: string) -> result<result<string, string>, js-runtime-error>;
23+
/// Returns Ok(Ok(string)) on success, Ok(Err(string)) on thrown string/Error,
24+
/// or Err(js-runtime-error) on type/declaration errors.
25+
run: func(fn-name: string, js-code: string, params-json: list<string>) -> result<result<string, string>, js-runtime-error>;
2526
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
async function fetch_get(params) {
2-
console.info("Fetching " + params[0]);
3-
const resp = await fetch(params[0]);
1+
async function fetch_get(url) {
2+
console.info("Fetching " + url);
3+
const resp = await fetch(url);
44
const text = await resp.text();
55
return text;
66
}

crates/val-json/src/type_wrapper.rs

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,3 +452,233 @@ mod tests {
452452
);
453453
}
454454
}
455+
456+
/// Parse a WIT type syntax string into a `TypeWrapper`.
457+
///
458+
/// Supports primitives (`bool`, `u8`..`u64`, `s8`..`s64`, `f32`, `f64`, `char`, `string`),
459+
/// `list<T>`, `option<T>`, `tuple<T1, T2, ...>`, and `result` variants.
460+
pub fn parse_wit_type(s: &str) -> Result<TypeWrapper, String> {
461+
let s = s.trim();
462+
let (ty, rest) = parse_type(s)?;
463+
let rest = rest.trim();
464+
if !rest.is_empty() {
465+
return Err(format!("unexpected trailing characters: '{rest}'"));
466+
}
467+
Ok(ty)
468+
}
469+
470+
fn parse_type(s: &str) -> Result<(TypeWrapper, &str), String> {
471+
let s = s.trim();
472+
if s.is_empty() {
473+
return Err("unexpected end of input".to_string());
474+
}
475+
476+
let ident_end = s
477+
.find(|c: char| !c.is_ascii_alphanumeric() && c != '-' && c != '_')
478+
.unwrap_or(s.len());
479+
let ident = &s[..ident_end];
480+
let rest = &s[ident_end..];
481+
482+
match ident {
483+
"bool" => Ok((TypeWrapper::Bool, rest)),
484+
"s8" => Ok((TypeWrapper::S8, rest)),
485+
"u8" => Ok((TypeWrapper::U8, rest)),
486+
"s16" => Ok((TypeWrapper::S16, rest)),
487+
"u16" => Ok((TypeWrapper::U16, rest)),
488+
"s32" => Ok((TypeWrapper::S32, rest)),
489+
"u32" => Ok((TypeWrapper::U32, rest)),
490+
"s64" => Ok((TypeWrapper::S64, rest)),
491+
"u64" => Ok((TypeWrapper::U64, rest)),
492+
"f32" => Ok((TypeWrapper::F32, rest)),
493+
"f64" => Ok((TypeWrapper::F64, rest)),
494+
"char" => Ok((TypeWrapper::Char, rest)),
495+
"string" => Ok((TypeWrapper::String, rest)),
496+
497+
"list" => {
498+
let rest = expect_char(rest.trim_start(), '<')?;
499+
let (inner, rest) = parse_type(rest)?;
500+
let rest = expect_char(rest.trim_start(), '>')?;
501+
Ok((TypeWrapper::List(Box::new(inner)), rest))
502+
}
503+
504+
"option" => {
505+
let rest = expect_char(rest.trim_start(), '<')?;
506+
let (inner, rest) = parse_type(rest)?;
507+
let rest = expect_char(rest.trim_start(), '>')?;
508+
Ok((TypeWrapper::Option(Box::new(inner)), rest))
509+
}
510+
511+
"tuple" => {
512+
let rest = expect_char(rest.trim_start(), '<')?;
513+
let (items, rest) = parse_comma_separated_types(rest, '>')?;
514+
Ok((TypeWrapper::Tuple(items.into_boxed_slice()), rest))
515+
}
516+
517+
"result" => {
518+
let rest_trimmed = rest.trim_start();
519+
if !rest_trimmed.starts_with('<') {
520+
return Ok((
521+
TypeWrapper::Result {
522+
ok: None,
523+
err: None,
524+
},
525+
rest,
526+
));
527+
}
528+
let rest = expect_char(rest_trimmed, '<')?;
529+
let rest_trimmed = rest.trim_start();
530+
531+
let (ok, rest) = if let Some(stripped) = rest_trimmed.strip_prefix('_') {
532+
(None, stripped)
533+
} else {
534+
let (ty, rest) = parse_type(rest)?;
535+
(Some(Box::new(ty)), rest)
536+
};
537+
538+
let rest_trimmed = rest.trim_start();
539+
540+
if let Some(rest) = rest_trimmed.strip_prefix(',') {
541+
let (err_ty, rest) = parse_type(rest)?;
542+
let rest = expect_char(rest.trim_start(), '>')?;
543+
Ok((
544+
TypeWrapper::Result {
545+
ok,
546+
err: Some(Box::new(err_ty)),
547+
},
548+
rest,
549+
))
550+
} else {
551+
let rest = expect_char(rest_trimmed, '>')?;
552+
Ok((TypeWrapper::Result { ok, err: None }, rest))
553+
}
554+
}
555+
556+
_ => Err(format!("unknown type: '{ident}'")),
557+
}
558+
}
559+
560+
fn expect_char(s: &str, expected: char) -> Result<&str, String> {
561+
let s = s.trim_start();
562+
if s.starts_with(expected) {
563+
Ok(&s[expected.len_utf8()..])
564+
} else {
565+
let found = s
566+
.chars()
567+
.next()
568+
.map_or("end of input".to_string(), |c| format!("'{c}'"));
569+
Err(format!("expected '{expected}', found {found}"))
570+
}
571+
}
572+
573+
fn parse_comma_separated_types(s: &str, closing: char) -> Result<(Vec<TypeWrapper>, &str), String> {
574+
let mut items = Vec::new();
575+
let mut rest = s.trim_start();
576+
577+
if rest.starts_with(closing) {
578+
return Ok((items, &rest[closing.len_utf8()..]));
579+
}
580+
581+
loop {
582+
let (ty, r) = parse_type(rest)?;
583+
items.push(ty);
584+
rest = r.trim_start();
585+
586+
if rest.starts_with(',') {
587+
rest = rest[1..].trim_start();
588+
} else if rest.starts_with(closing) {
589+
rest = &rest[closing.len_utf8()..];
590+
break;
591+
} else {
592+
let found = rest
593+
.chars()
594+
.next()
595+
.map_or("end of input".to_string(), |c| format!("'{c}'"));
596+
return Err(format!("expected ',' or '{closing}', found {found}"));
597+
}
598+
}
599+
600+
Ok((items, rest))
601+
}
602+
603+
#[cfg(test)]
604+
mod tests_parse_wit_type {
605+
use super::*;
606+
607+
#[test]
608+
fn primitives() {
609+
assert_eq!(parse_wit_type("bool").unwrap(), TypeWrapper::Bool);
610+
assert_eq!(parse_wit_type("u32").unwrap(), TypeWrapper::U32);
611+
assert_eq!(parse_wit_type("string").unwrap(), TypeWrapper::String);
612+
assert_eq!(parse_wit_type("f64").unwrap(), TypeWrapper::F64);
613+
}
614+
615+
#[test]
616+
fn list() {
617+
assert_eq!(
618+
parse_wit_type("list<string>").unwrap(),
619+
TypeWrapper::List(Box::new(TypeWrapper::String))
620+
);
621+
assert_eq!(
622+
parse_wit_type("list<list<u8>>").unwrap(),
623+
TypeWrapper::List(Box::new(TypeWrapper::List(Box::new(TypeWrapper::U8))))
624+
);
625+
}
626+
627+
#[test]
628+
fn option() {
629+
assert_eq!(
630+
parse_wit_type("option<u32>").unwrap(),
631+
TypeWrapper::Option(Box::new(TypeWrapper::U32))
632+
);
633+
}
634+
635+
#[test]
636+
fn tuple() {
637+
assert_eq!(
638+
parse_wit_type("tuple<u32, string>").unwrap(),
639+
TypeWrapper::Tuple(vec![TypeWrapper::U32, TypeWrapper::String].into_boxed_slice())
640+
);
641+
}
642+
643+
#[test]
644+
fn result_variants() {
645+
assert_eq!(
646+
parse_wit_type("result<string, string>").unwrap(),
647+
TypeWrapper::Result {
648+
ok: Some(Box::new(TypeWrapper::String)),
649+
err: Some(Box::new(TypeWrapper::String)),
650+
}
651+
);
652+
assert_eq!(
653+
parse_wit_type("result<string>").unwrap(),
654+
TypeWrapper::Result {
655+
ok: Some(Box::new(TypeWrapper::String)),
656+
err: None,
657+
}
658+
);
659+
assert_eq!(
660+
parse_wit_type("result").unwrap(),
661+
TypeWrapper::Result {
662+
ok: None,
663+
err: None,
664+
}
665+
);
666+
assert_eq!(
667+
parse_wit_type("result<_, string>").unwrap(),
668+
TypeWrapper::Result {
669+
ok: None,
670+
err: Some(Box::new(TypeWrapper::String)),
671+
}
672+
);
673+
}
674+
675+
#[test]
676+
fn trailing_chars() {
677+
assert!(parse_wit_type("u32 extra").is_err());
678+
}
679+
680+
#[test]
681+
fn unknown_type() {
682+
assert!(parse_wit_type("foobar").is_err());
683+
}
684+
}

0 commit comments

Comments
 (0)