Skip to content

Commit dbc23bb

Browse files
authored
Merge branch 'main' into dillon/workflow-rollback-types
2 parents b0f5322 + 1d41f31 commit dbc23bb

File tree

7 files changed

+470
-1
lines changed

7 files changed

+470
-1
lines changed

src/rust/jsg-macros/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,32 @@ impl DnsUtil {
7676
```
7777

7878
On struct definitions, generates `jsg::Type`, wrapper struct, and `ResourceTemplate` implementations. On impl blocks, scans for `#[jsg_method]` attributes and generates the `Resource` trait implementation.
79+
80+
## `#[jsg_oneof]`
81+
82+
Generates `jsg::Type` and `jsg::FromJS` implementations for union types. Use this to accept parameters that can be one of several JavaScript types.
83+
84+
Each enum variant should be a single-field tuple variant where the field type implements `jsg::Type` and `jsg::FromJS` (e.g., `String`, `f64`, `bool`).
85+
86+
```rust
87+
use jsg_macros::jsg_oneof;
88+
89+
#[jsg_oneof]
90+
#[derive(Debug, Clone)]
91+
enum StringOrNumber {
92+
String(String),
93+
Number(f64),
94+
}
95+
96+
impl MyResource {
97+
#[jsg_method]
98+
pub fn process(&self, value: StringOrNumber) -> Result<String, jsg::Error> {
99+
match value {
100+
StringOrNumber::String(s) => Ok(format!("string: {}", s)),
101+
StringOrNumber::Number(n) => Ok(format!("number: {}", n)),
102+
}
103+
}
104+
}
105+
```
106+
107+
The macro generates type-checking code that matches JavaScript values to enum variants without coercion. If no variant matches, a `TypeError` is thrown listing all expected types.

src/rust/jsg-macros/lib.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,121 @@ fn is_result_type(ty: &syn::Type) -> bool {
346346
}
347347
false
348348
}
349+
350+
/// Generates `jsg::Type` and `jsg::FromJS` implementations for union types.
351+
///
352+
/// This macro automatically implements the traits needed for enums with
353+
/// single-field tuple variants to be used directly as `jsg_method` parameters.
354+
/// Each variant should contain a type that implements `jsg::Type` and `jsg::FromJS`.
355+
///
356+
/// # Example
357+
///
358+
/// ```ignore
359+
/// use jsg_macros::jsg_oneof;
360+
///
361+
/// #[jsg_oneof]
362+
/// #[derive(Debug, Clone)]
363+
/// enum StringOrNumber {
364+
/// String(String),
365+
/// Number(f64),
366+
/// }
367+
///
368+
/// // Use directly as a parameter type:
369+
/// #[jsg_method]
370+
/// fn process(&self, value: StringOrNumber) -> Result<String, jsg::Error> {
371+
/// match value {
372+
/// StringOrNumber::String(s) => Ok(format!("string: {}", s)),
373+
/// StringOrNumber::Number(n) => Ok(format!("number: {}", n)),
374+
/// }
375+
/// }
376+
/// ```
377+
#[proc_macro_attribute]
378+
pub fn jsg_oneof(_attr: TokenStream, item: TokenStream) -> TokenStream {
379+
let input = parse_macro_input!(item as DeriveInput);
380+
let name = &input.ident;
381+
382+
let Data::Enum(data) = &input.data else {
383+
return error(&input, "#[jsg_oneof] can only be applied to enums");
384+
};
385+
386+
let mut variants = Vec::new();
387+
for variant in &data.variants {
388+
let variant_name = &variant.ident;
389+
let Fields::Unnamed(fields) = &variant.fields else {
390+
return error(
391+
variant,
392+
"#[jsg_oneof] variants must be tuple variants (e.g., `Variant(Type)`)",
393+
);
394+
};
395+
if fields.unnamed.len() != 1 {
396+
return error(variant, "#[jsg_oneof] variants must have exactly one field");
397+
}
398+
let inner_type = &fields.unnamed[0].ty;
399+
variants.push((variant_name, inner_type));
400+
}
401+
402+
if variants.is_empty() {
403+
return error(&input, "#[jsg_oneof] requires at least one variant");
404+
}
405+
406+
let type_checks: Vec<_> = variants
407+
.iter()
408+
.map(|(variant_name, inner_type)| {
409+
quote! {
410+
if let Some(result) = <#inner_type as jsg::FromJS>::try_from_js_exact(lock, &value) {
411+
return result.map(Self::#variant_name);
412+
}
413+
}
414+
})
415+
.collect();
416+
417+
let type_names: Vec<_> = variants
418+
.iter()
419+
.map(|(_, inner_type)| {
420+
quote! { <#inner_type as jsg::Type>::class_name() }
421+
})
422+
.collect();
423+
424+
let is_exact_checks: Vec<_> = variants
425+
.iter()
426+
.map(|(_, inner_type)| {
427+
quote! { <#inner_type as jsg::Type>::is_exact(value) }
428+
})
429+
.collect();
430+
431+
let error_msg = quote! {
432+
let expected: Vec<&str> = vec![#(#type_names),*];
433+
let msg = format!(
434+
"Expected one of [{}] but got {}",
435+
expected.join(", "),
436+
value.type_of()
437+
);
438+
Err(jsg::Error::new_type_error(msg))
439+
};
440+
441+
quote! {
442+
#input
443+
444+
#[automatically_derived]
445+
impl jsg::Type for #name {
446+
fn class_name() -> &'static str {
447+
stringify!(#name)
448+
}
449+
450+
fn is_exact(value: &jsg::v8::Local<jsg::v8::Value>) -> bool {
451+
#(#is_exact_checks)||*
452+
}
453+
}
454+
455+
#[automatically_derived]
456+
impl jsg::FromJS for #name {
457+
type ResultType = Self;
458+
459+
fn from_js(lock: &mut jsg::Lock, value: jsg::v8::Local<jsg::v8::Value>) -> Result<Self::ResultType, jsg::Error> {
460+
#(#type_checks)*
461+
#error_msg
462+
}
463+
}
464+
}
465+
.into()
466+
}

0 commit comments

Comments
 (0)