Implement readonly struct fields

Add support for `#[wasm_bindgen(readonly)]` which indicates that an exported
struct field is readonly and attempting to set it in JS will throw an exception.

Closes #151
This commit is contained in:
Alex Crichton
2018-04-20 10:56:10 -07:00
parent 3b4bf475be
commit 7108206835
6 changed files with 103 additions and 22 deletions

View File

@ -1044,6 +1044,23 @@ possibilities!
All of these functions will call `console.log` in Rust, but each identifier
will have only one signature in Rust.
* `readonly` - when attached to a `pub` struct field this indicates that it's
readonly from JS and a setter will not be generated.
```rust
#[wasm_bindgen]
pub struct Foo {
pub first: u32,
#[wasm_bindgen(readonly)]
pub second: u32,
}
```
Here the `first` field will be both readable and writable from JS, but the
`second` field will be a `readonly` field in JS where the setter isn't
implemented and attempting to set it will throw an exception.
## Rust Type conversions
Previously we've been seeing mostly abridged versions of type conversions when

View File

@ -72,6 +72,7 @@ pub struct Struct {
}
pub struct StructField {
pub opts: BindgenAttrs,
pub name: syn::Ident,
pub struct_name: syn::Ident,
pub ty: syn::Type,
@ -132,8 +133,8 @@ impl Program {
}
syn::Item::Struct(mut s) => {
let opts = opts.unwrap_or_else(|| BindgenAttrs::find(&mut s.attrs));
self.structs.push(Struct::from(&mut s, opts));
s.to_tokens(tokens);
self.structs.push(Struct::from(s, opts));
}
syn::Item::Impl(mut i) => {
let opts = opts.unwrap_or_else(|| BindgenAttrs::find(&mut i.attrs));
@ -668,10 +669,10 @@ impl ImportType {
}
impl Struct {
fn from(s: syn::ItemStruct, _opts: BindgenAttrs) -> Struct {
fn from(s: &mut syn::ItemStruct, _opts: BindgenAttrs) -> Struct {
let mut fields = Vec::new();
if let syn::Fields::Named(names) = s.fields {
for field in names.named.iter() {
if let syn::Fields::Named(names) = &mut s.fields {
for field in names.named.iter_mut() {
match field.vis {
syn::Visibility::Public(..) => {}
_ => continue,
@ -682,7 +683,9 @@ impl Struct {
};
let getter = shared::struct_field_get(s.ident.as_ref(), name.as_ref());
let setter = shared::struct_field_set(s.ident.as_ref(), name.as_ref());
let opts = BindgenAttrs::find(&mut field.attrs);
fields.push(StructField {
opts,
name,
struct_name: s.ident,
ty: field.ty.clone(),
@ -709,6 +712,7 @@ impl StructField {
fn shared(&self) -> shared::StructField {
shared::StructField {
name: self.name.as_ref().to_string(),
readonly: self.opts.readonly(),
}
}
}
@ -800,6 +804,13 @@ impl BindgenAttrs {
})
}
pub fn readonly(&self) -> bool {
self.attrs.iter().any(|a| match *a {
BindgenAttr::Readonly => true,
_ => false,
})
}
pub fn js_name(&self) -> Option<syn::Ident> {
self.attrs
.iter()
@ -836,6 +847,7 @@ enum BindgenAttr {
Getter(Option<syn::Ident>),
Setter(Option<syn::Ident>),
Structural,
Readonly,
JsName(syn::Ident),
}
@ -869,6 +881,8 @@ impl syn::synom::Synom for BindgenAttr {
|
call!(term, "structural") => { |_| BindgenAttr::Structural }
|
call!(term, "readonly") => { |_| BindgenAttr::Readonly }
|
do_parse!(
call!(term, "js_namespace") >>
punct!(=) >>

View File

@ -247,6 +247,18 @@ impl ToTokens for ast::StructField {
)
}
#[no_mangle]
pub extern fn #desc() {
use wasm_bindgen::describe::*;
<#ty as WasmDescribe>::describe();
}
}).to_tokens(tokens);
if self.opts.readonly() {
return
}
(quote! {
#[no_mangle]
pub unsafe extern fn #setter(
js: u32,
@ -263,13 +275,6 @@ impl ToTokens for ast::StructField {
);
(*js).borrow_mut().#name = val;
}
#[no_mangle]
pub extern fn #desc() {
use wasm_bindgen::describe::*;
<#ty as WasmDescribe>::describe();
}
}).to_tokens(tokens);
}
}

View File

@ -32,14 +32,15 @@ pub struct Context<'a> {
#[derive(Default)]
pub struct ExportedClass {
pub contents: String,
pub typescript: String,
pub constructor: Option<String>,
pub fields: Vec<ClassField>,
contents: String,
typescript: String,
constructor: Option<String>,
fields: Vec<ClassField>,
}
pub struct ClassField {
pub name: String,
struct ClassField {
name: String,
readonly: bool,
}
pub struct SubContext<'a, 'b: 'a> {
@ -416,7 +417,8 @@ impl<'a> Context<'a> {
cx.method(true)
.argument(&descriptor)
.ret(&None);
ts_dst.push_str(&format!("{}: {}\n",
ts_dst.push_str(&format!("{}{}: {}\n",
if field.readonly { "readonly " } else { "" },
field.name,
&cx.js_arguments[0].1));
cx.finish("", &format!("wasm.{}", wasm_setter)).0
@ -430,9 +432,11 @@ impl<'a> Context<'a> {
dst.push_str(&field.name);
dst.push_str(&get);
dst.push_str("\n");
dst.push_str("set ");
dst.push_str(&field.name);
dst.push_str(&set);
if !field.readonly {
dst.push_str("set ");
dst.push_str(&field.name);
dst.push_str(&set);
}
}
dst.push_str(&format!("
@ -1312,6 +1316,7 @@ impl<'a, 'b> SubContext<'a, 'b> {
.extend(s.fields.iter().map(|s| {
ClassField {
name: s.name.clone(),
readonly: s.readonly,
}
}));
}

View File

@ -1,7 +1,7 @@
#[macro_use]
extern crate serde_derive;
pub const SCHEMA_VERSION: &str = "3";
pub const SCHEMA_VERSION: &str = "4";
#[derive(Deserialize)]
pub struct ProgramOnlySchema {
@ -91,6 +91,7 @@ pub struct Struct {
#[derive(Deserialize, Serialize)]
pub struct StructField {
pub name: String,
pub readonly: bool,
}
pub fn new_function(struct_name: &str) -> String {

View File

@ -558,3 +558,42 @@ fn using_self() {
"#)
.test();
}
#[test]
fn readonly_fields() {
project()
.debug(false)
.file("src/lib.rs", r#"
#![feature(proc_macro, wasm_custom_section, wasm_import_module)]
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
#[derive(Default)]
pub struct Foo {
#[wasm_bindgen(readonly)]
pub a: u32,
}
#[wasm_bindgen]
impl Foo {
pub fn new() -> Foo {
Foo::default()
}
}
"#)
.file("test.ts", r#"
import { Foo } from "./out";
import * as assert from "assert";
export function test() {
const a = Foo.new();
assert.strictEqual(a.a, 0);
assert.throws(() => (a as any).a = 3, /has only a getter/);
a.free();
}
"#)
.test();
}