mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 00:00:56 +00:00
2215 lines
80 KiB
Rust
2215 lines
80 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use quote::{format_ident, quote};
|
|
use syn::spanned::Spanned;
|
|
use syn::{Data, DeriveInput, Error, Field, Fields, GenericParam, Ident, Lit, LitStr, Type};
|
|
|
|
use crate::attrs::{
|
|
ConstructorAttr, ConstructorDefaultAttr, FieldAttrs, FieldDefaults, FieldKind, ObjectAttrs,
|
|
ObjectRole, RenameRule, ValueInitAttr, parse_field_attrs_with_defaults,
|
|
parse_function_template_attrs, parse_object_attrs,
|
|
};
|
|
|
|
struct DeclaredField {
|
|
field: Field,
|
|
attrs: FieldAttrs,
|
|
}
|
|
|
|
fn declared_fields(
|
|
data: &Data,
|
|
rename_all: RenameRule,
|
|
defaults: FieldDefaults<'_>,
|
|
) -> Result<Vec<DeclaredField>, Error> {
|
|
named_fields(data)?
|
|
.into_iter()
|
|
.map(|field| {
|
|
let mut attrs = parse_field_attrs_with_defaults(&field, defaults)?;
|
|
if attrs.name.is_none()
|
|
&& attrs.symbol.is_none()
|
|
&& !matches!(
|
|
attrs.kind,
|
|
FieldKind::Input | FieldKind::Prototype | FieldKind::ToStringTag
|
|
)
|
|
{
|
|
let rename = if matches!(attrs.kind, FieldKind::Slot | FieldKind::Hidden) {
|
|
RenameRule::None
|
|
} else {
|
|
rename_all
|
|
};
|
|
let name = implicit_field_name(&field, rename)?;
|
|
attrs.name = Some(syn::parse_quote!(#name));
|
|
}
|
|
Ok(DeclaredField { field, attrs })
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
struct WebApiFieldKey {
|
|
display_name: proc_macro2::TokenStream,
|
|
display_name_literal: Option<LitStr>,
|
|
property_key: proc_macro2::TokenStream,
|
|
function_name: Option<proc_macro2::TokenStream>,
|
|
shared_template_method_name: Option<proc_macro2::TokenStream>,
|
|
}
|
|
|
|
pub(crate) fn expand_webapi_function_template(
|
|
input: DeriveInput,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
let struct_name = input.ident;
|
|
let generics = input.generics;
|
|
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
|
let attrs = parse_function_template_attrs(&input.attrs)?;
|
|
let template_name = if let Some(name) = &attrs.name {
|
|
quote!(#name)
|
|
} else if let Some(interface) = &attrs.interface {
|
|
quote!(<#interface>::DESCRIPTOR.name())
|
|
} else {
|
|
let name = LitStr::new(&struct_name.to_string(), struct_name.span());
|
|
quote!(#name)
|
|
};
|
|
let constructor_callback = match attrs
|
|
.constructor
|
|
.clone()
|
|
.unwrap_or(ConstructorAttr::Illegal)
|
|
{
|
|
ConstructorAttr::Illegal => {
|
|
quote!(::moli_webapi_declare::illegal_constructor_callback)
|
|
}
|
|
ConstructorAttr::Callback(callback) => {
|
|
let Some(interface) = &attrs.interface else {
|
|
return Err(Error::new(
|
|
struct_name.span(),
|
|
"constructor_callback requires an interface descriptor",
|
|
));
|
|
};
|
|
quote!(::moli_webapi_declare::web_api_constructor!(#interface, #callback))
|
|
}
|
|
};
|
|
let constructor_length = attrs.constructor_length.unwrap_or(0);
|
|
let initialize_intrinsic_prototype_parent =
|
|
attrs.intrinsic_prototype_parent.as_ref().map(|intrinsic| {
|
|
quote! {
|
|
let intrinsic_parent =
|
|
::moli_webapi_declare::v8::FunctionTemplate::builder(
|
|
::moli_webapi_declare::illegal_constructor_callback,
|
|
)
|
|
.constructor_behavior(
|
|
::moli_webapi_declare::v8::ConstructorBehavior::Throw,
|
|
)
|
|
.build(scope);
|
|
intrinsic_parent.remove_prototype();
|
|
intrinsic_parent.set_intrinsic_data_property(
|
|
::moli_webapi_declare::__private::v8str(scope, "prototype").into(),
|
|
#intrinsic,
|
|
::moli_webapi_declare::v8::PropertyAttribute::NONE,
|
|
);
|
|
template.inherit(intrinsic_parent);
|
|
}
|
|
});
|
|
let initialize_readonly_prototype = attrs.readonly_prototype.then(|| {
|
|
quote! {
|
|
template.read_only_prototype();
|
|
}
|
|
});
|
|
let initialize_prototype_to_string_tag = attrs.prototype_to_string_tag.as_ref().map(|tag| {
|
|
quote! {
|
|
prototype.set_with_attr(
|
|
::moli_webapi_declare::v8::Symbol::get_to_string_tag(scope).into(),
|
|
::moli_webapi_declare::__private::v8str(scope, #tag).into(),
|
|
::moli_webapi_declare::v8::PropertyAttribute::READ_ONLY
|
|
| ::moli_webapi_declare::v8::PropertyAttribute::DONT_ENUM,
|
|
);
|
|
}
|
|
});
|
|
let fields = declared_fields(
|
|
&input.data,
|
|
attrs.rename_all,
|
|
FieldDefaults {
|
|
receiver: attrs.receiver.as_ref(),
|
|
enumerable: attrs.default_enumerable,
|
|
..FieldDefaults::default()
|
|
},
|
|
)?;
|
|
let declaration_field_reads = fields.iter().filter_map(|DeclaredField { field, .. }| {
|
|
field.ident.as_ref().map(|ident| {
|
|
quote! {
|
|
let _ = &self.#ident;
|
|
}
|
|
})
|
|
});
|
|
let template_fields = expand_function_template_fields(&fields, &template_name)?;
|
|
let FunctionTemplateFieldExpansions {
|
|
template_methods,
|
|
prototype_methods,
|
|
} = template_fields;
|
|
|
|
Ok(quote! {
|
|
impl #impl_generics #struct_name #ty_generics #where_clause {
|
|
fn __webapi_mark_declaration_fields_used(&self) {
|
|
#(#declaration_field_reads)*
|
|
}
|
|
|
|
pub fn build<'s>(
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<'s, '_, ()>,
|
|
) -> ::moli_webapi_declare::v8::Local<'s, ::moli_webapi_declare::v8::FunctionTemplate> {
|
|
let _ = Self::__webapi_mark_declaration_fields_used;
|
|
let template = ::moli_webapi_declare::v8::FunctionTemplate::builder(#constructor_callback)
|
|
.length(#constructor_length)
|
|
.build(scope);
|
|
template.set_class_name(::moli_webapi_declare::__private::v8str(scope, #template_name));
|
|
Self::initialize_template(scope, template);
|
|
let prototype = template.prototype_template(scope);
|
|
Self::initialize_prototype_template(scope, prototype);
|
|
template
|
|
}
|
|
|
|
pub fn initialize_template<'s>(
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<'s, '_, ()>,
|
|
template: ::moli_webapi_declare::v8::Local<'s, ::moli_webapi_declare::v8::FunctionTemplate>,
|
|
) {
|
|
#initialize_intrinsic_prototype_parent
|
|
#initialize_readonly_prototype
|
|
#(#template_methods)*
|
|
}
|
|
|
|
pub fn initialize_prototype_template<'s>(
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<'s, '_, ()>,
|
|
prototype: ::moli_webapi_declare::v8::Local<'s, ::moli_webapi_declare::v8::ObjectTemplate>,
|
|
) {
|
|
#initialize_prototype_to_string_tag
|
|
#(#prototype_methods)*
|
|
}
|
|
}
|
|
|
|
impl #impl_generics ::moli_webapi_declare::WebApiFunctionTemplateDeclaration for #struct_name #ty_generics #where_clause {
|
|
const NAME: &'static str = #template_name;
|
|
|
|
fn build<'s>(
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<'s, '_, ()>,
|
|
) -> ::moli_webapi_declare::v8::Local<'s, ::moli_webapi_declare::v8::FunctionTemplate> {
|
|
<#struct_name #ty_generics>::build(scope)
|
|
}
|
|
|
|
fn initialize_template<'s>(
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<'s, '_, ()>,
|
|
template: ::moli_webapi_declare::v8::Local<'s, ::moli_webapi_declare::v8::FunctionTemplate>,
|
|
) {
|
|
<#struct_name #ty_generics>::initialize_template(scope, template)
|
|
}
|
|
|
|
fn initialize_prototype_template<'s>(
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<'s, '_, ()>,
|
|
prototype: ::moli_webapi_declare::v8::Local<'s, ::moli_webapi_declare::v8::ObjectTemplate>,
|
|
) {
|
|
<#struct_name #ty_generics>::initialize_prototype_template(scope, prototype)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub(crate) fn expand_webapi_object(input: DeriveInput) -> Result<proc_macro2::TokenStream, Error> {
|
|
let struct_name = input.ident;
|
|
let generics = input.generics;
|
|
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
|
let attrs = parse_object_attrs(&input.attrs)?;
|
|
let role = attrs.role.as_ref().ok_or_else(|| Error::new(
|
|
struct_name.span(), "choose an object role with #[webapi(interface = Type)], #[webapi(plain)], or #[webapi(fragment)]",
|
|
))?;
|
|
let prototype = attrs
|
|
.prototype
|
|
.as_ref()
|
|
.map(|prototype| quote!(#prototype))
|
|
.or_else(|| match role {
|
|
ObjectRole::Instance(interface) => Some(quote!(<#interface>::DESCRIPTOR.name())),
|
|
ObjectRole::Plain => Some(quote!("Object")),
|
|
ObjectRole::Fragment => None,
|
|
});
|
|
let own_to_string_tag = match attrs.own_to_string_tag.as_ref() {
|
|
Some(tag) => quote!(::std::option::Option::Some(#tag)),
|
|
None => quote!(::std::option::Option::None),
|
|
};
|
|
let fallback_to_string_tag = match attrs.fallback_to_string_tag.as_ref() {
|
|
Some(tag) => quote!(::std::option::Option::Some(#tag)),
|
|
None => quote!(::std::option::Option::None),
|
|
};
|
|
let define_own_to_string_tag = if attrs.readonly_to_string_tag {
|
|
quote! {
|
|
if let ::std::option::Option::Some(tag) = #own_to_string_tag {
|
|
::moli_webapi_declare::define_to_string_tag_with_attributes(
|
|
scope,
|
|
object,
|
|
tag,
|
|
::moli_webapi_declare::v8::PropertyAttribute::DONT_ENUM
|
|
| ::moli_webapi_declare::v8::PropertyAttribute::READ_ONLY,
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
quote! {
|
|
if let ::std::option::Option::Some(tag) = #own_to_string_tag {
|
|
::moli_webapi_declare::define_to_string_tag(scope, object, tag);
|
|
}
|
|
}
|
|
};
|
|
let set_prototype = match (prototype, attrs.require_prototype) {
|
|
(Some(prototype), true) => quote! {
|
|
::moli_webapi_declare::set_required_interface_prototype(scope, object, #prototype)?;
|
|
true
|
|
},
|
|
(Some(prototype), false) => quote!(
|
|
::moli_webapi_declare::set_interface_prototype(scope, object, #prototype)
|
|
),
|
|
(None, true) => {
|
|
return Err(Error::new(
|
|
struct_name.span(),
|
|
"require_prototype on a fragment requires an explicit prototype",
|
|
));
|
|
}
|
|
(None, false) => quote!(false),
|
|
};
|
|
let define_fallback_to_string_tag = quote! {
|
|
if !prototype_bound {
|
|
if let ::std::option::Option::Some(tag) = #fallback_to_string_tag {
|
|
::moli_webapi_declare::define_to_string_tag(scope, object, tag);
|
|
}
|
|
}
|
|
};
|
|
let fields = declared_fields(
|
|
&input.data,
|
|
attrs.rename_all,
|
|
FieldDefaults {
|
|
receiver: attrs.receiver.as_ref(),
|
|
data_properties: attrs.default_data_properties,
|
|
enumerable: attrs.default_enumerable,
|
|
},
|
|
)?;
|
|
let generated_constructor = expand_object_generated_constructor(&fields, &attrs, &struct_name)?;
|
|
let inferred_scope_lifetime = attrs
|
|
.scope_lifetime
|
|
.clone()
|
|
.or_else(|| single_lifetime_param(&generics));
|
|
let scope_lifetime = inferred_scope_lifetime.as_ref();
|
|
if let Some(lifetime) = scope_lifetime
|
|
&& !generics.params.iter().any(
|
|
|param| matches!(param, GenericParam::Lifetime(param) if param.lifetime == *lifetime),
|
|
)
|
|
{
|
|
return Err(Error::new(
|
|
lifetime.span(),
|
|
"#[webapi(scope_lifetime = ...)] must name a lifetime parameter on the struct",
|
|
));
|
|
}
|
|
let method_scope_lifetime = scope_lifetime
|
|
.map(|lifetime| quote!(#lifetime))
|
|
.unwrap_or_else(|| quote!('__webapi_scope));
|
|
let method_scope_generic = scope_lifetime.is_none().then(|| quote!(<'__webapi_scope>));
|
|
let fields = fields
|
|
.iter()
|
|
.filter_map(expand_object_field)
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let initialize_brand = role.interface().map(|interface| {
|
|
quote! {
|
|
<#interface>::DESCRIPTOR.initialize(scope, object)?;
|
|
}
|
|
});
|
|
let initialize_body = quote! {
|
|
#(#fields)*
|
|
#initialize_brand
|
|
::std::result::Result::Ok(())
|
|
};
|
|
let mut trait_generics = generics.clone();
|
|
if scope_lifetime.is_none() {
|
|
trait_generics
|
|
.params
|
|
.insert(0, syn::parse_quote!('__webapi_scope));
|
|
}
|
|
let (trait_impl_generics, _, trait_where_clause) = trait_generics.split_for_impl();
|
|
let trait_impl = quote! {
|
|
impl #trait_impl_generics ::moli_webapi_declare::WebApiObjectDeclaration<#method_scope_lifetime> for #struct_name #ty_generics #trait_where_clause {
|
|
fn initialize(
|
|
&self,
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<#method_scope_lifetime, '_>,
|
|
object: ::moli_webapi_declare::v8::Local<#method_scope_lifetime, ::moli_webapi_declare::v8::Object>,
|
|
) -> ::std::result::Result<(), ::moli_webapi_declare::BindError> {
|
|
self.initialize(scope, object)
|
|
}
|
|
|
|
fn bind_into(
|
|
&self,
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<#method_scope_lifetime, '_>,
|
|
object: ::moli_webapi_declare::v8::Local<#method_scope_lifetime, ::moli_webapi_declare::v8::Object>,
|
|
) -> ::std::result::Result<(), ::moli_webapi_declare::BindError> {
|
|
self.bind_into(scope, object)
|
|
}
|
|
|
|
fn bind(
|
|
&self,
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<#method_scope_lifetime, '_>,
|
|
) -> ::std::result::Result<
|
|
::moli_webapi_declare::v8::Local<#method_scope_lifetime, ::moli_webapi_declare::v8::Object>,
|
|
::moli_webapi_declare::BindError,
|
|
> {
|
|
self.bind(scope)
|
|
}
|
|
}
|
|
|
|
impl #trait_impl_generics ::moli_webapi_declare::WebApiValue<#method_scope_lifetime> for #struct_name #ty_generics #trait_where_clause {
|
|
fn to_v8_value(
|
|
&self,
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<#method_scope_lifetime, '_>,
|
|
) -> ::std::option::Option<::moli_webapi_declare::v8::Local<#method_scope_lifetime, ::moli_webapi_declare::v8::Value>> {
|
|
self.bind(scope).ok().map(::std::convert::Into::into)
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(quote! {
|
|
impl #impl_generics #struct_name #ty_generics #where_clause {
|
|
#generated_constructor
|
|
|
|
pub fn initialize #method_scope_generic(
|
|
&self,
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<#method_scope_lifetime, '_>,
|
|
object: ::moli_webapi_declare::v8::Local<#method_scope_lifetime, ::moli_webapi_declare::v8::Object>,
|
|
) -> ::std::result::Result<(), ::moli_webapi_declare::BindError> {
|
|
#initialize_body
|
|
}
|
|
|
|
pub fn bind_into #method_scope_generic(
|
|
&self,
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<#method_scope_lifetime, '_>,
|
|
object: ::moli_webapi_declare::v8::Local<#method_scope_lifetime, ::moli_webapi_declare::v8::Object>,
|
|
) -> ::std::result::Result<(), ::moli_webapi_declare::BindError> {
|
|
let prototype_bound = { #set_prototype };
|
|
#define_own_to_string_tag
|
|
#define_fallback_to_string_tag
|
|
self.initialize(scope, object)
|
|
}
|
|
|
|
pub fn bind #method_scope_generic(
|
|
&self,
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<#method_scope_lifetime, '_>,
|
|
) -> ::std::result::Result<
|
|
::moli_webapi_declare::v8::Local<#method_scope_lifetime, ::moli_webapi_declare::v8::Object>,
|
|
::moli_webapi_declare::BindError,
|
|
> {
|
|
let object = ::moli_webapi_declare::v8::Object::new(scope);
|
|
self.bind_into(scope, object)?;
|
|
::std::result::Result::Ok(object)
|
|
}
|
|
}
|
|
|
|
#trait_impl
|
|
})
|
|
}
|
|
|
|
fn expand_object_generated_constructor(
|
|
fields: &[DeclaredField],
|
|
attrs: &ObjectAttrs,
|
|
struct_name: &Ident,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
if attrs.no_dynamic_constructor {
|
|
return Ok(quote!());
|
|
}
|
|
let mut parameters = Vec::new();
|
|
let mut initializers = Vec::new();
|
|
for DeclaredField {
|
|
field,
|
|
attrs: field_attrs,
|
|
} in fields
|
|
{
|
|
let ident = field
|
|
.ident
|
|
.as_ref()
|
|
.ok_or_else(|| Error::new(field.span(), "WebApiObject field requires a name"))?;
|
|
if type_is_unit(&field.ty) {
|
|
initializers.push(quote!(#ident: ()));
|
|
} else if let Some(default) = &field_attrs.constructor_default {
|
|
let default = match default {
|
|
ConstructorDefaultAttr::Default => quote!(::std::default::Default::default()),
|
|
ConstructorDefaultAttr::Expr(expr) => quote!(#expr),
|
|
};
|
|
initializers.push(quote!(#ident: #default));
|
|
} else {
|
|
let ty = &field.ty;
|
|
parameters.push(quote!(#ident: #ty));
|
|
initializers.push(quote!(#ident));
|
|
}
|
|
}
|
|
let constructor = Ident::new("new", struct_name.span());
|
|
Ok(quote! {
|
|
pub fn #constructor(#(#parameters),*) -> Self {
|
|
Self {
|
|
#(#initializers),*
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
struct FunctionTemplateFieldExpansions {
|
|
template_methods: Vec<proc_macro2::TokenStream>,
|
|
prototype_methods: Vec<proc_macro2::TokenStream>,
|
|
}
|
|
|
|
fn expand_function_template_fields(
|
|
fields: &[DeclaredField],
|
|
template_name: &proc_macro2::TokenStream,
|
|
) -> Result<FunctionTemplateFieldExpansions, Error> {
|
|
let mut template_methods = Vec::new();
|
|
let mut prototype_methods = Vec::new();
|
|
let mut method_bindings = HashMap::new();
|
|
for (index, DeclaredField { field, attrs }) in fields.iter().enumerate() {
|
|
if matches!(attrs.kind, FieldKind::Constant) {
|
|
template_methods.push(expand_function_template_constant_field(
|
|
field,
|
|
attrs,
|
|
quote!(template),
|
|
)?);
|
|
prototype_methods.push(expand_function_template_constant_field(
|
|
field,
|
|
attrs,
|
|
quote!(prototype),
|
|
)?);
|
|
continue;
|
|
}
|
|
if matches!(attrs.kind, FieldKind::StaticMethod) {
|
|
let binding = format_ident!("__webapi_template_static_method_{index}");
|
|
template_methods.push(expand_function_template_method_field(
|
|
field,
|
|
attrs,
|
|
template_name,
|
|
binding,
|
|
)?);
|
|
continue;
|
|
}
|
|
if matches!(attrs.kind, FieldKind::Method) {
|
|
let binding = format_ident!("__webapi_template_method_{index}");
|
|
if attrs.symbol.is_none()
|
|
&& let Some(name) = webapi_field_name_literal(attrs.name.as_ref())
|
|
{
|
|
method_bindings.insert(name.value(), binding.clone());
|
|
}
|
|
prototype_methods.push(expand_function_template_method_field(
|
|
field,
|
|
attrs,
|
|
template_name,
|
|
binding,
|
|
)?);
|
|
continue;
|
|
}
|
|
if matches!(attrs.kind, FieldKind::AccessorProperty) {
|
|
prototype_methods.push(expand_function_template_accessor_property_field(
|
|
field,
|
|
attrs,
|
|
template_name,
|
|
)?);
|
|
continue;
|
|
}
|
|
if matches!(attrs.kind, FieldKind::NativeDataProperty) {
|
|
prototype_methods.push(expand_function_template_native_data_property_field(
|
|
field, attrs,
|
|
)?);
|
|
continue;
|
|
}
|
|
if matches!(attrs.kind, FieldKind::IntrinsicDataProperty(_)) {
|
|
prototype_methods.push(expand_function_template_intrinsic_data_property_field(
|
|
field, attrs,
|
|
)?);
|
|
continue;
|
|
}
|
|
if matches!(attrs.kind, FieldKind::Alias(_)) {
|
|
prototype_methods.push(expand_function_template_alias_field(
|
|
field,
|
|
attrs,
|
|
&method_bindings,
|
|
)?);
|
|
continue;
|
|
}
|
|
if attrs.has_installation_kind() || attrs.has_installation_attribute() {
|
|
return Err(Error::new(
|
|
field.span(),
|
|
"function template fields with #[webapi(...)] attributes must declare #[webapi(method)], #[webapi(static_method)], #[webapi(accessor_property)], #[webapi(native_data_property)], #[webapi(intrinsic_data_property = ...)], #[webapi(constant)], or #[webapi(alias = ...)]",
|
|
));
|
|
}
|
|
}
|
|
Ok(FunctionTemplateFieldExpansions {
|
|
template_methods,
|
|
prototype_methods,
|
|
})
|
|
}
|
|
|
|
fn expand_function_template_constant_field(
|
|
field: &Field,
|
|
attrs: &crate::attrs::FieldAttrs,
|
|
template: proc_macro2::TokenStream,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
let key = webapi_field_key(field, attrs)?;
|
|
let name = key.display_name;
|
|
let property_key = key.property_key;
|
|
let Some(value) = attrs.value.as_ref() else {
|
|
return Err(Error::new(
|
|
field.span(),
|
|
"constant field requires #[webapi(value = expr)]",
|
|
));
|
|
};
|
|
let attributes = constant_property_attributes();
|
|
let field_read = field
|
|
.ident
|
|
.as_ref()
|
|
.map(|ident| quote!(let _ = ::std::stringify!(#ident);));
|
|
Ok(quote! {
|
|
#field_read
|
|
let __webapi_constant_value = (#value);
|
|
let __webapi_constant_v8_value =
|
|
::moli_webapi_declare::WebApiTemplateValue::to_v8_template_value(
|
|
&__webapi_constant_value,
|
|
scope,
|
|
)
|
|
.expect("failed to convert Web API constant value");
|
|
#template.set_with_attr(
|
|
#property_key,
|
|
__webapi_constant_v8_value.into(),
|
|
#attributes,
|
|
);
|
|
let _ = #name;
|
|
})
|
|
}
|
|
|
|
fn expand_function_template_method_field(
|
|
field: &Field,
|
|
attrs: &crate::attrs::FieldAttrs,
|
|
template_name: &proc_macro2::TokenStream,
|
|
binding: syn::Ident,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
let key = webapi_field_key(field, attrs)?;
|
|
let name = key.display_name;
|
|
let (property_key, function_name) = expand_template_method_key(
|
|
key.property_key,
|
|
key.function_name,
|
|
key.shared_template_method_name,
|
|
);
|
|
let (target, install, missing_callback) = if matches!(attrs.kind, FieldKind::StaticMethod) {
|
|
(
|
|
quote!(template),
|
|
quote!(::moli_webapi_declare::__private::install_function_template_static_method),
|
|
"static method field requires #[webapi(callback = path)]",
|
|
)
|
|
} else {
|
|
(
|
|
quote!(prototype),
|
|
quote!(::moli_webapi_declare::__private::install_function_template_method),
|
|
"method field requires #[webapi(callback = path)]",
|
|
)
|
|
};
|
|
let Some(callback) = attrs.callback.as_ref() else {
|
|
return Err(Error::new(field.span(), missing_callback));
|
|
};
|
|
let length = attrs.length.unwrap_or(0);
|
|
let attributes = property_attributes(attrs);
|
|
let callback = expand_callback(callback, attrs, false);
|
|
let member = expand_template_function_member(
|
|
&callback,
|
|
length,
|
|
attrs.data.as_ref(),
|
|
template_name,
|
|
&name,
|
|
function_name,
|
|
);
|
|
let field_read = field
|
|
.ident
|
|
.as_ref()
|
|
.map(|ident| quote!(let _ = ::std::stringify!(#ident);));
|
|
Ok(quote! {
|
|
#field_read
|
|
#property_key
|
|
let __webapi_member = #member;
|
|
let #binding = #install(
|
|
scope,
|
|
#target,
|
|
__webapi_property_key,
|
|
__webapi_member,
|
|
#attributes,
|
|
);
|
|
let _ = #name;
|
|
})
|
|
}
|
|
|
|
fn expand_template_method_key(
|
|
property_key: proc_macro2::TokenStream,
|
|
function_name: Option<proc_macro2::TokenStream>,
|
|
shared_name: Option<proc_macro2::TokenStream>,
|
|
) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
|
|
if let Some(shared_name) = shared_name {
|
|
return (
|
|
quote! {
|
|
let __webapi_property_name =
|
|
::moli_webapi_declare::__private::v8str(scope, #shared_name);
|
|
let __webapi_property_key: ::moli_webapi_declare::v8::Local<
|
|
's,
|
|
::moli_webapi_declare::v8::Name,
|
|
> = __webapi_property_name.into();
|
|
},
|
|
quote!(::std::option::Option::Some(__webapi_property_name)),
|
|
);
|
|
}
|
|
|
|
let function_name = function_name
|
|
.map(|function_name| quote!(::std::option::Option::Some(#function_name)))
|
|
.unwrap_or_else(|| quote!(::std::option::Option::None));
|
|
(
|
|
quote! {
|
|
let __webapi_property_key: ::moli_webapi_declare::v8::Local<
|
|
's,
|
|
::moli_webapi_declare::v8::Name,
|
|
> = #property_key;
|
|
},
|
|
function_name,
|
|
)
|
|
}
|
|
|
|
fn expand_function_template_accessor_property_field(
|
|
field: &Field,
|
|
attrs: &crate::attrs::FieldAttrs,
|
|
template_name: &proc_macro2::TokenStream,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
let key = webapi_field_key(field, attrs)?;
|
|
let getter_class_name = expand_template_accessor_class_name(
|
|
"get",
|
|
&key.display_name,
|
|
key.display_name_literal.as_ref(),
|
|
);
|
|
let setter_class_name = expand_template_accessor_class_name(
|
|
"set",
|
|
&key.display_name,
|
|
key.display_name_literal.as_ref(),
|
|
);
|
|
let name = key.display_name;
|
|
let property_key = key.property_key;
|
|
let (getter_member, setter) = expand_accessor_callbacks(attrs, |callback, is_setter, data| {
|
|
let class_name = if is_setter {
|
|
&setter_class_name
|
|
} else {
|
|
&getter_class_name
|
|
};
|
|
expand_template_function_member(
|
|
callback,
|
|
i32::from(is_setter),
|
|
data,
|
|
template_name,
|
|
&name,
|
|
quote!(#class_name),
|
|
)
|
|
});
|
|
let Some(getter_member) = getter_member else {
|
|
return Err(Error::new(
|
|
field.span(),
|
|
"`accessor_property` field requires #[webapi(getter = path)]",
|
|
));
|
|
};
|
|
let attributes = property_attributes(attrs);
|
|
let field_read = field
|
|
.ident
|
|
.as_ref()
|
|
.map(|ident| quote!(let _ = ::std::stringify!(#ident);));
|
|
Ok(quote! {
|
|
#field_read
|
|
let __webapi_property_key: ::moli_webapi_declare::v8::Local<
|
|
's,
|
|
::moli_webapi_declare::v8::Name,
|
|
> = #property_key;
|
|
let __webapi_getter = #getter_member;
|
|
let __webapi_setter: ::std::option::Option<
|
|
::moli_webapi_declare::__private::FunctionTemplateMember<'s>,
|
|
> = #setter;
|
|
::moli_webapi_declare::__private::install_function_template_accessor(
|
|
scope,
|
|
prototype,
|
|
__webapi_property_key,
|
|
__webapi_getter,
|
|
__webapi_setter,
|
|
#attributes,
|
|
);
|
|
let _ = #name;
|
|
})
|
|
}
|
|
|
|
fn expand_template_accessor_class_name(
|
|
prefix: &str,
|
|
display_name: &proc_macro2::TokenStream,
|
|
display_name_literal: Option<&LitStr>,
|
|
) -> proc_macro2::TokenStream {
|
|
if let Some(display_name_literal) = display_name_literal {
|
|
let class_name = LitStr::new(
|
|
&format!("{prefix} {}", display_name_literal.value()),
|
|
display_name_literal.span(),
|
|
);
|
|
return quote!(::moli_webapi_declare::v8::String::new(scope, #class_name));
|
|
}
|
|
|
|
let format_string = LitStr::new(&format!("{prefix} {{}}"), proc_macro2::Span::call_site());
|
|
quote! {
|
|
{
|
|
let __webapi_accessor_name = ::std::format!(#format_string, #display_name);
|
|
::moli_webapi_declare::v8::String::new(scope, &__webapi_accessor_name)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn expand_function_template_native_data_property_field(
|
|
field: &Field,
|
|
attrs: &crate::attrs::FieldAttrs,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
let key = webapi_field_key(field, attrs)?;
|
|
let name = key.display_name;
|
|
let property_key = key.property_key;
|
|
let configuration = expand_native_data_property_configuration(field, attrs)?;
|
|
let field_read = field
|
|
.ident
|
|
.as_ref()
|
|
.map(|ident| quote!(let _ = ::std::stringify!(#ident);));
|
|
Ok(quote! {
|
|
#field_read
|
|
let __webapi_native_data_property_configuration = #configuration;
|
|
prototype.set_native_data_property_with_configuration(
|
|
#property_key,
|
|
__webapi_native_data_property_configuration,
|
|
);
|
|
let _ = #name;
|
|
})
|
|
}
|
|
|
|
fn expand_function_template_intrinsic_data_property_field(
|
|
field: &Field,
|
|
attrs: &crate::attrs::FieldAttrs,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
let key = webapi_field_key(field, attrs)?;
|
|
let name = key.display_name;
|
|
let property_key = key.property_key;
|
|
let FieldKind::IntrinsicDataProperty(intrinsic) = &attrs.kind else {
|
|
return Err(Error::new(
|
|
field.span(),
|
|
"function template intrinsic data property requires an intrinsic value",
|
|
));
|
|
};
|
|
let attributes = property_attributes(attrs);
|
|
let field_read = field
|
|
.ident
|
|
.as_ref()
|
|
.map(|ident| quote!(let _ = ::std::stringify!(#ident);));
|
|
Ok(quote! {
|
|
#field_read
|
|
prototype.set_intrinsic_data_property(
|
|
#property_key,
|
|
#intrinsic,
|
|
#attributes,
|
|
);
|
|
let _ = #name;
|
|
})
|
|
}
|
|
|
|
fn expand_function_template_alias_field(
|
|
field: &Field,
|
|
attrs: &crate::attrs::FieldAttrs,
|
|
method_bindings: &HashMap<String, syn::Ident>,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
if attrs.callback.is_some()
|
|
|| attrs.getter.is_some()
|
|
|| attrs.setter.is_some()
|
|
|| attrs.function_name.is_some()
|
|
|| attrs.length.is_some()
|
|
|| attrs.data.is_some()
|
|
|| attrs.value.is_some()
|
|
|| attrs.init.is_some()
|
|
{
|
|
return Err(Error::new(
|
|
field.span(),
|
|
"function template alias fields cannot define callbacks, function metadata, data, or values",
|
|
));
|
|
}
|
|
let FieldKind::Alias(source_name) = &attrs.kind else {
|
|
return Err(Error::new(
|
|
field.span(),
|
|
"function template alias field requires a source name",
|
|
));
|
|
};
|
|
let Some(source_binding) = method_bindings.get(&source_name.value()) else {
|
|
return Err(Error::new(
|
|
source_name.span(),
|
|
"function template alias source must refer to an earlier string-named method",
|
|
));
|
|
};
|
|
let key = webapi_field_key(field, attrs)?;
|
|
let name = key.display_name;
|
|
let property_key = key.property_key;
|
|
let attributes = property_attributes(attrs);
|
|
let field_read = field
|
|
.ident
|
|
.as_ref()
|
|
.map(|ident| quote!(let _ = ::std::stringify!(#ident);));
|
|
Ok(quote! {
|
|
#field_read
|
|
prototype.set_with_attr(
|
|
#property_key,
|
|
#source_binding.into(),
|
|
#attributes,
|
|
);
|
|
let _ = #name;
|
|
})
|
|
}
|
|
|
|
fn single_lifetime_param(generics: &syn::Generics) -> Option<syn::Lifetime> {
|
|
// Most object declarations carry exactly one lifetime, and that lifetime is
|
|
// the V8 scope lifetime used by `v8::Local<'scope, T>` fields. In that common
|
|
// case the derive can infer the generated `WebApiObjectDeclaration<'scope>`
|
|
// impl. Declarations with multiple lifetimes stay explicit so data lifetimes
|
|
// such as `&'tag str` are not accidentally treated as the V8 scope.
|
|
let mut lifetimes = generics.params.iter().filter_map(|param| match param {
|
|
GenericParam::Lifetime(param) => Some(param.lifetime.clone()),
|
|
_ => None,
|
|
});
|
|
let lifetime = lifetimes.next()?;
|
|
lifetimes.next().is_none().then_some(lifetime)
|
|
}
|
|
|
|
fn expand_object_field(
|
|
declaration: &DeclaredField,
|
|
) -> Option<Result<proc_macro2::TokenStream, Error>> {
|
|
let DeclaredField { field, attrs } = declaration;
|
|
if !attrs.has_installation_kind() {
|
|
if attrs.has_installation_attribute() {
|
|
return Some(Err(Error::new(
|
|
field.span(),
|
|
"declaration-only fields cannot use #[webapi(...)] installation attributes; add an installation kind or remove the attributes",
|
|
)));
|
|
}
|
|
return None;
|
|
}
|
|
if matches!(attrs.kind, FieldKind::StaticMethod) {
|
|
return Some(Err(Error::new(
|
|
field.span(),
|
|
"object fields with #[webapi(...)] attributes cannot declare #[webapi(static_method)]",
|
|
)));
|
|
}
|
|
if matches!(attrs.kind, FieldKind::IntrinsicDataProperty(_)) {
|
|
return Some(Err(Error::new(
|
|
field.span(),
|
|
"`intrinsic_data_property` is only supported by WebApiFunctionTemplate",
|
|
)));
|
|
}
|
|
if matches!(attrs.kind, FieldKind::Constant) {
|
|
let name = match webapi_field_name(field, attrs.name.as_ref()) {
|
|
Ok(name) => name,
|
|
Err(error) => return Some(Err(error)),
|
|
};
|
|
let value = match expand_object_field_value(field, attrs, false) {
|
|
Ok(value) => value,
|
|
Err(error) => return Some(Err(error)),
|
|
};
|
|
return Some(Ok(quote! {
|
|
#value
|
|
::moli_webapi_declare::define_declared_constant_property(
|
|
scope,
|
|
object,
|
|
#name,
|
|
__webapi_value_ref,
|
|
)?;
|
|
}));
|
|
}
|
|
if matches!(attrs.kind, FieldKind::Method) {
|
|
return Some(expand_object_method_field(field, attrs));
|
|
}
|
|
if matches!(attrs.kind, FieldKind::AccessorProperty) {
|
|
return Some(expand_accessor_property_field(field, attrs, quote!(object)));
|
|
}
|
|
if matches!(attrs.kind, FieldKind::NativeDataProperty) {
|
|
return Some(expand_native_data_property_field(field, attrs));
|
|
}
|
|
if matches!(attrs.kind, FieldKind::Alias(_)) {
|
|
return Some(expand_alias_field(field, attrs));
|
|
}
|
|
if matches!(attrs.kind, FieldKind::Prototype | FieldKind::ToStringTag) {
|
|
let field_is_option = type_is_option(&field.ty);
|
|
let optional = field_is_option && attrs.value.is_none() && attrs.init.is_none();
|
|
let value = match expand_object_field_value(field, attrs, optional) {
|
|
Ok(value) => value,
|
|
Err(error) => return Some(Err(error)),
|
|
};
|
|
let bind_value = if matches!(attrs.kind, FieldKind::Prototype) {
|
|
quote! {
|
|
::moli_webapi_declare::set_declared_prototype(
|
|
scope,
|
|
object,
|
|
__webapi_value_ref,
|
|
)?;
|
|
}
|
|
} else {
|
|
let attributes = to_string_tag_property_attributes(attrs);
|
|
quote! {
|
|
::moli_webapi_declare::define_declared_to_string_tag_with_attributes(
|
|
scope,
|
|
object,
|
|
__webapi_value_ref,
|
|
#attributes,
|
|
)?;
|
|
}
|
|
};
|
|
if optional {
|
|
return Some(Ok(quote! {
|
|
if let ::std::option::Option::Some(__webapi_value_ref) = #value {
|
|
#bind_value
|
|
}
|
|
}));
|
|
}
|
|
return Some(Ok(quote! {
|
|
#value
|
|
#bind_value
|
|
}));
|
|
}
|
|
// No installation kind means the field is intentionally not written to the
|
|
// object. This allows declarations to carry Rust/V8 values used only while
|
|
// generating methods or metadata. Keeping the skip here prevents accidental
|
|
// own-data-property exposure on web-facing wrappers.
|
|
if !matches!(
|
|
attrs.kind,
|
|
FieldKind::DataProperty | FieldKind::Hidden | FieldKind::Slot
|
|
) {
|
|
return None;
|
|
}
|
|
let name = match webapi_field_name(field, attrs.name.as_ref()) {
|
|
Ok(name) => name,
|
|
Err(error) => return Some(Err(error)),
|
|
};
|
|
let field_is_option = type_is_option(&field.ty);
|
|
let optional = field_is_option && attrs.value.is_none() && attrs.init.is_none();
|
|
let value = match expand_object_field_value(field, attrs, optional) {
|
|
Ok(value) => value,
|
|
Err(error) => return Some(Err(error)),
|
|
};
|
|
let define_value = if matches!(attrs.kind, FieldKind::Slot) {
|
|
quote! {
|
|
::moli_webapi_declare::define_declared_private_slot(
|
|
scope,
|
|
object,
|
|
#name,
|
|
__webapi_value_ref,
|
|
)?;
|
|
}
|
|
} else if matches!(attrs.kind, FieldKind::Hidden) {
|
|
let writable = syn::LitBool::new(!attrs.readonly, proc_macro2::Span::call_site());
|
|
let configurable = syn::LitBool::new(!attrs.dont_delete, proc_macro2::Span::call_site());
|
|
quote! {
|
|
::moli_webapi_declare::define_declared_hidden_property_with_descriptor(
|
|
scope,
|
|
object,
|
|
#name,
|
|
__webapi_value_ref,
|
|
#writable,
|
|
#configurable,
|
|
)?;
|
|
}
|
|
} else {
|
|
let (install, attributes) = if attrs.readonly || attrs.dont_delete {
|
|
let attributes = property_attributes(attrs);
|
|
(
|
|
quote!(::moli_webapi_declare::define_declared_data_property_with_attributes),
|
|
quote!(, #attributes),
|
|
)
|
|
} else if attrs.enumerable {
|
|
(
|
|
quote!(::moli_webapi_declare::define_declared_enumerable_data_property),
|
|
quote!(),
|
|
)
|
|
} else {
|
|
(
|
|
quote!(::moli_webapi_declare::define_declared_data_property),
|
|
quote!(),
|
|
)
|
|
};
|
|
quote! {
|
|
#install(scope, object, #name, __webapi_value_ref #attributes)?;
|
|
}
|
|
};
|
|
if optional {
|
|
return Some(Ok(quote! {
|
|
if let ::std::option::Option::Some(__webapi_value_ref) = #value {
|
|
#define_value
|
|
}
|
|
}));
|
|
}
|
|
Some(Ok(quote! {
|
|
#value
|
|
#define_value
|
|
}))
|
|
}
|
|
|
|
fn expand_accessor_property_field(
|
|
field: &Field,
|
|
attrs: &crate::attrs::FieldAttrs,
|
|
object: proc_macro2::TokenStream,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
let key = webapi_field_key(field, attrs)?;
|
|
let name = key.display_name;
|
|
let property_key = key.property_key;
|
|
if attrs.getter.is_some() && attrs.getter_value.is_some() {
|
|
return Err(Error::new(
|
|
field.span(),
|
|
"`accessor_property` field cannot declare both #[webapi(getter = ...)] and #[webapi(getter_value = ...)]",
|
|
));
|
|
}
|
|
let (getter, setter) = expand_accessor_callbacks(attrs, |callback, is_setter, data| {
|
|
let accessor_name = if is_setter { "setter" } else { "getter" };
|
|
let function = expand_function_builder(callback, i32::from(is_setter), data, &name);
|
|
quote! {
|
|
#function.ok_or_else(|| {
|
|
::moli_webapi_declare::BindError::new(
|
|
::std::format!("failed to build declared `{}` {}", #name, #accessor_name)
|
|
)
|
|
})?
|
|
}
|
|
});
|
|
let getter = getter.or_else(|| attrs.getter_value.as_ref().map(|value| quote!(#value)))
|
|
.ok_or_else(|| Error::new(
|
|
field.span(),
|
|
"`accessor_property` field requires #[webapi(getter = path)] or #[webapi(getter_value = expr)]",
|
|
))?;
|
|
let attributes = property_attributes(attrs);
|
|
let field_read = field
|
|
.ident
|
|
.as_ref()
|
|
.map(|ident| quote!(let _ = &self.#ident;));
|
|
Ok(quote! {
|
|
#field_read
|
|
let __webapi_getter = #getter;
|
|
let __webapi_setter = #setter;
|
|
::moli_webapi_declare::define_declared_accessor_property_by_key(
|
|
scope,
|
|
#object,
|
|
#property_key,
|
|
#name,
|
|
__webapi_getter,
|
|
__webapi_setter,
|
|
#attributes,
|
|
)?;
|
|
})
|
|
}
|
|
|
|
fn expand_native_data_property_field(
|
|
field: &Field,
|
|
attrs: &crate::attrs::FieldAttrs,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
let key = webapi_field_key(field, attrs)?;
|
|
let name = key.display_name;
|
|
let property_key = key.property_key;
|
|
let configuration = expand_native_data_property_configuration(field, attrs)?;
|
|
let field_read = field
|
|
.ident
|
|
.as_ref()
|
|
.map(|ident| quote!(let _ = &self.#ident;));
|
|
Ok(quote! {
|
|
#field_read
|
|
let __webapi_native_data_property_configuration = #configuration;
|
|
object
|
|
.set_native_data_property_with_configuration(
|
|
scope,
|
|
#property_key,
|
|
__webapi_native_data_property_configuration,
|
|
)
|
|
.unwrap_or(false)
|
|
.then_some(())
|
|
.ok_or_else(|| {
|
|
::moli_webapi_declare::BindError::new(
|
|
::std::format!("failed to define declared `{}` native data property", #name)
|
|
)
|
|
})?;
|
|
let _ = #name;
|
|
})
|
|
}
|
|
|
|
fn expand_object_method_field(
|
|
field: &Field,
|
|
attrs: &crate::attrs::FieldAttrs,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
let key = webapi_field_key(field, attrs)?;
|
|
let name = key.display_name;
|
|
let property_key = key.property_key;
|
|
let set_function_name = key.function_name.map(|function_name| {
|
|
quote! {
|
|
function.set_name(#function_name);
|
|
}
|
|
});
|
|
let Some(callback) = attrs.callback.as_ref() else {
|
|
return Err(Error::new(
|
|
field.span(),
|
|
"method field requires #[webapi(callback = path)]",
|
|
));
|
|
};
|
|
let length = attrs.length.unwrap_or(0);
|
|
let enumerable = attrs.enumerable;
|
|
let writable = !attrs.readonly;
|
|
let configurable = !attrs.dont_delete;
|
|
let callback = expand_callback(callback, attrs, false);
|
|
let build_function = expand_function_builder(&callback, length, attrs.data.as_ref(), &name);
|
|
let install_method = quote! {
|
|
let function = #build_function.ok_or_else(|| {
|
|
::moli_webapi_declare::BindError::new(
|
|
::std::format!("failed to build declared `{}` method", #name)
|
|
)
|
|
})?;
|
|
#set_function_name
|
|
let mut descriptor =
|
|
::moli_webapi_declare::v8::PropertyDescriptor::new_from_value_writable(
|
|
function.into(),
|
|
#writable,
|
|
);
|
|
descriptor.set_configurable(#configurable);
|
|
descriptor.set_enumerable(#enumerable);
|
|
object
|
|
.define_property(
|
|
scope,
|
|
#property_key,
|
|
&descriptor,
|
|
)
|
|
.unwrap_or(false)
|
|
.then_some(())
|
|
.ok_or_else(|| {
|
|
::moli_webapi_declare::BindError::new(
|
|
::std::format!("failed to define declared `{}` method", #name)
|
|
)
|
|
})?;
|
|
};
|
|
let Some(ident) = field.ident.as_ref() else {
|
|
return Ok(quote! {
|
|
#install_method
|
|
});
|
|
};
|
|
if type_is_option(&field.ty) {
|
|
return Ok(quote! {
|
|
if self.#ident.is_some() {
|
|
#install_method
|
|
}
|
|
});
|
|
}
|
|
Ok(quote! {
|
|
let _ = &self.#ident;
|
|
#install_method
|
|
})
|
|
}
|
|
|
|
fn expand_alias_field(
|
|
field: &Field,
|
|
attrs: &crate::attrs::FieldAttrs,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
let key = webapi_field_key(field, attrs)?;
|
|
let name = key.display_name;
|
|
let property_key = key.property_key;
|
|
let FieldKind::Alias(source_name) = &attrs.kind else {
|
|
return Err(Error::new(
|
|
field.span(),
|
|
"alias field requires a source name",
|
|
));
|
|
};
|
|
let enumerable = syn::LitBool::new(attrs.enumerable, proc_macro2::Span::call_site());
|
|
let writable = syn::LitBool::new(!attrs.readonly, proc_macro2::Span::call_site());
|
|
let configurable = syn::LitBool::new(!attrs.dont_delete, proc_macro2::Span::call_site());
|
|
let field_read = field
|
|
.ident
|
|
.as_ref()
|
|
.map(|ident| quote!(let _ = &self.#ident;));
|
|
let optional = type_is_option(&field.ty);
|
|
let install_alias = quote! {
|
|
let __webapi_alias_source_key =
|
|
::moli_webapi_declare::__private::v8str(scope, #source_name);
|
|
object
|
|
.has_own_property(scope, __webapi_alias_source_key.into())
|
|
.unwrap_or(false)
|
|
.then_some(())
|
|
.ok_or_else(|| {
|
|
::moli_webapi_declare::BindError::new(
|
|
::std::format!(
|
|
"failed to find declared alias source `{}` for `{}`",
|
|
#source_name,
|
|
#name
|
|
)
|
|
)
|
|
})?;
|
|
let __webapi_alias_value = object
|
|
.get(scope, __webapi_alias_source_key.into())
|
|
.ok_or_else(|| {
|
|
::moli_webapi_declare::BindError::new(
|
|
::std::format!(
|
|
"failed to read declared alias source `{}` for `{}`",
|
|
#source_name,
|
|
#name
|
|
)
|
|
)
|
|
})?;
|
|
let mut descriptor =
|
|
::moli_webapi_declare::v8::PropertyDescriptor::new_from_value_writable(
|
|
__webapi_alias_value,
|
|
#writable,
|
|
);
|
|
descriptor.set_configurable(#configurable);
|
|
descriptor.set_enumerable(#enumerable);
|
|
object
|
|
.define_property(scope, #property_key, &descriptor)
|
|
.unwrap_or(false)
|
|
.then_some(())
|
|
.ok_or_else(|| {
|
|
::moli_webapi_declare::BindError::new(
|
|
::std::format!("failed to define declared `{}` alias", #name)
|
|
)
|
|
})?;
|
|
};
|
|
if optional {
|
|
let Some(ident) = field.ident.as_ref() else {
|
|
return Err(Error::new(
|
|
field.span(),
|
|
"optional alias field requires a field name",
|
|
));
|
|
};
|
|
return Ok(quote! {
|
|
if self.#ident.is_some() {
|
|
#install_alias
|
|
}
|
|
});
|
|
}
|
|
Ok(quote! {
|
|
#field_read
|
|
#install_alias
|
|
})
|
|
}
|
|
|
|
fn expand_native_data_property_configuration(
|
|
field: &Field,
|
|
attrs: &FieldAttrs,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
let getter = attrs.getter.as_ref().ok_or_else(|| {
|
|
Error::new(
|
|
field.span(),
|
|
"`native_data_property` field requires #[webapi(getter = path)]",
|
|
)
|
|
})?;
|
|
let setter = attrs.setter.as_ref().map(|setter| quote!(.setter(#setter)));
|
|
let data = attrs
|
|
.data
|
|
.as_ref()
|
|
.map(|data| quote!(.data((#data).into())));
|
|
let attributes = property_attributes(attrs);
|
|
Ok(quote! {
|
|
::moli_webapi_declare::v8::NativeDataPropertyConfiguration::new(#getter)
|
|
#setter
|
|
#data
|
|
.property_attribute(#attributes)
|
|
})
|
|
}
|
|
|
|
/// Resolve callback policies and setter-data fallback once. Each backend owns
|
|
/// function construction and its installation-time failure handling.
|
|
fn expand_accessor_callbacks(
|
|
attrs: &FieldAttrs,
|
|
mut build: impl FnMut(
|
|
&proc_macro2::TokenStream,
|
|
bool,
|
|
Option<&syn::Expr>,
|
|
) -> proc_macro2::TokenStream,
|
|
) -> (Option<proc_macro2::TokenStream>, proc_macro2::TokenStream) {
|
|
let mut expand = |callback: &Option<syn::Path>, is_setter| {
|
|
callback.as_ref().map(|callback| {
|
|
let data = if is_setter {
|
|
attrs.setter_data.as_ref().or(attrs.data.as_ref())
|
|
} else {
|
|
attrs.data.as_ref()
|
|
};
|
|
build(
|
|
&expand_callback(callback, attrs, is_setter),
|
|
is_setter,
|
|
data,
|
|
)
|
|
})
|
|
};
|
|
let getter = expand(&attrs.getter, false);
|
|
let setter = expand(&attrs.setter, true)
|
|
.map(|setter| quote!(::std::option::Option::Some(#setter)))
|
|
.unwrap_or_else(|| quote!(::std::option::Option::None));
|
|
(getter, setter)
|
|
}
|
|
|
|
/// Generate a native callback adapter, without changing callback data or
|
|
/// introducing a JavaScript wrapper. Receiver checks precede argument conversion.
|
|
fn expand_callback(
|
|
callback: &syn::Path,
|
|
attrs: &crate::attrs::FieldAttrs,
|
|
is_setter: bool,
|
|
) -> proc_macro2::TokenStream {
|
|
let returns_promise = attrs.returns_promise && !is_setter;
|
|
if attrs.receiver.is_none() && !returns_promise {
|
|
return quote!(#callback);
|
|
}
|
|
let receiver_check = attrs.receiver.as_ref().map(|receiver| {
|
|
let check = quote!(#receiver(scope, args.this()));
|
|
quote! {
|
|
if !#check {
|
|
::moli_webapi_declare::__private::throw_illegal_invocation(scope);
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
let adapter = if returns_promise {
|
|
quote! {
|
|
fn __webapi_promise_callback<'s>(
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<'s, '_>,
|
|
args: ::moli_webapi_declare::v8::FunctionCallbackArguments<'s>,
|
|
rv: ::moli_webapi_declare::v8::ReturnValue<'s>,
|
|
) {
|
|
::moli_webapi_declare::__private::invoke_promise_callback(
|
|
scope, args, rv, __webapi_callback,
|
|
);
|
|
}
|
|
__webapi_promise_callback
|
|
}
|
|
} else {
|
|
quote!(__webapi_callback)
|
|
};
|
|
quote! {{
|
|
fn __webapi_callback<'s>(
|
|
scope: &mut ::moli_webapi_declare::v8::PinScope<'s, '_>,
|
|
args: ::moli_webapi_declare::v8::FunctionCallbackArguments<'s>,
|
|
rv: ::moli_webapi_declare::v8::ReturnValue<'s>,
|
|
) {
|
|
#receiver_check
|
|
#callback(scope, args, rv);
|
|
}
|
|
#adapter
|
|
}}
|
|
}
|
|
|
|
fn expand_function_builder(
|
|
callback: &proc_macro2::TokenStream,
|
|
length: i32,
|
|
data: Option<&syn::Expr>,
|
|
display_name: &proc_macro2::TokenStream,
|
|
) -> proc_macro2::TokenStream {
|
|
// Evaluate callback state during installation, where both `scope` and
|
|
// `self` exist. Merely carrying Rust state does not expose a JS property.
|
|
let convert_data = data.map(|data| quote! {
|
|
let __webapi_callback_data = (#data);
|
|
let __webapi_callback_data =
|
|
::moli_webapi_declare::WebApiValue::to_v8_value(
|
|
&__webapi_callback_data, scope,
|
|
).ok_or_else(|| {
|
|
::moli_webapi_declare::BindError::new(
|
|
::std::format!("failed to convert declared `{}` callback data", #display_name)
|
|
)
|
|
})?;
|
|
});
|
|
let attach_data = data.map(|_| quote!(.data(__webapi_callback_data)));
|
|
quote! {{
|
|
#convert_data
|
|
::moli_webapi_declare::v8::Function::builder(#callback)
|
|
.length(#length)
|
|
#attach_data
|
|
.constructor_behavior(::moli_webapi_declare::v8::ConstructorBehavior::Throw)
|
|
.build(scope)
|
|
}}
|
|
}
|
|
|
|
fn expand_template_function_member(
|
|
callback: &proc_macro2::TokenStream,
|
|
length: i32,
|
|
data: Option<&syn::Expr>,
|
|
template_name: &proc_macro2::TokenStream,
|
|
display_name: &proc_macro2::TokenStream,
|
|
class_name: proc_macro2::TokenStream,
|
|
) -> proc_macro2::TokenStream {
|
|
let data = match data {
|
|
Some(data) => quote! {
|
|
{
|
|
let __webapi_callback_data = (#data);
|
|
let __webapi_callback_data =
|
|
::moli_webapi_declare::WebApiTemplateValue::to_v8_template_value(
|
|
&__webapi_callback_data,
|
|
scope,
|
|
)
|
|
.unwrap_or_else(|| {
|
|
panic!(
|
|
"failed to convert Web API `{}` member `{}` template callback data",
|
|
#template_name,
|
|
#display_name,
|
|
)
|
|
});
|
|
::std::option::Option::Some(__webapi_callback_data)
|
|
}
|
|
},
|
|
None => quote!(::std::option::Option::None),
|
|
};
|
|
quote! {
|
|
::moli_webapi_declare::__private::FunctionTemplateMember {
|
|
callback: ::moli_webapi_declare::v8::MapFnTo::<
|
|
::moli_webapi_declare::v8::FunctionCallback,
|
|
>::map_fn_to(#callback),
|
|
length: #length,
|
|
data: #data,
|
|
class_name: #class_name,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn property_attributes(attrs: &crate::attrs::FieldAttrs) -> proc_macro2::TokenStream {
|
|
let mut property_attributes = if attrs.enumerable {
|
|
quote!(::moli_webapi_declare::v8::PropertyAttribute::NONE)
|
|
} else {
|
|
quote!(::moli_webapi_declare::v8::PropertyAttribute::DONT_ENUM)
|
|
};
|
|
if attrs.readonly {
|
|
property_attributes = quote! {
|
|
#property_attributes
|
|
| ::moli_webapi_declare::v8::PropertyAttribute::READ_ONLY
|
|
};
|
|
}
|
|
if attrs.dont_delete {
|
|
property_attributes = quote! {
|
|
#property_attributes
|
|
| ::moli_webapi_declare::v8::PropertyAttribute::DONT_DELETE
|
|
};
|
|
}
|
|
property_attributes
|
|
}
|
|
|
|
fn constant_property_attributes() -> proc_macro2::TokenStream {
|
|
quote! {
|
|
::moli_webapi_declare::webidl_constant_property_attributes()
|
|
}
|
|
}
|
|
|
|
fn to_string_tag_property_attributes(attrs: &crate::attrs::FieldAttrs) -> proc_macro2::TokenStream {
|
|
let mut property_attributes = quote!(::moli_webapi_declare::v8::PropertyAttribute::DONT_ENUM);
|
|
if attrs.readonly {
|
|
property_attributes = quote! {
|
|
#property_attributes
|
|
| ::moli_webapi_declare::v8::PropertyAttribute::READ_ONLY
|
|
};
|
|
}
|
|
if attrs.dont_delete {
|
|
property_attributes = quote! {
|
|
#property_attributes
|
|
| ::moli_webapi_declare::v8::PropertyAttribute::DONT_DELETE
|
|
};
|
|
}
|
|
property_attributes
|
|
}
|
|
|
|
fn webapi_field_key(
|
|
field: &Field,
|
|
attrs: &crate::attrs::FieldAttrs,
|
|
) -> Result<WebApiFieldKey, Error> {
|
|
let mut key = if let Some(symbol) = attrs.symbol.as_ref() {
|
|
webapi_symbol_field_key(symbol)?
|
|
} else {
|
|
let name = webapi_field_name(field, attrs.name.as_ref())?;
|
|
let name_literal = webapi_field_name_literal(attrs.name.as_ref());
|
|
WebApiFieldKey {
|
|
display_name: name.clone(),
|
|
display_name_literal: name_literal,
|
|
property_key: quote!(::moli_webapi_declare::__private::v8str(scope, #name).into()),
|
|
function_name: Some(quote!(
|
|
::moli_webapi_declare::__private::v8str(scope, #name)
|
|
)),
|
|
shared_template_method_name: Some(name),
|
|
}
|
|
};
|
|
if let Some(function_name) = attrs.function_name.as_ref() {
|
|
key.function_name = Some(quote!(
|
|
::moli_webapi_declare::__private::v8str(scope, #function_name)
|
|
));
|
|
key.shared_template_method_name = None;
|
|
}
|
|
Ok(key)
|
|
}
|
|
|
|
fn webapi_symbol_field_key(symbol: &LitStr) -> Result<WebApiFieldKey, Error> {
|
|
match symbol.value().as_str() {
|
|
"iterator" => {
|
|
let display_name = LitStr::new("[Symbol.iterator]", symbol.span());
|
|
Ok(WebApiFieldKey {
|
|
display_name: quote!(#display_name),
|
|
display_name_literal: Some(display_name),
|
|
property_key: quote!(::moli_webapi_declare::v8::Symbol::get_iterator(scope).into()),
|
|
function_name: None,
|
|
shared_template_method_name: None,
|
|
})
|
|
}
|
|
"asyncIterator" => {
|
|
let display_name = LitStr::new("[Symbol.asyncIterator]", symbol.span());
|
|
Ok(WebApiFieldKey {
|
|
display_name: quote!(#display_name),
|
|
display_name_literal: Some(display_name),
|
|
property_key: quote!(
|
|
::moli_webapi_declare::v8::Symbol::get_async_iterator(scope).into()
|
|
),
|
|
function_name: None,
|
|
shared_template_method_name: None,
|
|
})
|
|
}
|
|
"toStringTag" => {
|
|
let display_name = LitStr::new("[Symbol.toStringTag]", symbol.span());
|
|
Ok(WebApiFieldKey {
|
|
display_name: quote!(#display_name),
|
|
display_name_literal: Some(display_name),
|
|
property_key: quote!(
|
|
::moli_webapi_declare::v8::Symbol::get_to_string_tag(scope).into()
|
|
),
|
|
function_name: None,
|
|
shared_template_method_name: None,
|
|
})
|
|
}
|
|
_ => Err(Error::new(symbol.span(), "unsupported Web API symbol key")),
|
|
}
|
|
}
|
|
|
|
fn webapi_field_name(
|
|
field: &Field,
|
|
explicit: Option<&syn::Expr>,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
let name = explicit.ok_or_else(|| Error::new(field.span(), "webapi field requires a name"))?;
|
|
Ok(quote!(#name))
|
|
}
|
|
|
|
fn webapi_field_name_literal(explicit: Option<&syn::Expr>) -> Option<LitStr> {
|
|
match explicit {
|
|
Some(syn::Expr::Lit(syn::ExprLit {
|
|
lit: Lit::Str(name),
|
|
..
|
|
})) => Some(name.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn implicit_field_name(field: &Field, rename_all: RenameRule) -> Result<LitStr, Error> {
|
|
let ident = field
|
|
.ident
|
|
.as_ref()
|
|
.ok_or_else(|| Error::new(field.span(), "object field requires a name"))?;
|
|
let name = ident.to_string();
|
|
let name = name.strip_prefix("r#").unwrap_or(&name);
|
|
let name = name.strip_prefix('_').unwrap_or(name);
|
|
if name.is_empty() {
|
|
return Err(Error::new(field.span(), "webapi field requires a name"));
|
|
}
|
|
Ok(LitStr::new(
|
|
&apply_rename_rule(name, rename_all),
|
|
ident.span(),
|
|
))
|
|
}
|
|
|
|
fn apply_rename_rule(name: &str, rename_all: RenameRule) -> String {
|
|
match rename_all {
|
|
RenameRule::None => name.to_owned(),
|
|
RenameRule::CamelCase => snake_to_camel_case(name),
|
|
}
|
|
}
|
|
|
|
fn snake_to_camel_case(name: &str) -> String {
|
|
let mut output = String::with_capacity(name.len());
|
|
let mut uppercase_next = false;
|
|
for character in name.chars() {
|
|
if character == '_' {
|
|
uppercase_next = !output.is_empty();
|
|
continue;
|
|
}
|
|
if uppercase_next {
|
|
if character.is_alphabetic() {
|
|
output.extend(character.to_uppercase());
|
|
uppercase_next = false;
|
|
} else {
|
|
output.push(character);
|
|
}
|
|
} else {
|
|
output.push(character);
|
|
}
|
|
}
|
|
output
|
|
}
|
|
|
|
fn expand_object_field_value(
|
|
field: &Field,
|
|
attrs: &crate::attrs::FieldAttrs,
|
|
optional: bool,
|
|
) -> Result<proc_macro2::TokenStream, Error> {
|
|
if let Some(value) = attrs.value.as_ref() {
|
|
let field_read = field
|
|
.ident
|
|
.as_ref()
|
|
.map(|ident| quote!(let _ = &self.#ident;));
|
|
return Ok(quote! {
|
|
#field_read
|
|
let __webapi_value = (#value);
|
|
let __webapi_value_ref = &__webapi_value;
|
|
});
|
|
}
|
|
if let Some(init) = attrs.init.as_ref() {
|
|
let field_read = field
|
|
.ident
|
|
.as_ref()
|
|
.map(|ident| quote!(let _ = &self.#ident;));
|
|
let value = match init {
|
|
ValueInitAttr::Null => quote!(::moli_webapi_declare::v8::null(scope)),
|
|
ValueInitAttr::Object => quote!(::moli_webapi_declare::v8::Object::new(scope)),
|
|
ValueInitAttr::NullObject => quote!({
|
|
let object = ::moli_webapi_declare::v8::Object::new(scope);
|
|
let null = ::moli_webapi_declare::v8::null(scope);
|
|
let _ = object.set_prototype(scope, null.into());
|
|
object
|
|
}),
|
|
ValueInitAttr::Array => quote!(::moli_webapi_declare::v8::Array::new(scope, 0)),
|
|
ValueInitAttr::Undefined => quote!(::moli_webapi_declare::v8::undefined(scope)),
|
|
ValueInitAttr::True => quote!(::moli_webapi_declare::v8::Boolean::new(scope, true)),
|
|
ValueInitAttr::False => quote!(::moli_webapi_declare::v8::Boolean::new(scope, false)),
|
|
ValueInitAttr::Zero => quote!(::moli_webapi_declare::v8::Number::new(scope, 0.0)),
|
|
ValueInitAttr::EmptyString => quote!(::moli_webapi_declare::v8::String::empty(scope)),
|
|
ValueInitAttr::String(value) => {
|
|
let field_name = field
|
|
.ident
|
|
.as_ref()
|
|
.map(|ident| ident.to_string())
|
|
.unwrap_or_else(|| "<unnamed>".to_string());
|
|
let field_name = LitStr::new(&field_name, field.span());
|
|
quote! {
|
|
::moli_webapi_declare::WebApiValue::to_v8_value(&#value, scope)
|
|
.ok_or_else(|| ::moli_webapi_declare::BindError::new(
|
|
::std::format!("failed to convert declared `{}` initializer", #field_name)
|
|
))?
|
|
}
|
|
}
|
|
};
|
|
return Ok(quote! {
|
|
#field_read
|
|
let __webapi_value = #value;
|
|
let __webapi_value_ref = &__webapi_value;
|
|
});
|
|
}
|
|
|
|
if matches!(attrs.kind, FieldKind::DataProperty | FieldKind::Hidden)
|
|
|| matches!(
|
|
attrs.kind,
|
|
FieldKind::Slot | FieldKind::Prototype | FieldKind::ToStringTag
|
|
)
|
|
{
|
|
let Some(ident) = field.ident.as_ref() else {
|
|
return Err(Error::new(field.span(), "object field requires a name"));
|
|
};
|
|
if optional {
|
|
return Ok(quote! {
|
|
self.#ident.as_ref()
|
|
});
|
|
}
|
|
return Ok(quote! {
|
|
let __webapi_value_ref = &self.#ident;
|
|
});
|
|
}
|
|
Err(Error::new(
|
|
field.span(),
|
|
"object field requires #[webapi(value = expr)]",
|
|
))
|
|
}
|
|
|
|
fn type_is_option(ty: &Type) -> bool {
|
|
let Type::Path(path) = ty else {
|
|
return false;
|
|
};
|
|
path.qself.is_none()
|
|
&& path
|
|
.path
|
|
.segments
|
|
.last()
|
|
.is_some_and(|segment| segment.ident == "Option")
|
|
}
|
|
|
|
fn type_is_unit(ty: &Type) -> bool {
|
|
matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty())
|
|
}
|
|
|
|
fn named_fields(data: &Data) -> Result<Vec<Field>, Error> {
|
|
match data {
|
|
Data::Struct(data) => match &data.fields {
|
|
Fields::Named(fields) => Ok(fields.named.iter().cloned().collect()),
|
|
_ => Err(Error::new(
|
|
data.struct_token.span(),
|
|
"expected a struct with named fields",
|
|
)),
|
|
},
|
|
_ => Err(Error::new(
|
|
proc_macro2::Span::call_site(),
|
|
"expected a struct",
|
|
)),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{expand_webapi_function_template, expand_webapi_object};
|
|
|
|
#[test]
|
|
fn callback_policies_reject_fields_that_cannot_apply_them() {
|
|
let fields: Vec<syn::Field> = vec![
|
|
syn::parse_quote!(#[webapi(data_property, receiver = check)] value: ()),
|
|
syn::parse_quote!(#[webapi(static_method, callback = call, receiver = check)] value: ()),
|
|
syn::parse_quote!(#[webapi(native_data_property, getter = get, receiver = check)] value: ()),
|
|
syn::parse_quote!(#[webapi(alias = "other", receiver = check)] value: ()),
|
|
syn::parse_quote!(#[webapi(data_property, returns_promise)] value: ()),
|
|
syn::parse_quote!(#[webapi(native_data_property, getter = get, returns_promise)] value: ()),
|
|
syn::parse_quote!(#[webapi(accessor_property, getter_value = self.getter, receiver = check)] value: ()),
|
|
syn::parse_quote!(#[webapi(accessor_property, getter_value = self.getter, returns_promise)] value: ()),
|
|
syn::parse_quote!(#[webapi(method, callback = call, receiver = check, receiver = other)] value: ()),
|
|
];
|
|
for field in fields {
|
|
assert!(crate::attrs::parse_field_attrs(&field).is_err());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn inherited_receiver_cannot_silently_skip_an_already_built_getter() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(plain, receiver = check)]
|
|
struct Invalid {
|
|
#[webapi(accessor_property, getter_value = self.getter)]
|
|
getter: (),
|
|
}
|
|
};
|
|
let error = expand_webapi_object(input).unwrap_err();
|
|
assert!(error.to_string().contains("require a Rust callback"));
|
|
}
|
|
|
|
#[test]
|
|
fn declaration_only_field_attributes_are_rejected() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(plain)]
|
|
struct BadObject {
|
|
#[webapi(init = "null")]
|
|
ignored: (),
|
|
}
|
|
};
|
|
let error = match expand_webapi_object(input) {
|
|
Ok(_) => panic!("declaration-only field attributes should be rejected"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"declaration-only fields cannot use #[webapi(...)] installation attributes; add an installation kind or remove the attributes"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn default_data_property_fields_can_use_data_property_initializers() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(plain, data_properties)]
|
|
struct DefaultObject {
|
|
#[webapi(init = "null")]
|
|
value: (),
|
|
}
|
|
};
|
|
expand_webapi_object(input).expect("default data-property initializer should expand");
|
|
}
|
|
|
|
#[test]
|
|
fn object_generated_constructor_defaults_to_new_for_dynamic_fields() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(plain)]
|
|
struct DynamicObject {
|
|
#[webapi(data_property, init = true)]
|
|
brand: (),
|
|
#[webapi(data_property)]
|
|
value: u32,
|
|
}
|
|
};
|
|
let tokens = expand_webapi_object(input)
|
|
.expect("dynamic object should expand")
|
|
.to_string();
|
|
assert!(tokens.contains("pub fn new (value : u32) -> Self"));
|
|
assert!(tokens.contains("brand : ()"));
|
|
assert!(tokens.contains("value"));
|
|
}
|
|
|
|
#[test]
|
|
fn object_generated_constructor_defaults_to_new_for_unit_fields() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(plain)]
|
|
struct StaticObject {
|
|
#[webapi(data_property, init = true)]
|
|
brand: (),
|
|
#[webapi(method, callback = static_callback)]
|
|
action: (),
|
|
}
|
|
};
|
|
let tokens = expand_webapi_object(input)
|
|
.expect("static object should expand")
|
|
.to_string();
|
|
assert!(tokens.contains("pub fn new () -> Self"));
|
|
assert!(tokens.contains("brand : ()"));
|
|
assert!(tokens.contains("action : ()"));
|
|
}
|
|
|
|
#[test]
|
|
fn object_generated_constructor_uses_constructor_defaults() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(plain)]
|
|
struct DefaultedObject {
|
|
#[webapi(data_property)]
|
|
value: u32,
|
|
#[webapi(data_property, constructor_default = "ready")]
|
|
state: &'static str,
|
|
#[webapi(data_property, constructor_default = Vec::new())]
|
|
items: Vec<u32>,
|
|
#[webapi(data_property, constructor_default)]
|
|
count: usize,
|
|
}
|
|
};
|
|
let tokens = expand_webapi_object(input)
|
|
.expect("defaulted object should expand")
|
|
.to_string();
|
|
assert!(tokens.contains("pub fn new (value : u32) -> Self"));
|
|
assert!(tokens.contains("state : \"ready\""));
|
|
assert!(tokens.contains("items : Vec :: new ()"));
|
|
assert!(tokens.contains("count : :: std :: default :: Default :: default ()"));
|
|
}
|
|
|
|
#[test]
|
|
fn object_generated_constructor_defaults_can_reference_previous_fields() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(plain)]
|
|
struct DerivedDefaultObject {
|
|
#[webapi(data_property)]
|
|
client_x: i32,
|
|
#[webapi(data_property, constructor_default = client_x)]
|
|
x: i32,
|
|
}
|
|
};
|
|
let tokens = expand_webapi_object(input)
|
|
.expect("derived default object should expand")
|
|
.to_string();
|
|
assert!(tokens.contains("pub fn new (client_x : i32) -> Self"));
|
|
assert!(tokens.contains("client_x"));
|
|
assert!(tokens.contains("x : client_x"));
|
|
}
|
|
|
|
#[test]
|
|
fn object_generated_constructor_can_be_suppressed() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(plain, no_dynamic_constructor)]
|
|
struct ManualObject {
|
|
#[webapi(data_property)]
|
|
value: u32,
|
|
}
|
|
};
|
|
let tokens = expand_webapi_object(input)
|
|
.expect("manual object should expand")
|
|
.to_string();
|
|
assert!(!tokens.contains("pub fn new"));
|
|
}
|
|
|
|
#[test]
|
|
fn function_template_fields_with_unsupported_installation_kinds_are_rejected() {
|
|
for kind in [
|
|
"data_property",
|
|
"slot",
|
|
"hidden",
|
|
"prototype",
|
|
"to_string_tag",
|
|
] {
|
|
let input = syn::parse_str(&format!(
|
|
r#"
|
|
#[webapi(name = "Sample")]
|
|
struct BadTemplate {{
|
|
#[webapi({kind})]
|
|
ignored: (),
|
|
}}
|
|
"#
|
|
))
|
|
.expect("parse function template");
|
|
let error = match expand_webapi_function_template(input) {
|
|
Ok(_) => panic!("unsupported function template field kind should be rejected"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"function template fields with #[webapi(...)] attributes must declare #[webapi(method)], #[webapi(static_method)], #[webapi(accessor_property)], #[webapi(native_data_property)], #[webapi(intrinsic_data_property = ...)], #[webapi(constant)], or #[webapi(alias = ...)]"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn function_template_intrinsic_data_properties_are_supported() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(name = "Sample", enumerable)]
|
|
struct SampleTemplate {
|
|
#[webapi(
|
|
intrinsic_data_property = v8::Intrinsic::ArrayProtoValues
|
|
)]
|
|
values: (),
|
|
#[webapi(
|
|
intrinsic_data_property = v8::Intrinsic::ArrayProtoValues,
|
|
symbol = "iterator",
|
|
readonly,
|
|
dont_delete
|
|
)]
|
|
iterator: (),
|
|
}
|
|
};
|
|
expand_webapi_function_template(input)
|
|
.expect("intrinsic data properties should expand on function templates");
|
|
}
|
|
|
|
#[test]
|
|
fn function_template_intrinsic_prototype_parent_is_supported() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(
|
|
name = "Example Iterator",
|
|
intrinsic_prototype_parent = v8::Intrinsic::IteratorPrototype,
|
|
prototype_to_string_tag = "Example Iterator",
|
|
readonly_prototype
|
|
)]
|
|
struct IteratorTemplate {
|
|
#[webapi(method, callback = sample_callback)]
|
|
value: (),
|
|
}
|
|
};
|
|
expand_webapi_function_template(input)
|
|
.expect("intrinsic prototype parents should expand on function templates");
|
|
}
|
|
|
|
#[test]
|
|
fn object_intrinsic_data_properties_are_rejected() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(plain)]
|
|
struct BadObject {
|
|
#[webapi(
|
|
intrinsic_data_property = v8::Intrinsic::ArrayProtoValues
|
|
)]
|
|
values: (),
|
|
}
|
|
};
|
|
let error = match expand_webapi_object(input) {
|
|
Ok(_) => panic!("runtime object intrinsic property should be rejected"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"`intrinsic_data_property` is only supported by WebApiFunctionTemplate"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn function_template_native_data_properties_are_supported() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(name = "Sample", enumerable)]
|
|
struct SampleTemplate {
|
|
#[webapi(native_data_property, getter = sample_getter)]
|
|
value: (),
|
|
#[webapi(native_data_property = "named", getter = sample_getter, setter = sample_setter, dont_delete)]
|
|
named: (),
|
|
}
|
|
};
|
|
expand_webapi_function_template(input)
|
|
.expect("native accessors should expand on function templates");
|
|
}
|
|
|
|
#[test]
|
|
fn function_template_accessor_property_fields_are_supported() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(name = "Sample", enumerable)]
|
|
struct SampleTemplate {
|
|
#[webapi(accessor_property, getter = sample_getter)]
|
|
value: (),
|
|
#[webapi(accessor_property = "named", getter = sample_getter, setter = sample_setter, data = getter_data, setter_data = setter_data, dont_delete)]
|
|
named: (),
|
|
}
|
|
};
|
|
let tokens = expand_webapi_function_template(input)
|
|
.expect("template accessors should expand")
|
|
.to_string();
|
|
assert!(tokens.contains("String :: new (scope , \"get value\")"));
|
|
assert!(tokens.contains("String :: new (scope , \"get named\")"));
|
|
assert!(tokens.contains("String :: new (scope , \"set named\")"));
|
|
assert!(tokens.contains("install_function_template_accessor"));
|
|
assert_eq!(tokens.matches("FunctionTemplate :: builder").count(), 1);
|
|
assert!(!tokens.contains("format ! (\"get {}\""));
|
|
assert!(!tokens.contains("format ! (\"set {}\""));
|
|
}
|
|
|
|
#[test]
|
|
fn function_template_dynamic_accessor_names_are_formatted_at_runtime() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(name = "Sample")]
|
|
struct SampleTemplate {
|
|
#[webapi(accessor_property = DYNAMIC_NAME, getter = sample_getter, setter = sample_setter)]
|
|
value: (),
|
|
}
|
|
};
|
|
let tokens = expand_webapi_function_template(input)
|
|
.expect("dynamic template accessor should expand")
|
|
.to_string();
|
|
assert!(tokens.contains("format ! (\"get {}\" , DYNAMIC_NAME)"));
|
|
assert!(tokens.contains("format ! (\"set {}\" , DYNAMIC_NAME)"));
|
|
}
|
|
|
|
#[test]
|
|
fn function_template_readonly_accessor_property_fields_are_rejected() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(name = "Sample")]
|
|
struct BadTemplate {
|
|
#[webapi(accessor_property, getter = sample_getter, readonly)]
|
|
value: (),
|
|
}
|
|
};
|
|
let error = match expand_webapi_function_template(input) {
|
|
Ok(_) => panic!("readonly function-template accessor property should be rejected"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"`accessor_property` fields have no writable attribute; omit #[webapi(setter)] instead of using readonly"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn object_readonly_accessor_property_fields_are_rejected() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(interface = interfaces::Sample)]
|
|
struct BadObject {
|
|
#[webapi(accessor_property, getter = sample_getter, readonly)]
|
|
value: (),
|
|
}
|
|
};
|
|
let error = match expand_webapi_object(input) {
|
|
Ok(_) => panic!("readonly runtime-object accessor property should be rejected"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"`accessor_property` fields have no writable attribute; omit #[webapi(setter)] instead of using readonly"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn object_accessor_property_rejects_callback_and_getter_value_together() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(interface = interfaces::Sample)]
|
|
struct BadObject<'scope> {
|
|
getter: ::moli_webapi_declare::v8::Local<'scope, ::moli_webapi_declare::v8::Function>,
|
|
#[webapi(accessor_property, getter = sample_getter, getter_value = self.getter)]
|
|
value: (),
|
|
}
|
|
};
|
|
let error = match expand_webapi_object(input) {
|
|
Ok(_) => panic!("getter and getter_value should be rejected together"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"`accessor_property` field cannot declare both #[webapi(getter = ...)] and #[webapi(getter_value = ...)]"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn object_native_data_property_fields_are_supported() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(interface = interfaces::Sample, enumerable)]
|
|
struct SampleObject {
|
|
#[webapi(native_data_property, getter = sample_getter)]
|
|
value: (),
|
|
#[webapi(native_data_property = "named", getter = sample_getter, setter = sample_setter, dont_delete)]
|
|
named: (),
|
|
}
|
|
};
|
|
expand_webapi_object(input).expect("native accessors should expand on objects");
|
|
}
|
|
|
|
#[test]
|
|
fn object_readonly_native_data_property_fields_are_rejected() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(interface = interfaces::Sample)]
|
|
struct BadObject {
|
|
#[webapi(native_data_property, getter = sample_getter, readonly)]
|
|
value: (),
|
|
}
|
|
};
|
|
let error = match expand_webapi_object(input) {
|
|
Ok(_) => panic!("readonly native data property should be rejected"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"`native_data_property` fields have no writable attribute; omit #[webapi(setter)] instead of using readonly"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn object_static_method_fields_are_rejected() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(plain, data_properties)]
|
|
struct BadObject {
|
|
#[webapi(static_method, callback = sample_callback)]
|
|
create: (),
|
|
}
|
|
};
|
|
let error = match expand_webapi_object(input) {
|
|
Ok(_) => panic!("object static_method should be rejected"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"object fields with #[webapi(...)] attributes cannot declare #[webapi(static_method)]"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn object_constant_fields_are_supported() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(plain, data_properties)]
|
|
struct BadObject {
|
|
#[webapi(constant = "READY", value = 4u32)]
|
|
ready: (),
|
|
}
|
|
};
|
|
expand_webapi_object(input).expect("object constant should expand");
|
|
}
|
|
|
|
#[test]
|
|
fn function_template_alias_source_must_be_earlier_string_method() {
|
|
let input = syn::parse_quote! {
|
|
#[webapi(name = "Sample")]
|
|
struct BadTemplate {
|
|
#[webapi(alias = "entries", symbol = "iterator")]
|
|
iterator: (),
|
|
#[webapi(method, callback = sample_callback)]
|
|
entries: (),
|
|
}
|
|
};
|
|
let error = match expand_webapi_function_template(input) {
|
|
Ok(_) => panic!("out-of-order template alias should be rejected"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"function template alias source must refer to an earlier string-named method"
|
|
);
|
|
|
|
let input = syn::parse_quote! {
|
|
#[webapi(name = "Sample")]
|
|
struct BadTemplate {
|
|
#[webapi(method, symbol = "iterator", callback = sample_callback)]
|
|
entries: (),
|
|
#[webapi(alias = "entries", symbol = "asyncIterator")]
|
|
async_iterator: (),
|
|
}
|
|
};
|
|
let error = match expand_webapi_function_template(input) {
|
|
Ok(_) => panic!("symbol method source should not be a template alias source"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"function template alias source must refer to an earlier string-named method"
|
|
);
|
|
}
|
|
}
|