diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..89af8c2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +target +Cargo.lock +examples/todomvc/target +examples/todomvc/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..b451523e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "stdweb" +version = "0.1.0" +authors = ["Jan Bujak "] +repository = "https://github.com/koute/stdweb" +homepage = "https://github.com/koute/stdweb" +documentation = "https://docs.rs/stdweb/*/stdweb/" +license = "MIT/Apache-2.0" +readme = "README.md" +keywords = ["web", "asmjs", "webasm", "javascript"] +description = "A standard library for the client-side Web" + +[dependencies] +serde = { version = "1", optional = true } +serde_json = { version = "1", optional = true } +clippy = { version = "0.0", optional = true } + +[dev-dependencies] +serde_json = "1" +serde_derive = "1" + +[features] +default = ["serde", "serde_json"] +dev = ["serde", "serde_json", "clippy"] +serde-support = ["serde", "serde_json"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 00000000..cc176fb7 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2017 Jan Bujak + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 00000000..3cb2325e --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2017 Jan Bujak + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..3c56ed46 --- /dev/null +++ b/README.md @@ -0,0 +1,140 @@ +

+ +

+ +[![Build Status](https://api.travis-ci.org/koute/stdweb.svg)](https://travis-ci.org/koute/stdweb) + +# A standard library for the client-side Web + +[![Documentation](https://docs.rs/stdweb/badge.svg)](https://docs.rs/stdweb/*/stdweb/) + +The goal of this crate is to provide Rust bindings to the Web APIs and to allow +a high degree of interoperability between Rust and JavaScript. + +## Examples + +You can directly embed JavaScript code into Rust: + +```rust +let message = "Hello, 世界!"; +let result = js! { + alert!( @{message} ); + return 2 + 2 * 2; +}; + +println!( "2 + 2 * 2 = {:?}", result ); +``` + +Even closures are are supported: + +```rust +let print_hello = |name: String| { + println!( "Hello, {}!", name ); +}; + +js! { + var print_hello = @{print_hello}; + print_hello( "Bob" ); + print_hello.drop(); // Necessary to clean up the closure on Rust's side. +} +``` + +You can also pass arbitrary structures thanks to [serde]: + +```rust +#[derive(Serialize)] +struct Person { + name: String, + age: i32 +} + +js_serializable!( Person ); + +js! { + var person = @{person}; + console.log( person.name + " is " + person.age + " years old." ); +}; +``` + +[serde]: https://serde.rs/ + +This crate also exposes a number of Web APIs, for example: + +```rust +let button = document().query_selector( "#hide-button" ).unwrap(); +button.add_event_listener( move |_: ClickEvent| { + for anchor in document().query_selector_all( "#main a" ) { + js!( @{anchor}.style = "display: none;"; ); + } +}); +``` + +## Design goals + + * Expose a full suite of Web APIs as exposed by web browsers. + * Try to follow the original JavaScript conventions and structure as much as possible, + except in cases where doing otherwise results in a clearly superior design. + * Be a building block from which higher level frameworks and libraries + can be built. + * Make it convenient and easy to embed JavaScript code directly into Rust + and to marshal data between the two. + * Integrate with the wider Rust ecosystem, e.g. support marshaling of structs + which implement serde's Serializable. + * Put Rust in the driver's seat where a non-trivial Web application can be + written without touching JavaScript at all. + * Allow Rust to take part in the upcoming WebAssembly (re)volution. + +## Getting started + +WARNING: This crate is still a work-in-progress. Things might not work. +Things might break. The APIs are in flux. Please do not use it in production. + +1. Add an asmjs target with rustup: + + $ rustup target add asmjs-unknown-emscripten + +2. Install emscripten. If you're on Arch Linux then you can just + run `sudo pacman -S emscripten`; other distributions might also + have recent enough emscripten packages in their repositories. + + Alternatively you can install it like this: + + $ curl -O https://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz + $ tar -xzf emsdk-portable.tar.gz + $ source emsdk_portable/emsdk_env.sh + $ emsdk update + $ emsdk install sdk-incoming-64bit + $ emsdk activate sdk-incoming-64bit + +3. Install [cargo-web]; it's not strictly necessary but it makes things + more convenient: + + $ cargo install cargo-web + +4. Go into `examples/todomvc` and type: + + $ cargo web start + +5. Visit `http://localhost:8000` with your browser. + +[cargo-web]: https://github.com/koute/cargo-web + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +Snippets of documentation which come from [Mozilla Developer Network] are covered under the [CC-BY-SA, version 2.5] or later. + +[Mozilla Developer Network]: https://developer.mozilla.org/en-US/ +[CC-BY-SA, version 2.5]: https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. diff --git a/examples/todomvc/Cargo.toml b/examples/todomvc/Cargo.toml new file mode 100644 index 00000000..fe01ede2 --- /dev/null +++ b/examples/todomvc/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "todomvc" +version = "0.1.0" +authors = ["Jan Bujak "] + +[dependencies] +serde = "1" +serde_json = "1" +serde_derive = "1" +stdweb = { path = "../.." } diff --git a/examples/todomvc/README.md b/examples/todomvc/README.md new file mode 100644 index 00000000..e69de29b diff --git a/examples/todomvc/src/main.rs b/examples/todomvc/src/main.rs new file mode 100644 index 00000000..25c9d88b --- /dev/null +++ b/examples/todomvc/src/main.rs @@ -0,0 +1,241 @@ +#[macro_use] +extern crate stdweb; + +#[macro_use] +extern crate serde_derive; +extern crate serde_json; + +use std::cell::RefCell; +use std::rc::Rc; + +use stdweb::unstable::TryInto; +use stdweb::web::{ + IEventTarget, + IElement, + IHtmlElement, + INode, + HtmlElement, + Element, + document, + window +}; + +use stdweb::web::event::{ + IEvent, + IKeyboardEvent, + DoubleClickEvent, + ClickEvent, + KeypressEvent, + ChangeEvent, + BlurEvent, + HashChangeEvent +}; + +use stdweb::web::html_element::InputElement; + +// Shamelessly stolen from webplatform's TodoMVC example. +macro_rules! enclose { + ( ($( $x:ident ),*) $y:expr ) => { + { + $(let $x = $x.clone();)* + $y + } + }; +} + +#[derive(Clone, Serialize, Deserialize)] +struct Todo { + title: String, + completed: bool +} + +#[derive(Serialize, Deserialize)] +struct State { + todo_list: Vec< Todo > +} + +impl State { + fn new() -> Self { + State { + todo_list: Vec::new() + } + } +} + +type StateRef = Rc< RefCell< State > >; + +fn start_editing( state: &StateRef, index: usize, li: &HtmlElement, label: &Element ) { + li.class_list().add( "editing" ); + + let edit: InputElement = document().create_element( "input" ).try_into().unwrap(); + edit.class_list().add( "edit" ); + edit.set_value( label.inner_text() ); + edit.add_event_listener( enclose!( (edit) move |event: KeypressEvent| { + if event.key() == "Enter" { + edit.blur(); + } + })); + + edit.add_event_listener( enclose!( (state, li, edit) move |_: BlurEvent| { + li.class_list().remove( "editing" ); + li.remove_child( &edit ).unwrap(); + state.borrow_mut().todo_list[ index ].title = edit.value().into_string().unwrap(); + update_dom( &state ); + })); + + li.append_child( &edit ); + edit.focus(); +} + +fn create_entry( state: &StateRef, index: usize, text: &str ) -> HtmlElement { + let li: HtmlElement = document().create_element( "li" ).try_into().unwrap(); + let div = document().create_element( "div" ); + let checkbox: InputElement = document().create_element( "input" ).try_into().unwrap(); + let label = document().create_element( "label" ); + let button = document().create_element( "button" ); + + div.class_list().add( "view" ); + + checkbox.class_list().add( "toggle" ); + checkbox.set_kind( "checkbox" ); + checkbox.add_event_listener( enclose!( (state, checkbox) move |_: ChangeEvent| { + let checked: bool = js!( return @{&checkbox}.checked; ).try_into().unwrap(); + state.borrow_mut().todo_list[ index ].completed = checked; + update_dom( &state ); + })); + + label.append_child( &document().create_text_node( text ) ); + label.add_event_listener( enclose!( (state, li, label) move |_: DoubleClickEvent| { + start_editing( &state, index, &li, &label ); + })); + + button.class_list().add( "destroy" ); + button.add_event_listener( enclose!( (state) move |_: ClickEvent| { + state.borrow_mut().todo_list.remove( index ); + update_dom( &state ); + })); + + li.append_child( &div ); + div.append_child( &checkbox ); + div.append_child( &label ); + div.append_child( &button ); + + li +} + +fn update_dom( state: &StateRef ) { + // Ideally you'd use some kind of DOM diffing here; + // since it's supposed to be a simple example we opt + // for the nuclear option and just rebuild everything + // from scratch. + + fn only_active( todo: &Todo ) -> bool { todo.completed == false } + fn only_completed( todo: &Todo ) -> bool { todo.completed == true } + fn all( _: &Todo ) -> bool { true } + + // See which filter we're supposed to use based on the URL. + let hash = document().location().unwrap().hash(); + let filter = match hash.as_str() { + "#/active" => only_active, + "#/completed" => only_completed, + _ => all + }; + + let filter_anchor_selector = match hash.as_str() { + "#/active" | "#/completed" => hash.as_str(), + _ => "#/" + }; + + // Select the filter "button". + let filter_anchors = document().query_selector_all( ".filters a" ); + for anchor in &filter_anchors { + let anchor: Element = anchor.try_into().unwrap(); + anchor.class_list().remove( "selected" ); + } + + let filter_anchor_selector = format!( ".filters a[href='{}']", filter_anchor_selector ); + let selected_anchor: Element = document().query_selector( filter_anchor_selector.as_str() ).unwrap().try_into().unwrap(); + selected_anchor.class_list().add( "selected" ); + + // Clear previous entries in the list. + let list = document().query_selector( ".todo-list" ).unwrap(); + while let Some( child ) = list.first_child() { + list.remove_child( &child ).unwrap(); + } + + // Fill out the list. + let state_borrow = state.borrow(); + for (index, todo) in state_borrow.todo_list.iter().enumerate().filter( |&(_, todo)| filter( todo ) ) { + let entry_node = create_entry( state, index, todo.title.as_str() ); + if todo.completed { + entry_node.class_list().add( "completed" ); + let checkbox = entry_node.query_selector( "input[type='checkbox']" ).unwrap(); + js!( @{checkbox}.checked = true; ); + } + list.append_child( &entry_node ); + } + + // Display the amount of active TODOs lefs. + let items_left = state_borrow.todo_list.iter().filter( |todo| { + todo.completed == false + }).count(); + + let counter_display = document().query_selector( ".todo-count" ).unwrap(); + if items_left == 1 { + counter_display.set_text_content( "1 item left" ); + } else { + counter_display.set_text_content( format!( "{} items left", items_left ).as_str() ); + } + + // Hide the list if we don't have any TODOs. + let main = document().query_selector( ".main" ).unwrap(); + if state_borrow.todo_list.is_empty() { + js!( @{main}.style = "display: none;" ); + } else { + js!( @{main}.style = "display: block;" ); + } + + // Save the state into local storage. + let state_json = serde_json::to_string( &*state_borrow ).unwrap(); + window().local_storage().insert( "state", state_json.as_str() ); +} + +fn main() { + stdweb::initialize(); + + let state = window().local_storage().get( "state" ).and_then( |state_json| { + serde_json::from_str( state_json.as_str() ).ok() + }).unwrap_or_else( State::new ); + let state = Rc::new( RefCell::new( state ) ); + + let title_entry: InputElement = document().query_selector( ".new-todo" ).unwrap().try_into().unwrap(); + title_entry.add_event_listener( enclose!( (state, title_entry) move |event: KeypressEvent| { + if event.key() == "Enter" { + event.prevent_default(); + + let title: String = title_entry.value().try_into().unwrap(); + if title.is_empty() == false { + state.borrow_mut().todo_list.push( Todo { + title: title, + completed: false + }); + + title_entry.set_value( "" ); + update_dom( &state ); + } + } + })); + + let clear_completed = document().query_selector( ".clear-completed" ).unwrap(); + clear_completed.add_event_listener( enclose!( (state) move |_: ClickEvent| { + state.borrow_mut().todo_list.retain( |todo| todo.completed == false ); + update_dom( &state ); + })); + + window().add_event_listener( enclose!( (state) move |_: HashChangeEvent| { + update_dom( &state ); + })); + + update_dom( &state ); + stdweb::event_loop(); +} diff --git a/examples/todomvc/static/css/todomvc-app-css/index.css b/examples/todomvc/static/css/todomvc-app-css/index.css new file mode 100644 index 00000000..e6e089cb --- /dev/null +++ b/examples/todomvc/static/css/todomvc-app-css/index.css @@ -0,0 +1,378 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; + font-weight: 300; +} + +button, +input[type="checkbox"] { + outline: none; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + outline: none; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +label[for='toggle-all'] { + display: none; +} + +.toggle-all { + position: absolute; + top: -55px; + left: -12px; + width: 60px; + height: 34px; + text-align: center; + border: none; /* Mobile Safari */ +} + +.toggle-all:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: 506px; + padding: 13px 17px 12px 17px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle:after { + content: url('data:image/svg+xml;utf8,'); +} + +.todo-list li .toggle:checked:after { + content: url('data:image/svg+xml;utf8,'); +} + +.todo-list li label { + white-space: pre-line; + word-break: break-all; + padding: 15px 60px 15px 15px; + margin-left: 45px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: '×'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a.selected, +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; + position: relative; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } + + .toggle-all { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-appearance: none; + appearance: none; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} diff --git a/examples/todomvc/static/css/todomvc-common/base.css b/examples/todomvc/static/css/todomvc-common/base.css new file mode 100644 index 00000000..da65968a --- /dev/null +++ b/examples/todomvc/static/css/todomvc-common/base.css @@ -0,0 +1,141 @@ +hr { + margin: 20px 0; + border: 0; + border-top: 1px dashed #c5c5c5; + border-bottom: 1px dashed #f7f7f7; +} + +.learn a { + font-weight: normal; + text-decoration: none; + color: #b83f45; +} + +.learn a:hover { + text-decoration: underline; + color: #787e7e; +} + +.learn h3, +.learn h4, +.learn h5 { + margin: 10px 0; + font-weight: 500; + line-height: 1.2; + color: #000; +} + +.learn h3 { + font-size: 24px; +} + +.learn h4 { + font-size: 18px; +} + +.learn h5 { + margin-bottom: 0; + font-size: 14px; +} + +.learn ul { + padding: 0; + margin: 0 0 30px 25px; +} + +.learn li { + line-height: 20px; +} + +.learn p { + font-size: 15px; + font-weight: 300; + line-height: 1.3; + margin-top: 0; + margin-bottom: 0; +} + +#issue-count { + display: none; +} + +.quote { + border: none; + margin: 20px 0 60px 0; +} + +.quote p { + font-style: italic; +} + +.quote p:before { + content: '“'; + font-size: 50px; + opacity: .15; + position: absolute; + top: -20px; + left: 3px; +} + +.quote p:after { + content: '”'; + font-size: 50px; + opacity: .15; + position: absolute; + bottom: -42px; + right: 3px; +} + +.quote footer { + position: absolute; + bottom: -40px; + right: 0; +} + +.quote footer img { + border-radius: 3px; +} + +.quote footer a { + margin-left: 5px; + vertical-align: middle; +} + +.speech-bubble { + position: relative; + padding: 10px; + background: rgba(0, 0, 0, .04); + border-radius: 5px; +} + +.speech-bubble:after { + content: ''; + position: absolute; + top: 100%; + right: 30px; + border: 13px solid transparent; + border-top-color: rgba(0, 0, 0, .04); +} + +.learn-bar > .learn { + position: absolute; + width: 272px; + top: 8px; + left: -300px; + padding: 10px; + border-radius: 5px; + background-color: rgba(255, 255, 255, .6); + transition-property: left; + transition-duration: 500ms; +} + +@media (min-width: 899px) { + .learn-bar { + width: auto; + padding-left: 300px; + } + + .learn-bar > .learn { + left: 8px; + } +} diff --git a/examples/todomvc/static/index.html b/examples/todomvc/static/index.html new file mode 100644 index 00000000..85433955 --- /dev/null +++ b/examples/todomvc/static/index.html @@ -0,0 +1,43 @@ + + + + + stdweb • TodoMVC + + + + +
+
+

todos

+ +
+
+ + +
    + +
    +
    +
    +

    Double-click to edit a todo

    +

    Part of the stdweb project

    +

    Based on TodoMVC

    +
    + + + diff --git a/info/logo.png b/info/logo.png new file mode 100644 index 00000000..c08bd5e4 Binary files /dev/null and b/info/logo.png differ diff --git a/src/ecosystem/mod.rs b/src/ecosystem/mod.rs new file mode 100644 index 00000000..84f13648 --- /dev/null +++ b/src/ecosystem/mod.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "serde")] +pub mod serde; + +#[cfg(feature = "serde_json")] +pub mod serde_json; diff --git a/src/ecosystem/serde.rs b/src/ecosystem/serde.rs new file mode 100644 index 00000000..7aefce44 --- /dev/null +++ b/src/ecosystem/serde.rs @@ -0,0 +1,1462 @@ +// To give credit where it is due - a significant chunk of code +// in this file was borrowed from `serde_json`. + +use std::fmt; +use std::error; +use std::collections::BTreeMap; +use std::vec; + +use serde_crate::ser::{self, Serialize}; +use serde_crate::de::{self, Deserialize, Visitor}; +use serde_crate::de::IntoDeserializer; + +use webcore::value::{ + Undefined, + Null, + Value +}; + +use webcore::serialization::{ + JsSerializable, + SerializedValue, + PreallocatedArena +}; + +use webcore::number::{self, Number, Storage, get_storage}; +use webcore::try_from::{TryInto, TryFrom}; + +impl Serialize for Undefined { + #[inline] + fn serialize< S: ser::Serializer >( &self, serializer: S ) -> Result< S::Ok, S::Error > { + serializer.serialize_unit_struct( "undefined" ) + } +} + +impl< 'de > Deserialize< 'de > for Undefined { + #[inline] + fn deserialize< D: de::Deserializer< 'de > >( deserializer: D ) -> Result< Self, D::Error > { + struct UndefinedVisitor; + impl< 'de > Visitor< 'de > for UndefinedVisitor { + type Value = Undefined; + + fn expecting( &self, formatter: &mut fmt::Formatter ) -> fmt::Result { + formatter.write_str( "undefined" ) + } + + fn visit_unit< E: de::Error >( self ) -> Result< Self::Value, E > { + Ok( Undefined ) + } + } + + deserializer.deserialize_unit_struct( "undefined", UndefinedVisitor ) + } +} + +impl Serialize for Null { + #[inline] + fn serialize< S: ser::Serializer >( &self, serializer: S ) -> Result< S::Ok, S::Error > { + serializer.serialize_unit_struct( "null" ) + } +} + +impl< 'de > Deserialize< 'de > for Null { + #[inline] + fn deserialize< D: de::Deserializer< 'de > >( deserializer: D ) -> Result< Self, D::Error > { + struct NullVisitor; + impl< 'de > Visitor< 'de > for NullVisitor { + type Value = Null; + + fn expecting( &self, formatter: &mut fmt::Formatter ) -> fmt::Result { + formatter.write_str( "null" ) + } + + fn visit_unit< E: de::Error >( self ) -> Result< Self::Value, E > { + Ok( Null ) + } + } + + deserializer.deserialize_unit_struct( "null", NullVisitor ) + } +} + +impl Serialize for Number { + #[inline] + fn serialize< S: ser::Serializer >( &self, serializer: S ) -> Result< S::Ok, S::Error > { + match *get_storage( self ) { + Storage::I32( value ) => serializer.serialize_i32( value ), + Storage::F64( value ) => serializer.serialize_f64( value ) + } + } +} + +impl< 'de > Deserialize< 'de > for Number { + #[inline] + fn deserialize< D: de::Deserializer< 'de > >( deserializer: D ) -> Result< Self, D::Error > { + struct NumberVisitor; + impl< 'de > Visitor< 'de > for NumberVisitor { + type Value = Number; + + fn expecting( &self, formatter: &mut fmt::Formatter ) -> fmt::Result { + formatter.write_str( "a number" ) + } + + fn visit_i8< E: de::Error >( self, value: i8 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_i16< E: de::Error >( self, value: i16 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_i32< E: de::Error >( self, value: i32 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_i64< E: de::Error >( self, value: i64 ) -> Result< Self::Value, E > { + value.try_into().map_err( E::custom ) + } + + fn visit_u8< E: de::Error >( self, value: u8 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_u16< E: de::Error >( self, value: u16 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_u32< E: de::Error >( self, value: u32 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_u64< E: de::Error >( self, value: u64 ) -> Result< Self::Value, E > { + value.try_into().map_err( E::custom ) + } + + fn visit_f32< E: de::Error >( self, value: f32 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_f64< E: de::Error >( self, value: f64 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + } + + deserializer.deserialize_f64( NumberVisitor ) + } +} + +impl Serialize for Value { + #[inline] + fn serialize< S: ser::Serializer >( &self, serializer: S ) -> Result< S::Ok, S::Error > { + use serde_crate::ser::SerializeMap; + match *self { + Value::Undefined => serializer.serialize_unit_struct( "undefined" ), + Value::Null => serializer.serialize_unit_struct( "null" ), + Value::Bool( value ) => serializer.serialize_bool( value ), + Value::Number( ref value ) => value.serialize( serializer ), + Value::String( ref value ) => serializer.serialize_str( value ), + Value::Array( ref value ) => value.serialize( serializer ), + Value::Object( ref value ) => { + let mut map = try!( serializer.serialize_map( Some( value.len() ) ) ); + for (key, value) in value { + try!( map.serialize_key( key ) ); + try!( map.serialize_value( value ) ); + } + + map.end() + }, + Value::Reference( _ ) => { + let map = try!( serializer.serialize_map( None ) ); + map.end() + } + } + } +} + +impl< 'de > Deserialize< 'de > for Value { + #[inline] + fn deserialize< D: de::Deserializer< 'de > >( deserializer: D ) -> Result< Self, D::Error > { + struct ValueVisitor; + impl< 'de > Visitor< 'de > for ValueVisitor { + type Value = Value; + + fn expecting( &self, formatter: &mut fmt::Formatter ) -> fmt::Result { + formatter.write_str( "a value which is convertible into a JavaScript value" ) + } + + fn visit_bool< E: de::Error >( self, value: bool ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_i8< E: de::Error >( self, value: i8 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_i16< E: de::Error >( self, value: i16 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_i32< E: de::Error >( self, value: i32 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_i64< E: de::Error >( self, value: i64 ) -> Result< Self::Value, E > { + value.try_into().map_err( E::custom ) + } + + fn visit_u8< E: de::Error >( self, value: u8 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_u16< E: de::Error >( self, value: u16 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_u32< E: de::Error >( self, value: u32 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_u64< E: de::Error >( self, value: u64 ) -> Result< Self::Value, E > { + value.try_into().map_err( E::custom ) + } + + fn visit_f32< E: de::Error >( self, value: f32 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_f64< E: de::Error >( self, value: f64 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_char< E: de::Error >( self, value: char ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_str< E: de::Error >( self, value: &str ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_string< E: de::Error >( self, value: String ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_unit< E: de::Error >( self ) -> Result< Self::Value, E > { + Ok( Null.into() ) + } + + fn visit_none< E: de::Error >( self ) -> Result< Self::Value, E > { + Ok( Null.into() ) + } + + fn visit_some< D: de::Deserializer< 'de > >( self, deserializer: D ) -> Result< Self::Value, D::Error > { + deserializer.deserialize_any( self ) + } + + fn visit_seq< V: de::SeqAccess< 'de > >( self, mut visitor: V ) -> Result< Self::Value, V::Error > { + let mut output: Vec< Value > = Vec::with_capacity( visitor.size_hint().unwrap_or( 0 ) ); + while let Some( element ) = visitor.next_element()? { + output.push( element ); + } + + Ok( output.into() ) + } + + fn visit_map< V: de::MapAccess< 'de > >( self, mut visitor: V ) -> Result< Self::Value, V::Error > { + let mut output: BTreeMap< String, Value > = BTreeMap::new(); + while let Some( (key, value) ) = visitor.next_entry()? { + output.insert( key, value ); + } + + Ok( output.into() ) + } + + fn visit_bytes< E: de::Error >( self, value: &[u8] ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_byte_buf< E: de::Error >( self, value: Vec< u8 > ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + // Not really sure how (if?) to implement these at this point: + + // fn visit_newtype_struct< D: de::Deserializer< 'de > >( self, deserializer: D ) -> Result< Self::Value, D::Error > { + // unimplemented!(); + // } + + // fn visit_enum< V: de::EnumAccess >( self, visitor: V ) -> Result< Self::Value, V::Error > { + // unimplemented!(); + // } + } + + deserializer.deserialize_any( ValueVisitor ) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +enum ConversionErrorKind { + InvalidKey, + NumberConversionError( number::ConversionError ), + Custom( String ) +} + +/// A structure denoting a conversion error encountered during +/// serialization or deserialization. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct ConversionError { + kind: ConversionErrorKind +} + +impl ConversionError { + fn invalid_key() -> Self { + ConversionError { + kind: ConversionErrorKind::InvalidKey + } + } +} + +impl fmt::Display for ConversionError { + fn fmt( &self, formatter: &mut fmt::Formatter ) -> Result< (), fmt::Error > { + let message = error::Error::description( self ); + write!( formatter, "{}", message ) + } +} + +impl error::Error for ConversionError { + fn description( &self ) -> &str { + match self.kind { + ConversionErrorKind::InvalidKey => "key must be either a string or an integer", + ConversionErrorKind::NumberConversionError( ref error ) => error.description(), + ConversionErrorKind::Custom( ref message ) => message.as_str() + } + } +} + +impl ser::Error for ConversionError { + fn custom< T: fmt::Display >( message: T ) -> Self { + ConversionError { + kind: ConversionErrorKind::Custom( message.to_string() ) + } + } +} + +impl de::Error for ConversionError { + fn custom< T: fmt::Display >( message: T ) -> Self { + ConversionError { + kind: ConversionErrorKind::Custom( message.to_string() ) + } + } +} + +impl From< number::ConversionError > for ConversionError { + fn from( error: number::ConversionError ) -> Self { + ConversionError { + kind: ConversionErrorKind::NumberConversionError( error ) + } + } +} + +#[derive(Debug)] +pub struct Serializer { +} + +impl Serializer { + pub fn new() -> Self { + Serializer {} + } +} + +impl< 'a > ser::Serializer for &'a mut Serializer { + type Ok = Value; + type Error = ConversionError; + type SerializeSeq = SerializeVec; + type SerializeTuple = SerializeVec; + type SerializeTupleStruct = SerializeVec; + type SerializeTupleVariant = SerializeTupleVariant; + type SerializeMap = SerializeMap; + type SerializeStruct = SerializeMap; + type SerializeStructVariant = SerializeStructVariant; + + fn serialize_bool( self, value: bool ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_i8( self, value: i8 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_i16( self, value: i16 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_i32( self, value: i32 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_i64( self, value: i64 ) -> Result< Self::Ok, Self::Error > { + Ok( value.try_into()? ) + } + + fn serialize_u8( self, value: u8 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_u16( self, value: u16 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_u32( self, value: u32 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_u64( self, value: u64 ) -> Result< Self::Ok, Self::Error > { + Ok( value.try_into()? ) + } + + fn serialize_f32( self, value: f32 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_f64( self, value: f64 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_char( self, value: char ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_str( self, value: &str ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_bytes( self, value: &[u8] ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_none( self ) -> Result< Self::Ok, Self::Error > { + self.serialize_unit() + } + + fn serialize_some< T: ?Sized + Serialize >( self, value: &T ) -> Result< Self::Ok, Self::Error > { + value.serialize( self ) + } + + fn serialize_unit( self ) -> Result< Self::Ok, Self::Error > { + Ok( Null.into() ) + } + + fn serialize_unit_struct( self, _name: &'static str ) -> Result< Self::Ok, Self::Error > { + self.serialize_unit() + } + + fn serialize_unit_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str ) -> Result< Self::Ok, Self::Error > { + self.serialize_str( variant ) + } + + fn serialize_newtype_struct< T: ?Sized + Serialize >( self, _name: &'static str, value: &T ) -> Result< Self::Ok, Self::Error > { + value.serialize( self ) + } + + fn serialize_newtype_variant< T: ?Sized + Serialize >( self, _name: &'static str, _variant_index: u32, variant: &'static str, value: &T ) -> Result< Self::Ok, Self::Error > { + let mut object = BTreeMap::new(); + object.insert( String::from( variant ), to_value( &value )? ); + Ok( Value::Object( object ) ) + } + + fn serialize_seq( self, length: Option< usize > ) -> Result< Self::SerializeSeq, Self::Error > { + Ok( SerializeVec { + elements: Vec::with_capacity( length.unwrap_or( 0 ) ) + }) + } + + fn serialize_tuple( self, length: usize ) -> Result< Self::SerializeTuple, Self::Error > { + self.serialize_seq( Some( length ) ) + } + + fn serialize_tuple_struct( self, _name: &'static str, length: usize ) -> Result< Self::SerializeTupleStruct, Self::Error > { + self.serialize_seq( Some( length ) ) + } + + fn serialize_tuple_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str, length: usize ) -> Result< Self::SerializeTupleVariant, Self::Error > { + Ok( SerializeTupleVariant { + name: String::from( variant ), + elements: Vec::with_capacity( length ), + }) + } + + fn serialize_map( self, _length: Option< usize > ) -> Result< Self::SerializeMap, Self::Error > { + Ok( SerializeMap { + map: BTreeMap::new(), + next_key: None, + }) + } + + fn serialize_struct( self, _name: &'static str, length: usize ) -> Result< Self::SerializeStruct, Self::Error > { + self.serialize_map( Some( length ) ) + } + + fn serialize_struct_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str, _length: usize ) -> Result< Self::SerializeStructVariant, Self::Error > { + Ok( SerializeStructVariant { + name: String::from( variant ), + map: BTreeMap::new(), + }) + } +} + +#[doc(hidden)] +#[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))] +#[inline] +pub fn to_value< T: Serialize >( value: T ) -> Result< Value, ConversionError > { + let mut serializer = Serializer {}; + value.serialize( &mut serializer ) +} + +#[doc(hidden)] +#[inline] +pub fn from_value< 'de, T: Deserialize< 'de > >( value: Value ) -> Result< T, ConversionError > { + Deserialize::deserialize( value ) +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct SerializeVec { + elements: Vec< Value >, +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct SerializeTupleVariant { + name: String, + elements: Vec< Value >, +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct SerializeMap { + map: BTreeMap< String, Value >, + next_key: Option< String >, +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct SerializeStructVariant { + name: String, + map: BTreeMap< String, Value >, +} + +impl ser::SerializeSeq for SerializeVec { + type Ok = Value; + type Error = ConversionError; + + #[inline] + fn serialize_element< T: ?Sized + Serialize >( &mut self, value: &T ) -> Result< (), Self::Error > { + self.elements.push( to_value( &value )? ); + Ok(()) + } + + #[inline] + fn end( self ) -> Result< Self::Ok, Self::Error > { + Ok( self.elements.into() ) + } +} + +impl ser::SerializeTuple for SerializeVec { + type Ok = Value; + type Error = ConversionError; + + #[inline] + fn serialize_element< T: ?Sized + Serialize >( &mut self, value: &T ) -> Result< (), Self::Error > { + ser::SerializeSeq::serialize_element( self, value ) + } + + #[inline] + fn end( self ) -> Result< Self::Ok, Self::Error > { + ser::SerializeSeq::end( self ) + } +} + +impl ser::SerializeTupleStruct for SerializeVec { + type Ok = Value; + type Error = ConversionError; + + fn serialize_field< T: ?Sized + Serialize >( &mut self, value: &T ) -> Result< (), Self::Error > { + ser::SerializeSeq::serialize_element( self, value ) + } + + fn end( self ) -> Result< Self::Ok, Self::Error > { + ser::SerializeSeq::end( self ) + } +} + +impl ser::SerializeTupleVariant for SerializeTupleVariant { + type Ok = Value; + type Error = ConversionError; + + fn serialize_field< T: ?Sized + Serialize >( &mut self, value: &T ) -> Result< (), Self::Error > { + self.elements.push( to_value( &value )? ); + Ok(()) + } + + fn end( self ) -> Result< Self::Ok, Self::Error > { + let mut object: BTreeMap< String, Value > = BTreeMap::new(); + object.insert( self.name, Value::Array( self.elements ) ); + Ok( Value::Object( object ) ) + } +} + +impl ser::SerializeMap for SerializeMap { + type Ok = Value; + type Error = ConversionError; + + fn serialize_key< T: ?Sized + Serialize >( &mut self, key: &T ) -> Result< (), Self::Error > { + match to_value( &key )? { + Value::String( string ) => self.next_key = Some( string ), + Value::Number( number ) => { + if let Ok( value ) = number.try_into() { + let value: u64 = value; + self.next_key = Some( value.to_string() ); + } else if let Ok( value ) = number.try_into() { + let value: i64 = value; + self.next_key = Some( value.to_string() ); + } else { + return Err( ConversionError::invalid_key() ) + } + }, + _ => return Err( ConversionError::invalid_key() ) + } + + Ok(()) + } + + fn serialize_value< T: ?Sized + Serialize >( &mut self, value: &T ) -> Result< (), Self::Error > { + let key = self.next_key.take(); + // Panic because this indicates a bug in the program rather than an + // expected failure. + let key = key.expect( "serialize_value called before serialize_key" ); + self.map.insert( key, to_value( &value )? ); + Ok(()) + } + + fn end( self ) -> Result< Self::Ok, Self::Error > { + Ok( Value::Object( self.map ) ) + } +} + +impl ser::SerializeStruct for SerializeMap { + type Ok = Value; + type Error = ConversionError; + + fn serialize_field< T: ?Sized + Serialize >( &mut self, key: &'static str, value: &T ) -> Result< (), Self::Error > { + ser::SerializeMap::serialize_key( self, key )?; + ser::SerializeMap::serialize_value( self, value ) + } + + fn end( self ) -> Result< Self::Ok, Self::Error > { + ser::SerializeMap::end( self ) + } +} + +impl ser::SerializeStructVariant for SerializeStructVariant { + type Ok = Value; + type Error = ConversionError; + + fn serialize_field< T: ?Sized + Serialize >( &mut self, key: &'static str, value: &T ) -> Result< (), Self::Error > { + self.map.insert( String::from( key ), to_value( &value )? ); + Ok(()) + } + + fn end( self ) -> Result< Self::Ok, Self::Error > { + let mut object = BTreeMap::new(); + object.insert( self.name, Value::Object( self.map ) ); + Ok( Value::Object( object ) ) + } +} + +impl< 'de > de::Deserializer< 'de > for Number { + type Error = ConversionError; + + #[inline] + fn deserialize_any< V: Visitor< 'de > >( self, visitor: V ) -> Result< V::Value, Self::Error > { + // TODO: Consider dispatching the visitor based on the actual value? + match *get_storage( &self ) { + number::Storage::I32( value ) => visitor.visit_i32( value ), + number::Storage::F64( value ) => visitor.visit_f64( value ) + } + } + + forward_to_deserialize_any! { + bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit option + seq bytes byte_buf map unit_struct newtype_struct + tuple_struct struct identifier tuple enum ignored_any + } +} + +// TODO: Impl for `&'a Number` and `&'a mut Number`. + +impl Value { + fn unexpected( &self ) -> de::Unexpected { + match *self { + Value::Undefined => de::Unexpected::Other( "undefined" ), + Value::Null => de::Unexpected::Other( "null" ), + Value::Bool( value ) => de::Unexpected::Bool( value ), + Value::Number( ref value ) => { + match *get_storage( value ) { + number::Storage::I32( value ) => de::Unexpected::Signed( value as i64 ), + number::Storage::F64( value ) => de::Unexpected::Float( value ) + } + }, + Value::String( ref value ) => de::Unexpected::Str( value ), + Value::Array( _ ) => de::Unexpected::Seq, + Value::Object( _ ) => de::Unexpected::Map, + Value::Reference( _ ) => de::Unexpected::Other( "reference to a JavaScript value" ) + } + } +} + +impl< 'de > de::Deserializer< 'de > for Value { + type Error = ConversionError; + + #[inline] + fn deserialize_any< V: Visitor< 'de > >( self, visitor: V ) -> Result< V::Value, Self::Error > { + match self { + Value::Undefined => visitor.visit_unit(), + Value::Null => visitor.visit_unit(), + Value::Bool( value ) => visitor.visit_bool( value ), + Value::Number( value ) => de::Deserializer::deserialize_any( value, visitor ), + Value::String( value ) => visitor.visit_string( value ), + Value::Array( value ) => { + let length = value.len(); + let mut deserializer = SeqDeserializer::new( value ); + let seq = visitor.visit_seq( &mut deserializer )?; + let remaining = deserializer.iter.len(); + if remaining == 0 { + Ok( seq ) + } else { + Err( de::Error::invalid_length( length, &"fewer elements in the array" ) ) + } + }, + Value::Object( value ) => { + let length = value.len(); + let mut deserializer = MapDeserializer::new( value ); + let map = visitor.visit_map( &mut deserializer )?; + let remaining = deserializer.iter.len(); + if remaining == 0 { + Ok( map ) + } else { + Err( de::Error::invalid_length( length, &"fewer elements in the object" ) ) + } + }, + Value::Reference( _ ) => { + unimplemented!(); // TODO: ? + } + } + } + + #[inline] + fn deserialize_option< V: Visitor< 'de > >( self, visitor: V ) -> Result< V::Value, Self::Error > { + match self { + Value::Undefined => visitor.visit_none(), + Value::Null => visitor.visit_none(), + _ => visitor.visit_some( self ) + } + } + + #[inline] + fn deserialize_enum< V: Visitor< 'de > >( self, _name: &str, _variants: &'static [&'static str], visitor: V ) -> Result< V::Value, Self::Error > { + let (variant, value) = match self { + Value::Object( value ) => { + let mut iter = value.into_iter(); + let (variant, value) = match iter.next() { + Some( value ) => value, + None => { + return Err( de::Error::invalid_value( de::Unexpected::Map, &"map with a single key" ) ); + } + }; + + // Enums are encoded as objects with a single key:value pair. + if iter.next().is_some() { + return Err( de::Error::invalid_value( de::Unexpected::Map, &"map with a single key" ) ); + } + + (variant, Some( value )) + }, + Value::String( variant ) => (variant, None), + other => { + return Err( de::Error::invalid_type( other.unexpected(), &"string or map" ) ); + } + }; + + visitor.visit_enum( EnumDeserializer { + variant: variant, + value: value, + }) + } + + #[inline] + fn deserialize_newtype_struct< V: Visitor< 'de > >( self, _name: &'static str, visitor: V ) -> Result< V::Value, Self::Error > { + visitor.visit_newtype_struct( self ) + } + + forward_to_deserialize_any! { + bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit seq + bytes byte_buf map unit_struct tuple_struct struct + identifier tuple ignored_any + } +} + +struct EnumDeserializer { + variant: String, + value: Option< Value >, +} + +impl< 'de > de::EnumAccess< 'de > for EnumDeserializer { + type Error = ConversionError; + type Variant = VariantDeserializer; + + fn variant_seed< V: de::DeserializeSeed< 'de > >( self, seed: V ) -> Result< (V::Value, VariantDeserializer), Self::Error > { + let variant = self.variant.into_deserializer(); + let visitor = VariantDeserializer { value: self.value }; + seed.deserialize( variant ).map( |v| (v, visitor) ) + } +} + +struct VariantDeserializer { + value: Option< Value >, +} + +impl< 'de > de::VariantAccess< 'de > for VariantDeserializer { + type Error = ConversionError; + + fn unit_variant( self ) -> Result< (), Self::Error > { + match self.value { + Some( value ) => de::Deserialize::deserialize( value ), + None => Ok(()), + } + } + + fn newtype_variant_seed< T: de::DeserializeSeed< 'de > >( self, seed: T ) -> Result< T::Value, Self::Error > { + match self.value { + Some( value ) => seed.deserialize( value ), + None => Err( de::Error::invalid_type( de::Unexpected::UnitVariant, &"newtype variant" ) ), + } + } + + fn tuple_variant< V: Visitor< 'de > >( self, _length: usize, visitor: V ) -> Result< V::Value, Self::Error > { + match self.value { + Some( Value::Array( value ) ) => { + de::Deserializer::deserialize_any( SeqDeserializer::new( value ), visitor ) + }, + Some( other ) => Err( de::Error::invalid_type( other.unexpected(), &"tuple variant" ) ), + None => Err( de::Error::invalid_type( de::Unexpected::UnitVariant, &"tuple variant" ) ) + } + } + + fn struct_variant< V: Visitor< 'de > >( self, _fields: &'static [&'static str], visitor: V ) -> Result< V::Value, Self::Error > { + match self.value { + Some( Value::Object( value ) ) => { + de::Deserializer::deserialize_any( MapDeserializer::new( value ), visitor ) + }, + Some( other ) => Err( de::Error::invalid_type( other.unexpected(), &"struct variant" ) ), + _ => Err( de::Error::invalid_type( de::Unexpected::UnitVariant, &"struct variant" ) ) + } + } +} + +struct SeqDeserializer { + iter: vec::IntoIter< Value >, +} + +impl SeqDeserializer { + fn new( vec: Vec< Value >) -> Self { + SeqDeserializer { + iter: vec.into_iter(), + } + } +} + +impl< 'de > de::Deserializer< 'de > for SeqDeserializer { + type Error = ConversionError; + + #[inline] + fn deserialize_any< V: Visitor< 'de > >( mut self, visitor: V ) -> Result< V::Value, Self::Error > { + let length = self.iter.len(); + if length == 0 { + visitor.visit_unit() + } else { + let ret = visitor.visit_seq( &mut self )?; + let remaining = self.iter.len(); + if remaining == 0 { + Ok( ret ) + } else { + Err( de::Error::invalid_length( length, &"fewer elements in array" ) ) + } + } + } + + forward_to_deserialize_any! { + bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit option + seq bytes byte_buf map unit_struct newtype_struct + tuple_struct struct identifier tuple enum ignored_any + } +} + +impl< 'de > de::SeqAccess< 'de > for SeqDeserializer { + type Error = ConversionError; + + fn next_element_seed< T: de::DeserializeSeed< 'de > >( &mut self, seed: T ) -> Result< Option< T::Value >, Self::Error > { + match self.iter.next() { + Some( value ) => seed.deserialize( value ).map( Some ), + None => Ok( None ), + } + } + + fn size_hint( &self ) -> Option< usize > { + match self.iter.size_hint() { + (lower, Some( upper )) if lower == upper => Some( upper ), + _ => None + } + } +} + +struct MapDeserializer { + iter: as IntoIterator>::IntoIter, + value: Option< Value >, +} + +impl MapDeserializer { + fn new( map: BTreeMap< String, Value > ) -> Self { + MapDeserializer { + iter: map.into_iter(), + value: None, + } + } +} + +impl< 'de > de::MapAccess< 'de > for MapDeserializer { + type Error = ConversionError; + + fn next_key_seed< T: de::DeserializeSeed< 'de > >( &mut self, seed: T ) -> Result< Option< T::Value >, Self::Error> { + match self.iter.next() { + Some( (key, value) ) => { + self.value = Some( value ); + seed.deserialize( key.into_deserializer() ).map( Some ) + } + None => Ok( None ) + } + } + + fn next_value_seed< T: de::DeserializeSeed< 'de > >( &mut self, seed: T ) -> Result< T::Value, Self::Error > { + match self.value.take() { + Some( value ) => seed.deserialize( value ), + None => Err( de::Error::custom( "value is missing" ) ), + } + } + + fn size_hint( &self ) -> Option< usize > { + match self.iter.size_hint() { + (lower, Some( upper )) if lower == upper => Some( upper ), + _ => None + } + } +} + +impl< 'de > de::Deserializer< 'de > for MapDeserializer { + type Error = ConversionError; + + #[inline] + fn deserialize_any< V: Visitor< 'de > >( self, visitor: V ) -> Result< V::Value, Self::Error > { + visitor.visit_map( self ) + } + + forward_to_deserialize_any! { + bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit option + seq bytes byte_buf map unit_struct newtype_struct + tuple_struct struct identifier tuple enum ignored_any + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __js_serializable_serde_boilerplate { + (($($impl_arg:tt)*) ($($kind_arg:tt)*) ($($bounds:tt)*)) => { + __js_serializable_boilerplate!( ($($impl_arg)*) ($($kind_arg)*) ($($bounds)*) ); + + impl< $($impl_arg),* > $crate::private::JsSerializable for $($kind_arg)* where $($bounds)* { + #[inline] + fn into_js< 'x >( &'x self, arena: &'x $crate::private::PreallocatedArena ) -> $crate::private::SerializedValue< 'x > { + let value = $crate::private::to_value( self ).unwrap(); + let value = arena.save( value ); + $crate::private::JsSerializable::into_js( value, arena ) + } + + #[inline] + fn memory_required( &self ) -> usize { + // TODO: This is very inefficient. The actual conversion into + // the Value should be only done once. + let value = to_value( self ).unwrap(); + $crate::private::JsSerializable::memory_required( &value ) + } + } + + impl< $($impl_arg),* > $crate::unstable::TryFrom< $($kind_arg)* > for $crate::Value where $($bounds)* { + type Error = $crate::serde::ConversionError; + #[inline] + fn try_from( value: $($kind_arg)* ) -> Result< Self, Self::Error > { + $crate::private::to_value( value ) + } + } + + impl< '_a, $($impl_arg),* > $crate::unstable::TryFrom< &'_a $($kind_arg)* > for $crate::Value where $($bounds)* { + type Error = $crate::serde::ConversionError; + #[inline] + fn try_from( value: &'_a $($kind_arg)* ) -> Result< Self, Self::Error > { + $crate::private::to_value( value ) + } + } + + impl< '_a, $($impl_arg),* > $crate::unstable::TryFrom< &'_a mut $($kind_arg)* > for $crate::Value where $($bounds)* { + type Error = $crate::serde::ConversionError; + #[inline] + fn try_from( value: &'_a mut $($kind_arg)* ) -> Result< Self, Self::Error > { + $crate::private::to_value( value ) + } + } + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __js_deserializable_serde_boilerplate { + (($($impl_arg:tt)*) ($($kind_arg:tt)*) ($($bounds:tt)*)) => { + impl< $($impl_arg),* > TryFrom< $crate::Value > for $($kind_arg)* where $($bounds)* { + type Error = $crate::serde::ConversionError; + #[inline] + fn try_from( value: $crate::Value ) -> Result< Self, Self::Error > { + $crate::private::from_value( value ) + } + } + } +} + +/// A macro which makes it possible to pass an instance of a given type +/// implementing Serde's `Serialize` into the [js!](macro.js.html) macro. +/// +/// For types defined outside of your crate you can also use the [Serde](struct.Serde.html) +/// newtype to make them serializable indirectly. +/// +/// # Examples +/// +/// ``` +/// #[derive(Serialize, Debug)] +/// struct Person { +/// name: String, +/// age: i32 +/// } +/// +/// js_serializable!( Person ); +/// +/// let person = Person { +/// name: "Bob".to_owned(), +/// age: 33 +/// }; +/// +/// js! { +/// var person = @{person}; +/// console.log( person.name + " is " + person.age + " years old." ); +/// }; +/// ``` +/// +/// This macro also accepts generics: +/// +/// ``` +/// trait Foobar {} +/// +/// #[derive(Serialize)] +/// struct Wrapper< 'a, T: Serialize + 'a >( &'a T ); +/// +/// js_serializable!( impl< 'a, T > for Wrapper< 'a, T > where T: Serialize + Foobar ); +/// ``` +#[macro_export] +macro_rules! js_serializable { + ($kind:tt) => { + __js_serializable_serde_boilerplate!( () ($kind) () ); + }; + + (impl< $($impl_arg:tt),* > for $kind:ty where $($bounds:tt)*) => { + __js_serializable_serde_boilerplate!( ($($impl_arg),*) ($kind) ($($bounds)*) ); + }; + + (impl< $($impl_arg:tt),* > for $kind:ty) => { + __js_serializable_serde_boilerplate!( ($($impl_arg),*) ($kind) () ); + }; +} + +/// A macro which makes it possible to convert an instance of a given type +/// implementing Serde's `Deserialize` into a [Value](enum.Value.html) using +/// [TryInto](unstable/trait.TryInto.html). +/// +/// For types defined outside of your crate you can also use the [Serde](serde/struct.Serde.html) +/// newtype to make them deserializable indirectly. +/// +/// # Examples +/// +/// ``` +/// #[derive(Deserialize, Debug)] +/// struct Person { +/// name: String, +/// age: i32 +/// } +/// +/// js_deserializable!( Person ); +/// +/// let value = js! { +/// return { +/// number: 123, +/// string: "Hello!" +/// }; +/// }; +/// +/// let structure: StructureSerializable = value.try_into().unwrap(); +/// assert_eq!( structure.number, 123 ); +/// assert_eq!( structure.string, "Hello!" ); +/// ``` +/// +/// This macro also accepts generics just as the [js_serializable!](macro.js_serializable.html) does. +#[macro_export] +macro_rules! js_deserializable { + ($kind:tt) => { + __js_deserializable_serde_boilerplate!( () ($kind) () ); + }; + + (impl< $($impl_arg:tt),* > for $kind:ty where $($bounds:tt)*) => { + __js_deserializable_serde_boilerplate!( ($($impl_arg),*) ($kind) ($($bounds)*) ); + }; + + (impl< $($impl_arg:tt),* > for $kind:ty) => { + __js_deserializable_serde_boilerplate!( ($($impl_arg),*) ($kind) () ); + }; +} + +/// A newtype which makes it possible to pass a value which implements +/// Serde's `Serializable` into the [js!](macro.js.html) macro. +/// +/// For types defined in your crate you can also use the [js_serializable!](macro.js_serializable.html) +/// macro to make them serializable directly. +/// +/// # Examples +/// +/// ``` +/// #[derive(Serialize, Debug)] +/// struct Person { +/// name: String, +/// age: i32 +/// } +/// +/// let person = Person { +/// name: "Bob".to_owned(), +/// age: 33 +/// }; +/// +/// js! { +/// var person = @{Serde( person )}; +/// console.log( person.name + " is " + person.age + " years old." ); +/// }; +/// ``` +pub struct Serde< T >( pub T ); + +impl< T: fmt::Debug > fmt::Debug for Serde< T > { + #[inline] + fn fmt( &self, formatter: &mut fmt::Formatter ) -> Result< (), fmt::Error > { + self.0.fmt( formatter ) + } +} + +impl< T: Serialize > JsSerializable for Serde< T > { + #[inline] + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a > { + let value = to_value( &self.0 ).unwrap(); + let value = arena.save( value ); + value.into_js( arena ) + } + + #[inline] + fn memory_required( &self ) -> usize { + let value = to_value( &self.0 ).unwrap(); + value.memory_required() + } +} + +impl< T: Serialize > TryFrom< Serde< T > > for Value { + type Error = ConversionError; + #[inline] + fn try_from( value: Serde< T > ) -> Result< Self, Self::Error > { + to_value( &value.0 ) + } +} + +impl< 'a, T: Serialize > TryFrom< &'a Serde< T > > for Value { + type Error = ConversionError; + #[inline] + fn try_from( value: &'a Serde< T > ) -> Result< Self, Self::Error > { + to_value( &value.0 ) + } +} + +impl< 'a, T: Serialize > TryFrom< &'a mut Serde< T > > for Value { + type Error = ConversionError; + #[inline] + fn try_from( value: &'a mut Serde< T > ) -> Result< Self, Self::Error > { + to_value( &value.0 ) + } +} + +impl< 'de, T: Deserialize< 'de > > TryFrom< Value > for Serde< T > { + type Error = ConversionError; + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + Ok( Serde( from_value( value )? ) ) + } +} + +__js_serializable_boilerplate!( impl< T > for Serde< T > where T: Serialize ); + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + + #[test] + fn serialize_undefined() { + // This is technically incorrect as `undefined` is not serializable into JSON, + // but serde is generic so it serialized it anyway. + assert_eq!( serde_json::to_string( &Undefined ).unwrap(), "null" ); + } + + #[test] + fn serialize_null() { + assert_eq!( serde_json::to_string( &Null ).unwrap(), "null" ); + } + + #[test] + fn serialize_number_negative() { + let value: Number = (-123_i32).into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "-123" ); + } + + #[test] + fn serialize_number_positive() { + let value: Number = 123_i32.into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "123" ); + } + + #[test] + fn serialize_number_float() { + let value: Number = 3.33_f64.into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "3.33" ); + } + + #[test] + fn serialize_value_undefined() { + let value: Value = Undefined.into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "null" ); + } + + #[test] + fn serialize_value_null() { + let value: Value = Null.into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "null" ); + } + + #[test] + fn serialize_value_bool_true() { + let value: Value = true.into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "true" ); + } + + #[test] + fn serialize_value_bool_false() { + let value: Value = false.into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "false" ); + } + + #[test] + fn serialize_value_number() { + let value: Value = (123_i32).into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "123" ); + } + + #[test] + fn serialize_value_string() { + let value: Value = "死神はりんごしか食べない".into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "\"死神はりんごしか食べない\"" ); + } + + #[test] + fn serialize_value_array() { + let value: Value = (&[true, false][..]).into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "[true,false]" ); + } + + #[test] + fn serialize_value_object() { + use std::collections::BTreeMap; + let mut map = BTreeMap::new(); + map.insert( "1", "one" ); + map.insert( "2", "two" ); + + let value: Value = map.into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "{\"1\":\"one\",\"2\":\"two\"}" ); + } + + #[test] + fn deserialize_value_null() { + let value: Value = serde_json::from_str( "null" ).unwrap(); + assert_eq!( value, Value::Null ); + } + + #[test] + fn deserialize_value_bool_false() { + let value: Value = serde_json::from_str( "false" ).unwrap(); + assert_eq!( value, Value::Bool( false ) ); + } + + #[test] + fn deserialize_value_bool_true() { + let value: Value = serde_json::from_str( "true" ).unwrap(); + assert_eq!( value, Value::Bool( true ) ); + } + + #[test] + fn deserialize_value_number_integer() { + let value: Value = serde_json::from_str( "33" ).unwrap(); + assert_eq!( value, Value::Number( 33.into() ) ); + } + + #[test] + fn deserialize_value_number_float() { + let value: Value = serde_json::from_str( "33.33" ).unwrap(); + assert_eq!( value, Value::Number( 33.33.into() ) ); + } + + #[test] + fn deserialize_value_string() { + let value: Value = serde_json::from_str( "\"Bob\"" ).unwrap(); + assert_eq!( value, Value::String( "Bob".to_owned() ) ); + } + + #[test] + fn deserialize_value_array() { + let value: Value = serde_json::from_str( "[true, false]" ).unwrap(); + assert_eq!( value, Value::Array( vec![ Value::Bool( true ), Value::Bool( false ) ] ) ); + } + + #[test] + fn deserialize_value_object() { + let value: Value = serde_json::from_str( "{\"1\":\"one\",\"2\":\"two\"}" ).unwrap(); + let mut map: BTreeMap< String, Value > = BTreeMap::new(); + map.insert( "1".to_owned(), Value::String( "one".to_owned() ) ); + map.insert( "2".to_owned(), Value::String( "two".to_owned() ) ); + assert_eq!( value, Value::Object( map ) ); + } + + #[derive(Serialize, Deserialize, Debug)] + struct Structure { + number: i32, + string: String + } + + #[derive(Serialize, Deserialize, Debug)] + struct StructureSerializable { + number: i32, + string: String + } + + js_serializable!( StructureSerializable ); + js_deserializable!( StructureSerializable ); + + #[test] + fn serialization_into_value_through_macro() { + let structure = StructureSerializable { + number: 123, + string: "Hello!".to_owned() + }; + + let value: Value = structure.try_into().unwrap(); + let mut map = BTreeMap::new(); + map.insert( "number".to_owned(), Value::Number( 123.into() ) ); + map.insert( "string".to_owned(), Value::String( "Hello!".to_owned() ) ); + assert_eq!( value, Value::Object( map ) ); + } + + #[test] + fn serialization_into_javascript_through_macro() { + let structure = StructureSerializable { + number: 123, + string: "Hello!".to_owned() + }; + + let result = js! { + var object = @{structure}; + return object.number === 123 && object.string === "Hello!" && Object.keys( object ).length == 2; + }; + + assert_eq!( result, true ); + } + + #[test] + fn serialization_into_value_through_newtype() { + let structure = Structure { + number: 123, + string: "Hello!".to_owned() + }; + + let value: Value = Serde( structure ).try_into().unwrap(); + let mut map = BTreeMap::new(); + map.insert( "number".to_owned(), Value::Number( 123.into() ) ); + map.insert( "string".to_owned(), Value::String( "Hello!".to_owned() ) ); + assert_eq!( value, Value::Object( map ) ); + } + + #[test] + fn serialization_into_javascript_through_newtype() { + let structure = Structure { + number: 123, + string: "Hello!".to_owned() + }; + + let result = js! { + var object = @{Serde( structure )}; + return object.number === 123 && object.string === "Hello!" && Object.keys( object ).length == 2; + }; + + assert_eq!( result, true ); + } + + #[test] + fn deserialization_into_value_through_macro() { + let value = js! { + return { + number: 123, + string: "Hello!" + }; + }; + + let structure: StructureSerializable = value.try_into().unwrap(); + assert_eq!( structure.number, 123 ); + assert_eq!( structure.string, "Hello!" ); + } + + #[test] + fn deserialization_into_value_through_newtype() { + let value = js! { + return { + number: 123, + string: "Hello!" + }; + }; + + let structure: Serde< Structure > = value.try_into().unwrap(); + assert_eq!( structure.0.number, 123 ); + assert_eq!( structure.0.string, "Hello!" ); + } +} diff --git a/src/ecosystem/serde_json.rs b/src/ecosystem/serde_json.rs new file mode 100644 index 00000000..d66b21b3 --- /dev/null +++ b/src/ecosystem/serde_json.rs @@ -0,0 +1,47 @@ +use std::collections::BTreeMap; +use serde_json::value::Value as JsonValue; +use webcore::value::Value; +use webcore::try_from::{TryFrom, TryInto}; +use webcore::number::ConversionError; + +impl TryFrom< JsonValue > for Value { + type Error = ConversionError; + + #[inline] + fn try_from( value: JsonValue ) -> Result< Self, Self::Error > { + let result = match value { + JsonValue::Null => Value::Null, + JsonValue::Bool( value ) => Value::Bool( value ), + JsonValue::Number( value ) => { + if let Some( value ) = value.as_u64() { + Value::Number( value.try_into()? ) + } else if let Some( value ) = value.as_i64() { + Value::Number( value.try_into()? ) + } else { + Value::Number( value.as_f64().unwrap().into() ) + } + }, + JsonValue::String( value ) => Value::String( value ), + JsonValue::Array( value ) => { + let mut vector = Vec::new(); + + vector.reserve( value.len() ); + for element in value.into_iter() { + vector.push( element.try_into()? ); + } + + Value::Array( vector ) + }, + JsonValue::Object( value ) => { + let mut map = BTreeMap::new(); + for (key, value) in value.into_iter() { + map.insert( key.into(), value.try_into()? ); + } + + Value::Object( map ) + } + }; + + Ok( result ) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..d47a00ad --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,209 @@ +//! The goal of this crate is to provide Rust bindings to the Web APIs and to allow +//! a high degree of interoperability between Rust and JavaScript. +//! +//! ## Examples +//! +//! You can directly embed JavaScript code into Rust: +//! +//! ```rust +//! let message = "Hello, 世界!"; +//! let result = js! { +//! alert!( @{message} ); +//! return 2 + 2 * 2; +//! }; +//! +//! println!( "2 + 2 * 2 = {:?}", result ); +//! ``` +//! +//! Even closures are are supported: +//! +//! ```rust +//! let print_hello = |name: String| { +//! println!( "Hello, {}!", name ); +//! }; +//! +//! js! { +//! var print_hello = @{print_hello}; +//! print_hello( "Bob" ); +//! print_hello.drop(); // Necessary to clean up the closure on Rust's side. +//! } +//! ``` +//! +//! You can also pass arbitrary structures thanks to [serde]: +//! +//! ```rust +//! #[derive(Serialize)] +//! struct Person { +//! name: String, +//! age: i32 +//! } +//! +//! js_serializable!( Person ); +//! +//! js! { +//! var person = @{person}; +//! console.log( person.name + " is " + person.age + " years old." ); +//! }; +//! ``` +//! +//! [serde]: https://serde.rs/ +//! +//! This crate also exposes a number of Web APIs, for example: +//! +//! ```rust +//! let button = document().query_selector( "#hide-button" ).unwrap(); +//! button.add_event_listener( move |_: ClickEvent| { +//! for anchor in document().query_selector_all( "#main a" ) { +//! js!( @{anchor}.style = "display: none;"; ); +//! } +//! }); +//! ``` + +#![deny( + missing_docs, + missing_debug_implementations, + trivial_numeric_casts, + unstable_features, + unused_import_braces, + unused_qualifications +)] +#![cfg_attr(feature = "dev", allow(unstable_features))] +#![cfg_attr(feature = "dev", feature(plugin))] +#![cfg_attr(feature = "dev", plugin(clippy))] +#![recursion_limit="750"] + +#[cfg(feature = "serde")] +#[macro_use] +extern crate serde as serde_crate; + +#[cfg(any(test, feature = "serde_json"))] +extern crate serde_json; + +#[cfg(all(test, feature = "serde"))] +#[macro_use] +extern crate serde_derive; + +#[macro_use] +mod webcore; +mod webapi; +mod ecosystem; + +pub use webcore::initialization::{ + initialize, + event_loop +}; +pub use webcore::value::{ + Undefined, + Null, + Value, + Reference +}; +pub use webcore::number::Number; + +#[cfg(feature = "serde")] +/// A module with serde-related APIs. +pub mod serde { + pub use ecosystem::serde::{ + ConversionError, + Serde + }; +} + +/// A module with bindings to the Web APIs. +pub mod web { + pub use webapi::window::{ + Window, + window + }; + pub use webapi::document::{ + Document, + document + }; + pub use webapi::global::{ + set_timeout, + alert + }; + pub use webapi::date::Date; + pub use webapi::event_target::{IEventTarget, EventTarget}; + pub use webapi::node::{INode, Node, CloneKind}; + pub use webapi::element::{IElement, Element}; + pub use webapi::html_element::{IHtmlElement, HtmlElement}; + pub use webapi::window_or_worker::IWindowOrWorker; + pub use webapi::token_list::TokenList; + pub use webapi::node_list::NodeList; + pub use webapi::string_map::StringMap; + pub use webapi::storage::Storage; + pub use webapi::location::Location; + + /// A module containing error types. + pub mod error { + pub use webapi::node::NotFoundError; + } + + /// A module containing HTML DOM elements. + pub mod html_element { + pub use webapi::html_elements::InputElement; + } + + /// A module containing JavaScript DOM events. + pub mod event { + pub use webapi::event::{ + IEvent, + IKeyboardEvent, + IUiEvent, + IMouseEvent, + IFocusEvent, + + ChangeEvent, + KeypressEvent, + ClickEvent, + DoubleClickEvent, + FocusEvent, + BlurEvent, + HashChangeEvent + }; + } +} + +/// A module containing stable counterparts to currently +/// unstable Rust features. +pub mod unstable { + pub use webcore::try_from::{ + TryFrom, + TryInto + }; + + pub use webcore::void::Void; +} + +#[doc(hidden)] +pub mod private { + pub use webcore::ffi::emscripten_asm_const_int; + pub use webcore::serialization::{ + JsSerializable, + JsSerializableOwned, + PreallocatedArena, + SerializedValue + }; + + pub use webcore::newtype::{ + IntoNewtype, + Newtype + }; + + pub use webcore::value::{ + FromReference, + FromReferenceUnchecked + }; + + #[cfg(feature = "serde")] + pub use ecosystem::serde::{ + to_value, + from_value + }; + + // This is to prevent an unused_mut warnings in macros, because an `allow` doesn't work apparently? + #[allow(dead_code)] + #[inline(always)] + pub fn noop< T >( _: &mut T ) {} +} diff --git a/src/webapi/date.rs b/src/webapi/date.rs new file mode 100644 index 00000000..0d9c795a --- /dev/null +++ b/src/webapi/date.rs @@ -0,0 +1,22 @@ +use std::marker::PhantomData; + +/// [(JavaScript docs)](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Date) +#[derive(Debug)] +pub struct Date { + dummy: PhantomData< () > +} + +impl Date { + /// The Date.now() method returns the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now) + pub fn now() -> f64 { + em_asm_double!( "return Date.now();" ) + } +} + +#[test] +fn test_date_now() { + let now = Date::now(); + assert!( now > 0.0 ); +} diff --git a/src/webapi/document.rs b/src/webapi/document.rs new file mode 100644 index 00000000..84d9ceae --- /dev/null +++ b/src/webapi/document.rs @@ -0,0 +1,97 @@ +use webcore::value::Reference; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::node::{INode, Node}; +use webapi::element::Element; +use webapi::text_node::TextNode; +use webapi::node_list::NodeList; +use webapi::location::Location; + +/// The `Document` interface represents any web page loaded in the browser and +/// serves as an entry point into the web page's content, which is the DOM tree. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document) +pub struct Document( Reference ); + +impl IEventTarget for Document {} +impl INode for Document {} + +reference_boilerplate! { + Document, + instanceof Document + convertible to EventTarget + convertible to Node +} + +/// A global instance of [Document](struct.Document.html). +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document) +pub fn document() -> Document { + unsafe { js!( return document; ).into_reference_unchecked() }.unwrap() +} + +impl Document { + /// Returns the first [Element](struct.Element.html) within the document that matches the specified selector, or group of selectors. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) + pub fn query_selector( &self, selector: &str ) -> Option< Element > { + // TODO: This can throw an exception in case of an invalid selector; + // convert the return type to a Result. + unsafe { + js!( return @{self}.querySelector( @{selector} ); ).into_reference_unchecked() + } + } + + /// Returns a list of the elements within the document (using depth-first + /// pre-order traversal of the document's nodes) that match the + /// specified group of selectors. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll) + pub fn query_selector_all( &self, selector: &str ) -> NodeList { + unsafe { + js!( return @{self}.querySelectorAll( @{selector} ); ).into_reference_unchecked().unwrap() + } + } + + /// Returns a reference to the element by its ID; the ID is a string which can + /// be used to uniquely identify the element, found in the HTML `id` attribute. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById) + pub fn get_element_by_id( &self, id: &str ) -> Option< Element > { + unsafe { + js!( return @{self}.getElementById( @{id} ); ).into_reference_unchecked() + } + } + + /// In an HTML document, the Document.createElement() method creates the HTML + /// element specified by `tag`, or an HTMLUnknownElement if `tag` isn't + /// recognized. In other documents, it creates an element with a null namespace URI. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) + pub fn create_element( &self, tag: &str ) -> Element { + unsafe { + js!( return @{self}.createElement( @{tag} ); ).into_reference_unchecked().unwrap() + } + } + + /// Creates a new text node. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTextNode) + pub fn create_text_node( &self, text: &str ) -> TextNode { + unsafe { + js!( return @{self}.createTextNode( @{text} ); ).into_reference_unchecked().unwrap() + } + } + + /// Returns a [Location](struct.Location.html) object which contains + /// information about the URL of the document and provides methods + /// for changing that URL and loading another URL. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document/location) + pub fn location( &self ) -> Option< Location > { + unsafe { + js!( + return @{self}.location; + ).into_reference_unchecked() + } + } +} diff --git a/src/webapi/element.rs b/src/webapi/element.rs new file mode 100644 index 00000000..fb8416ca --- /dev/null +++ b/src/webapi/element.rs @@ -0,0 +1,62 @@ +use webcore::value::Reference; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::node::{INode, Node}; +use webapi::token_list::TokenList; +use webapi::node_list::NodeList; + +/// The `IElement` interface represents an object of a [Document](struct.Document.html). +/// This interface describes methods and properties common to all +/// kinds of elements. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Element) +pub trait IElement: IEventTarget { + /// The Element.classList is a read-only property which returns a live + /// [TokenList](struct.TokenList.html) collection of the class attributes + /// of the element. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) + fn class_list( &self ) -> TokenList { + unsafe { + js!( return @{self.as_ref()}.classList; ).into_reference_unchecked().unwrap() + } + } + + /// Returns the first element that is a descendant of the element on which it is + /// invoked that matches the specified group of selectors. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelector) + fn query_selector( &self, selector: &str ) -> Option< Element > { + // TODO: This can throw an exception in case of an invalid selector; + // convert the return type to a Result. + unsafe { + js!( return @{self.as_ref()}.querySelector( @{selector} ); ).into_reference_unchecked() + } + } + + /// Returns a non-live [NodeList](struct.NodeList.html) of all elements descended + /// from the element on which it is invoked that matches the specified group of CSS selectors. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll) + fn query_selector_all( &self, selector: &str ) -> NodeList { + unsafe { + js!( return @{self.as_ref()}.querySelectorAll( @{selector} ); ).into_reference_unchecked().unwrap() + } + } +} + +/// A reference to a JavaScript object which implements the [IElement](trait.IElement.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Element) +pub struct Element( Reference ); + +impl IEventTarget for Element {} +impl INode for Element {} +impl IElement for Element {} + +reference_boilerplate! { + Element, + instanceof Element + convertible to EventTarget + convertible to Node +} diff --git a/src/webapi/event.rs b/src/webapi/event.rs new file mode 100644 index 00000000..3f05dceb --- /dev/null +++ b/src/webapi/event.rs @@ -0,0 +1,307 @@ +use webcore::value::{Reference, Value}; +use webcore::try_from::{TryFrom, TryInto}; + +/// The `IEvent` interface represents any event which takes place in the DOM; some +/// are user-generated (such as mouse or keyboard events), while others are +/// generated by APIs (such as events that indicate an animation has finished +/// running, a video has been paused, and so forth). There are many types of event, +/// some of which use other interfaces based on the main `IEvent` interface. `IEvent` +/// itself contains the properties and methods which are common to all events. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Event) +pub trait IEvent: AsRef< Reference > + TryFrom< Value > { + /// Returns a string containing the type of event. It is set when + /// the event is constructed and is the name commonly used to refer + /// to the specific event. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Event/type) + fn event_type( &self ) -> String { + js!( return @{self.as_ref()}.type; ).try_into().unwrap() + } + + /// Cancels the event if it is cancelable, without + /// stopping further propagation of the event. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault) + fn prevent_default( &self ) { + js! { @(no_return) + @{self.as_ref()}.preventDefault(); + } + } +} + +pub trait ConcreteEvent: IEvent { + // TODO: Switch to an associated constant for `event_type` once they stabilize. + + /// Returns a string representing the event type. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Event/type) + fn static_event_type() -> &'static str; +} + +/// A reference to a JavaScript object which implements the [IEvent](trait.IEvent.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Event) +pub struct Event( Reference ); + +impl IEvent for Event {} + +reference_boilerplate! { + Event, + instanceof Event +} + +/// The `ChangeEvent` is fired for input, select, and textarea +/// elements when a change to the element's value is committed +/// by the user. Unlike the input event, the change event is not +/// necessarily fired for each change to an element's value. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/change) +pub struct ChangeEvent( Reference ); + +impl IEvent for ChangeEvent {} +impl ConcreteEvent for ChangeEvent { + #[inline] + fn static_event_type() -> &'static str { + "change" + } +} + +reference_boilerplate! { + ChangeEvent, + instanceof Event + convertible to Event +} + +/// The `IUiEvent` interface represents simple user interface events. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent) +pub trait IUiEvent: IEvent { +} + +/// A reference to a JavaScript object which implements the [IUiEvent](trait.IUiEvent.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent) +pub struct UiEvent( Reference ); + +impl IEvent for UiEvent {} +impl IUiEvent for UiEvent {} + +reference_boilerplate! { + UiEvent, + instanceof UiEvent + convertible to Event +} + +/// The `IMouseEvent` interface represents events that occur due to the user +/// interacting with a pointing device (such as a mouse). +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) +pub trait IMouseEvent: IUiEvent { +} + +/// A reference to a JavaScript object which implements the [IMouseEvent](trait.IMouseEvent.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) +pub struct MouseEvent( Reference ); + +impl IEvent for MouseEvent {} +impl IUiEvent for MouseEvent {} +impl IMouseEvent for MouseEvent {} + +reference_boilerplate! { + MouseEvent, + instanceof MouseEvent + convertible to Event + convertible to UiEvent +} + +/// The `ClickEvent` is fired when a pointing device button (usually a +/// mouse's primary button) is pressed and released on a single element. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/click) +pub struct ClickEvent( Reference ); + +impl IEvent for ClickEvent {} +impl IUiEvent for ClickEvent {} +impl IMouseEvent for ClickEvent {} +impl ConcreteEvent for ClickEvent { + #[inline] + fn static_event_type() -> &'static str { + "click" + } +} + +reference_boilerplate! { + ClickEvent, + instanceof MouseEvent + convertible to Event + convertible to UiEvent + convertible to MouseEvent +} + +/// The `DoubleClickEvent` is fired when a pointing device button +/// (usually a mouse's primary button) is clicked twice on a single +/// element. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/dblclick) +pub struct DoubleClickEvent( Reference ); + +impl IEvent for DoubleClickEvent {} +impl IUiEvent for DoubleClickEvent {} +impl IMouseEvent for DoubleClickEvent {} +impl ConcreteEvent for DoubleClickEvent { + #[inline] + fn static_event_type() -> &'static str { + "dblclick" + } +} + +reference_boilerplate! { + DoubleClickEvent, + instanceof MouseEvent + convertible to Event + convertible to UiEvent + convertible to MouseEvent +} + +/// `IKeyboardEvent` objects describe a user interaction with the +/// keyboard. Each event describes a key; the event type identifies +/// what kind of activity was performed. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent) +pub trait IKeyboardEvent: IEvent { + /// Returns the value of a key or keys pressed by the user. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key) + fn key( &self ) -> String { + js!( return @{self.as_ref()}.key; ).into_string().unwrap() + } +} + +/// A reference to a JavaScript object which implements the [IKeyboardEvent](trait.IKeyboardEvent.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent) +pub struct KeyboardEvent( Reference ); + +impl IEvent for KeyboardEvent {} +impl IKeyboardEvent for KeyboardEvent {} + +reference_boilerplate! { + KeyboardEvent, + instanceof KeyboardEvent + convertible to Event +} + +/// The `KeypressEvent` is fired when a key is pressed down, and that +/// key normally produces a character value. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/keypress) +pub struct KeypressEvent( Reference ); + +impl IEvent for KeypressEvent {} +impl IKeyboardEvent for KeypressEvent {} +impl ConcreteEvent for KeypressEvent { + #[inline] + fn static_event_type() -> &'static str { + "keypress" + } +} + +reference_boilerplate! { + KeypressEvent, + instanceof KeyboardEvent + convertible to Event + convertible to KeyboardEvent +} + +/// The `IFocusEvent` interface represents focus-related +/// events. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent) +pub trait IFocusEvent: IEvent { +} + +/// A reference to a JavaScript object which implements the [IFocusEvent](trait.IFocusEvent.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent) +pub struct FocusRelatedEvent( Reference ); + +impl IEvent for FocusRelatedEvent {} +impl IFocusEvent for FocusRelatedEvent {} + +reference_boilerplate! { + FocusRelatedEvent, + instanceof FocusEvent + convertible to Event +} + +/// The `FocusEvent` is fired when an element has received focus. The main +/// difference between this event and focusin is that only the latter bubbles. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/focus) +pub struct FocusEvent( Reference ); + +impl IEvent for FocusEvent {} +impl IFocusEvent for FocusEvent {} +impl ConcreteEvent for FocusEvent { + #[inline] + fn static_event_type() -> &'static str { + "focus" + } +} + +reference_boilerplate! { + FocusEvent, + instanceof FocusEvent + convertible to Event + convertible to FocusRelatedEvent +} + +/// The `BlurEvent` is fired when an element has lost focus. The main difference +/// between this event and focusout is that only the latter bubbles. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/blur) +pub struct BlurEvent( Reference ); + +impl IEvent for BlurEvent {} +impl IFocusEvent for BlurEvent {} +impl ConcreteEvent for BlurEvent { + #[inline] + fn static_event_type() -> &'static str { + "blur" + } +} + +reference_boilerplate! { + BlurEvent, + instanceof FocusEvent + convertible to Event + convertible to FocusRelatedEvent +} + +/// The `HashChangeEvent` is fired when the fragment +/// identifier of the URL has changed (the part of the URL +/// that follows the # symbol, including the # symbol). +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/hashchange) +pub struct HashChangeEvent( Reference ); + +impl IEvent for HashChangeEvent {} +impl ConcreteEvent for HashChangeEvent { + #[inline] + fn static_event_type() -> &'static str { + "hashchange" + } +} + +reference_boilerplate! { + HashChangeEvent, + instanceof HashChangeEvent + convertible to Event +} diff --git a/src/webapi/event_target.rs b/src/webapi/event_target.rs new file mode 100644 index 00000000..9e5d7822 --- /dev/null +++ b/src/webapi/event_target.rs @@ -0,0 +1,73 @@ +use std::fmt; + +use webcore::value::Reference; +use webcore::try_from::TryInto; +use webapi::event::ConcreteEvent; + +pub struct EventListenerHandle { + event_type: &'static str, + reference: Reference, + listener_reference: Reference +} + +impl fmt::Debug for EventListenerHandle { + fn fmt( &self, formatter: &mut fmt::Formatter ) -> fmt::Result { + write!( formatter, "EventListenerHandle {{ event_type: {}, reference: {:?} }}", self.event_type, self.reference ) + } +} + +impl EventListenerHandle { + /// Removes the handler from the [IEventTarget](trait.IEventTarget.html) on + /// which it was previously registered. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) + pub fn remove( self ) { + js! { @(no_return) + var self = @{self.reference}; + var event_type = @{self.event_type}; + var listener = @{self.listener_reference}; + listener.drop(); + self.removeEventListener( event_type, listener ); + } + } +} + +/// `IEventTarget` is an interface implemented by objects that +/// can receive events and may have listeners for them. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) +pub trait IEventTarget: AsRef< Reference > { + /// Adds given event handler to the list the list of event listeners for + /// the specified `EventTarget` on which it's called. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) + fn add_event_listener< T, F >( &self, listener: F ) -> EventListenerHandle + where T: ConcreteEvent, F: FnMut( T ) + 'static + { + let reference = self.as_ref(); + let listener_reference = js! { + var listener = @{listener}; + @{reference}.addEventListener( @{T::static_event_type()}, listener ); + return listener; + }.try_into().unwrap(); + + EventListenerHandle { + event_type: T::static_event_type(), + reference: reference.clone(), + listener_reference: listener_reference + } + } +} + +/// A reference to a JavaScript object which implements the [IEventTarget](trait.IEventTarget.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) +pub struct EventTarget( Reference ); + +impl IEventTarget for EventTarget {} + +reference_boilerplate! { + EventTarget, + instanceof EventTarget +} diff --git a/src/webapi/global.rs b/src/webapi/global.rs new file mode 100644 index 00000000..6c63a1a9 --- /dev/null +++ b/src/webapi/global.rs @@ -0,0 +1,12 @@ +use webapi::window::window; +use webapi::window_or_worker::IWindowOrWorker; + +/// An alias for [window.set_timeout](struct.Window.html#method.set_timeout). +pub fn set_timeout< F: FnOnce() >( callback: F, timeout: u32 ) { + window().set_timeout( callback, timeout ); +} + +/// An alias for [window.alert](struct.Window.html#method.alert). +pub fn alert( message: &str ) { + window().alert( message ); +} diff --git a/src/webapi/html_element.rs b/src/webapi/html_element.rs new file mode 100644 index 00000000..0f0fefb8 --- /dev/null +++ b/src/webapi/html_element.rs @@ -0,0 +1,59 @@ +use webcore::value::Reference; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::node::{INode, Node}; +use webapi::element::{IElement, Element}; +use webapi::string_map::StringMap; + +/// The `IHtmlElement` interface represents any HTML element. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) +pub trait IHtmlElement: IElement { + /// Sets focus on the specified element, if it can be focused. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) + fn focus( &self ) { + js! { @(no_return) + @{self.as_ref()}.focus(); + } + } + + /// Removes keyboard focus from the current element. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur) + fn blur( &self ) { + js! { @(no_return) + @{self.as_ref()}.blur(); + } + } + + /// Allows access, both in reading and writing, to all of the custom data attributes (data-*) + /// set on the element, either in HTML or in the DOM. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) + fn dataset( &self ) -> StringMap { + unsafe { + js!( + return @{self.as_ref()}.dataset; + ).into_reference_unchecked().unwrap() + } + } +} + +/// A reference to a JavaScript object which implements the [IHtmlElement](trait.IHtmlElement.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) +pub struct HtmlElement( Reference ); + +impl IEventTarget for HtmlElement {} +impl INode for HtmlElement {} +impl IElement for HtmlElement {} +impl IHtmlElement for HtmlElement {} + +reference_boilerplate! { + HtmlElement, + instanceof HTMLElement + convertible to EventTarget + convertible to Node + convertible to Element +} diff --git a/src/webapi/html_elements/input.rs b/src/webapi/html_elements/input.rs new file mode 100644 index 00000000..49c3cb08 --- /dev/null +++ b/src/webapi/html_elements/input.rs @@ -0,0 +1,52 @@ +use webcore::value::{Value, Reference}; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::node::{INode, Node}; +use webapi::element::{IElement, Element}; +use webapi::html_element::{IHtmlElement, HtmlElement}; + +/// The HTML input element is used to create interactive controls +/// for web-based forms in order to accept data from the user. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en/docs/Web/HTML/Element/input) +pub struct InputElement( Reference ); + +impl IEventTarget for InputElement {} +impl INode for InputElement {} +impl IElement for InputElement {} +impl IHtmlElement for InputElement {} + +reference_boilerplate! { + InputElement, + instanceof HTMLInputElement + convertible to EventTarget + convertible to Node + convertible to Element + convertible to HtmlElement +} + +impl InputElement { + /// The value of the control. This attribute is optional except when the input is a radio button or a checkbox. + #[inline] + pub fn value( &self ) -> Value { + js! ( + return @{self}.value; + ) + } + + /// Sets the value of the control. + #[inline] + pub fn set_value< T: Into< Value > >( &self, value: T ) { + js! { @(no_return) + @{self}.value = @{value.into()}; + } + } + + /// The type of control to render. See [Form types](https://developer.mozilla.org/en/docs/Web/HTML/Element/input#Form__types) + /// for the individual types, with links to more information about each. + #[inline] + pub fn set_kind( &self, kind: &str ) { + js! { @(no_return) + @{self}.type = @{kind}; + } + } +} diff --git a/src/webapi/html_elements/mod.rs b/src/webapi/html_elements/mod.rs new file mode 100644 index 00000000..e41ee3cb --- /dev/null +++ b/src/webapi/html_elements/mod.rs @@ -0,0 +1,3 @@ +mod input; + +pub use self::input::InputElement; diff --git a/src/webapi/location.rs b/src/webapi/location.rs new file mode 100644 index 00000000..a4e17e38 --- /dev/null +++ b/src/webapi/location.rs @@ -0,0 +1,37 @@ +use webcore::value::Reference; +use webcore::try_from::TryInto; + +/// The `Location` interface represents the location (URL) of the object it +/// is linked to. Changes done on it are reflected on the object it relates +/// to. Both the [Document](struct.Document.html) and [Window](struct.Window.html) +/// interface have such a linked `Location`, accessible via [Document::location](struct.Document.html#method.location) +/// and [Window::location](struct.Window.html#method.location) respectively. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Location) +pub struct Location( Reference ); + +reference_boilerplate! { + Location, + instanceof Location +} + +impl Location { + /// The entire URL. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Location/href) + pub fn href( &self ) -> String { + js!( + return @{self}.href; + ).try_into().unwrap() + } + + /// Returns a `String` containing a '#' followed by the fragment + /// identifier of the URL. The fragment is not percent-decoded. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Location/hash) + pub fn hash( &self ) -> String { + js!( + return @{self}.hash; + ).try_into().unwrap() + } +} diff --git a/src/webapi/mod.rs b/src/webapi/mod.rs new file mode 100644 index 00000000..0457fb54 --- /dev/null +++ b/src/webapi/mod.rs @@ -0,0 +1,17 @@ +pub mod global; +pub mod date; +pub mod document; +pub mod window; +pub mod event; +pub mod event_target; +pub mod node; +pub mod element; +pub mod html_element; +pub mod html_elements; +pub mod window_or_worker; +pub mod token_list; +pub mod text_node; +pub mod node_list; +pub mod string_map; +pub mod location; +pub mod storage; diff --git a/src/webapi/node.rs b/src/webapi/node.rs new file mode 100644 index 00000000..8c77a86e --- /dev/null +++ b/src/webapi/node.rs @@ -0,0 +1,197 @@ +use std::fmt; +use std::error; + +use webcore::value::{Reference, FromReference}; +use webcore::try_from::TryInto; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::node_list::NodeList; + +/// A structure denoting that the specified DOM [Node](trait.INode.html) was not found. +#[derive(Debug)] +pub struct NotFoundError( String ); +impl error::Error for NotFoundError { + fn description( &self ) -> &str { + self.0.as_str() + } +} + +impl fmt::Display for NotFoundError { + fn fmt( &self, formatter: &mut fmt::Formatter ) -> fmt::Result { + write!( formatter, "{}", self.0 ) + } +} + +/// An enum which determines whenever the DOM [Node](trait.INode.html)'s children will also be cloned or not. +/// +/// Mainly used in [INode::clone_node](trait.INode.html#method.clone_node). +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum CloneKind { + /// Will not clone the children. + Shallow, + /// Will clone the children. + Deep +} + +/// `INode` is an interface from which a number of DOM API object types inherit. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node) +pub trait INode: IEventTarget + FromReference { + /// Adds a node to the end of the list of children of a specified parent node. + /// + /// If the given child is a reference to an existing node in the document then + /// it is moved from its current position to the new position (there is no requirement + /// to remove the node from its parent node before appending it to some other node). + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild) + fn append_child< T: INode >( &self, child: &T ) { + js! { @(no_return) + @{self.as_ref()}.appendChild( @{child.as_ref()} ); + } + } + + /// Removes a child node from the DOM. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild) + fn remove_child< T: INode >( &self, child: &T ) -> Result< (), NotFoundError > { + // TODO: Return the removed node. + let status = js! { + try { + @{self.as_ref()}.removeChild( @{child.as_ref()} ); + return true; + } catch( exception ) { + if( exception instanceof NotFoundError ) { + return false; + } else { + throw exception; + } + } + }; + + if status == true { + Ok(()) + } else { + Err( NotFoundError( "The node to be removed is not a child of this node.".to_owned() ) ) + } + } + + /// Returns a duplicate of the node on which this method was called. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode) + fn clone_node( &self, kind: CloneKind ) -> Self { + let is_deep = match kind { + CloneKind::Deep => true, + CloneKind::Shallow => false + }; + + let cloned = js! { + return @{self.as_ref()}.cloneNode( @{is_deep} ); + }; + + cloned.into_reference().unwrap().downcast::< Self >().unwrap() + } + + /// Checks whenever a given node is a descendant of this one or not. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/contains) + fn contains< T: INode >( &self, node: &T ) -> bool { + js!( + return @{self.as_ref()}.contains( @{node.as_ref()} ); + ).try_into().unwrap() + } + + /// Inserts the specified node before the reference node as a child of the current node. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore) + fn insert_before< T: INode, U: INode >( &self, new_node: &T, reference_node: &U ) { + js! { @(no_return) + @{self.as_ref()}.insertBefore( @{new_node.as_ref()}, @{reference_node.as_ref()} ); + } + } + + /// Replaces one hild node of the specified node with another. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/replaceChild) + fn replace_child< T: INode, U: INode >( &self, new_child: &T, old_child: &U ) { + js! { @(no_return) + @{self.as_ref()}.replaceChild( @{new_child.as_ref()}, @{old_child.as_ref()} ); + } + } + + /// Returns the parent of this node in the DOM tree. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/parentNode) + fn parent_node( &self ) -> Option< Node > { + js!( + return @{self.as_ref()}.parentNode; + ).try_into().ok() + } + + /// Returns the node's first child in the tree, or `None` if the node is childless. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en/docs/Web/API/Node/firstChild) + fn first_child( &self ) -> Option< Node > { + js!( + return @{self.as_ref()}.firstChild; + ).try_into().ok() + } + + /// A property which represents the "rendered" text content of a node and its descendants. + /// It approximates the text the user would get if they highlighted the contents of the element + /// with the cursor and then copied to the clipboard. + /// + /// This feature was originally introduced by Internet Explorer, and was formally specified in the HTML + /// standard in 2016 after being adopted by all major browser vendors. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/innerText) + fn inner_text( &self ) -> String { + js!( + return @{self.as_ref()}.innerText; + ).try_into().unwrap() + } + + /// A property which represents the text content of a node and its descendants. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) + fn text_content( &self ) -> Option< String > { + js!( + return @{self.as_ref()}.textContent; + ).try_into().unwrap() + } + + /// Sets the text content of this node; calling thil removes all + /// of node's children and replaces them with a single text node + /// with the given value. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) + fn set_text_content( &self, text: &str ) { + js! { @(no_return) + @{self.as_ref()}.textContent = @{text}; + } + } + + /// Returns a live collection of child nodes of this node. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/childNodes) + fn child_nodes( &self ) -> NodeList { + unsafe { + js!( + return @{self.as_ref()}.childNodes; + ).into_reference_unchecked().unwrap() + } + } +} + +/// A reference to a JavaScript object which implements the [INode](trait.INode.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node) +pub struct Node( Reference ); + +impl IEventTarget for Node {} +impl INode for Node {} + +reference_boilerplate! { + Node, + instanceof Node + convertible to EventTarget +} \ No newline at end of file diff --git a/src/webapi/node_list.rs b/src/webapi/node_list.rs new file mode 100644 index 00000000..3da31b0e --- /dev/null +++ b/src/webapi/node_list.rs @@ -0,0 +1,91 @@ +use webcore::value::{Value, Reference, FromReferenceUnchecked}; +use webcore::try_from::TryInto; +use webapi::node::Node; + +/// `NodeList` objects are collections of nodes such as those returned by properties +/// such as [INode::child_nodes](trait.INode.html#method.child_nodes) and the +/// [Document::query_selector_all](struct.Document.html#method.query_selector_all) method. +/// +/// In some cases, the `NodeList` is a live collection, which means that changes in the DOM +/// are reflected in the collection - for example [INode::child_nodes](trait.INode.html#method.child_nodes) is live. +/// +/// In other cases, the `NodeList` is a static collection, meaning any subsequent change +/// in the DOM does not affect the content of the collection - for example +/// [Document::query_selector_all](struct.Document.html#method.query_selector_all) returns +/// a static `NodeList`. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) +pub struct NodeList( Reference ); + +reference_boilerplate! { + NodeList, + instanceof NodeList +} + +impl NodeList { + /// Returns the number of [Node](struct.Node.html)s contained in this list. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/NodeList/length) + pub fn len( &self ) -> usize { + let length: i32 = js!( return @{self}.length; ).try_into().unwrap(); + length as usize + } + + /// Returns an iterator over the list. + pub fn iter( &self ) -> NodeIter { + NodeIter { + list: self.clone(), + index: 0 + } + } +} + +impl IntoIterator for NodeList { + type Item = Node; + type IntoIter = NodeIter; + + #[inline] + fn into_iter( self ) -> Self::IntoIter { + NodeIter { + list: self, + index: 0 + } + } +} + +impl< 'a > IntoIterator for &'a NodeList { + type Item = Node; + type IntoIter = NodeIter; + + #[inline] + fn into_iter( self ) -> Self::IntoIter { + NodeIter { + list: self.clone(), + index: 0 + } + } +} + +#[derive(Debug)] +pub struct NodeIter { + list: NodeList, + index: i32 +} + +impl Iterator for NodeIter { + type Item = Node; + fn next( &mut self ) -> Option< Self::Item > { + let value = js!( + return @{&self.list}[ @{self.index} ]; + ); + + let node = match value { + Value::Undefined => return None, + Value::Reference( reference ) => unsafe { Node::from_reference_unchecked( reference ) }, + _ => unreachable!() + }; + + self.index += 1; + Some( node ) + } +} diff --git a/src/webapi/storage.rs b/src/webapi/storage.rs new file mode 100644 index 00000000..70fa5620 --- /dev/null +++ b/src/webapi/storage.rs @@ -0,0 +1,69 @@ +use webcore::value::Reference; +use webcore::try_from::TryInto; + +/// The `Storage` interface of the Web Storage API provides access to +/// the session storage or local storage for a particular domain. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Storage) +pub struct Storage( Reference ); + +reference_boilerplate! { + Storage, + instanceof Storage +} + +impl Storage { + /// Gets the number of data items stored in the `Storage` object. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Storage/length) + pub fn len( &self ) -> usize { + let length: i32 = js!( return @{self}.length; ).try_into().unwrap(); + length as usize + } + + /// Returns a value corresponding to the key. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem) + pub fn get( &self, key: &str ) -> Option< String > { + js!( return @{self}.getItem( @{key} ); ).try_into().ok() + } + + /// Inserts a key-value pair into the storage. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem) + pub fn insert( &self, key: &str, value: &str ) { + js!( @(no_return) + @{self}.setItem( @{key}, @{value} ); + ); + } + + /// Removes a key from the storage. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Storage/removeItem) + pub fn remove( &self, key: &str ) { + js!( @(no_return) + @{self}.removeItem( @{key} ); + ); + } + + /// When invoked, will empty all keys out of the storage. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Storage/clear) + pub fn clear( &self ) { + js!( @(no_return) + @{self}.clear(); + ); + } + + /// Return the name of the nth key in the storage. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Storage/key) + pub fn key( &self, nth: usize ) -> Option< String > { + js!( return @{self}.key( @{nth as u32} ); ).try_into().ok() + } + + /// Returns true if the storage contains a value for the specified key. + pub fn contains_key( &self, key: &str ) -> bool { + js!( return !!@{self}.getItem( @{key} ); ).try_into().unwrap() + } +} \ No newline at end of file diff --git a/src/webapi/string_map.rs b/src/webapi/string_map.rs new file mode 100644 index 00000000..aee24402 --- /dev/null +++ b/src/webapi/string_map.rs @@ -0,0 +1,39 @@ +use webcore::value::Reference; +use webcore::try_from::TryInto; + +/// Used by the `dataset` HTML attribute to represent data for custom attributes added to elements. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/DOMStringMap) +pub struct StringMap( Reference ); + +reference_boilerplate! { + StringMap, + instanceof DOMStringMap +} + +// The methods here are deliberately named exactly as those from Rust's HashMap. +impl StringMap { + /// Returns a value corresponding to the key. + pub fn get( &self, key: &str ) -> Option< String > { + js!( return @{self}[ @{key} ]; ).try_into().ok() + } + + /// Inserts a key-value pair into the map. + pub fn insert( &self, key: &str, value: &str ) { + js!( @(no_return) + @{self}[ @{key} ] = @{value}; + ); + } + + /// Removes a key from the map. + pub fn remove( &self, key: &str ) { + js!( @(no_return) + delete @{self}[ @{key} ]; + ); + } + + /// Returns true if the map contains a value for the specified key. + pub fn contains_key( &self, key: &str ) -> bool { + js!( return @{key} in @{self}; ).try_into().unwrap() + } +} diff --git a/src/webapi/text_node.rs b/src/webapi/text_node.rs new file mode 100644 index 00000000..3fd2bc31 --- /dev/null +++ b/src/webapi/text_node.rs @@ -0,0 +1,23 @@ +use webcore::value::Reference; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::node::{INode, Node}; + +/// The `TextNode` represents the textual content of an [IElement](trait.IElement.html) +/// +/// If an element has no markup within its content, it has +/// a single child `TextNode` that contains the element's +/// text. However, if the element contains markup, it is parsed +/// into information items and `TextNode`s that form its children. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Text) +pub struct TextNode( Reference ); + +impl IEventTarget for TextNode {} +impl INode for TextNode {} + +reference_boilerplate! { + TextNode, + instanceof Text + convertible to EventTarget + convertible to Node +} diff --git a/src/webapi/token_list.rs b/src/webapi/token_list.rs new file mode 100644 index 00000000..7356ee33 --- /dev/null +++ b/src/webapi/token_list.rs @@ -0,0 +1,47 @@ +use webcore::value::Reference; +use webcore::try_from::TryInto; + +/// The `TokenList` represents a set of space-separated tokens. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList) +pub struct TokenList( Reference ); + +reference_boilerplate! { + TokenList, + instanceof DOMTokenList +} + +impl TokenList { + /// Gets the number of tokens in the list. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/length) + pub fn len( &self ) -> usize { + let length: i32 = js!( return @{self}.length; ).try_into().unwrap(); + length as usize + } + + /// Adds token to the underlying string. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/add) + pub fn add( &self, token: &str ) { + js! { @(no_return) + @{self}.add( @{token} ); + } + } + + /// Removes token from the underlying string. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/remove) + pub fn remove( &self, token: &str ) { + js! { @(no_return) + @{self}.remove( @{token} ); + } + } + + /// Returns `true` if the underlying string contains token, otherwise `false`. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/contains) + pub fn contains( &self, token: &str ) -> bool { + js!( return @{self}.contains( @{token} ); ).try_into().unwrap() + } +} \ No newline at end of file diff --git a/src/webapi/window.rs b/src/webapi/window.rs new file mode 100644 index 00000000..cc41a4b2 --- /dev/null +++ b/src/webapi/window.rs @@ -0,0 +1,89 @@ +use webcore::value::Reference; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::window_or_worker::IWindowOrWorker; +use webapi::storage::Storage; +use webapi::location::Location; + +/// The `Window` object represents a window containing a DOM document. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Window) +pub struct Window( Reference ); + +impl IEventTarget for Window {} +impl IWindowOrWorker for Window {} + +reference_boilerplate! { + Window, + instanceof Window + convertible to EventTarget +} + +/// A global instance of [Window](struct.Window.html). +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Window) +pub fn window() -> Window { + unsafe { js!( return window; ).into_reference_unchecked() }.unwrap() +} + +impl Window { + /// The Window.alert() method displays an alert dialog + /// with the optional specified content and an OK button. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert) + pub fn alert( &self, message: &str ) { + js!( @(no_return) + @{self}.alert( @{message} ); + ); + } + + /// The `local_storage` property allows you to access a local [Storage](struct.Storage.html) + /// object. + /// + /// It is similar to the [Window::session_storage](struct.Window.html#method.session_storage). + /// The only difference is that, while data stored in `local_storage` has + /// no expiration time, data stored in `session_storage` gets cleared when + /// the browsing session ends - that is, when the browser is closed. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) + pub fn local_storage( &self ) -> Storage { + unsafe { + js!( + return @{self.as_ref()}.localStorage; + ).into_reference_unchecked().unwrap() + } + } + + /// The `session_storage` property allows you to access a session [Storage](struct.Storage.html) + /// object for the current origin. + /// + /// It is similar to the [Window::local_storage](struct.Window.html#method.local_storage), + /// The only difference is that, while data stored in `local_storage` has + /// no expiration time, data stored in `session_storage` gets cleared when + /// the browsing session ends. + /// + /// A page session lasts for as long as the browser is open and survives over + /// page reloads and restores. Opening a page in a new tab or window will cause + /// a new session to be initiated, which differs from how session cookies work. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) + pub fn session_storage( &self ) -> Storage { + unsafe { + js!( + return @{self.as_ref()}.sessionStorage; + ).into_reference_unchecked().unwrap() + } + } + + /// Returns a [Location](struct.Location.html) object which contains + /// information about the URL of the document and provides methods + /// for changing that URL and loading another URL. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Window/location) + pub fn location( &self ) -> Option< Location > { + unsafe { + js!( + return @{self}.location; + ).into_reference_unchecked() + } + } +} diff --git a/src/webapi/window_or_worker.rs b/src/webapi/window_or_worker.rs new file mode 100644 index 00000000..24d631d3 --- /dev/null +++ b/src/webapi/window_or_worker.rs @@ -0,0 +1,27 @@ +use webcore::value::Reference; + +extern fn funcall_adapter< F: FnOnce() >( callback: *mut F ) { + let callback = unsafe { + Box::from_raw( callback ) + }; + + callback(); +} + +/// The `IWindowOrWorker` mixin describes several features common to +/// the `Window` and the global scope of web workers. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope) +pub trait IWindowOrWorker: AsRef< Reference > { + /// Sets a timer which executes a function once after the timer expires. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout) + fn set_timeout< F: FnOnce() >( &self, callback: F, timeout: u32 ) { + let callback = Box::into_raw( Box::new( callback ) ); + em_asm_int!( "\ + Module.STDWEB.acquire_js_reference( $0 ).setTimeout( function() {\ + Runtime.dynCall( 'vi', $1, [$2] );\ + }, $3 );\ + ", self.as_ref().as_raw(), funcall_adapter::< F > as extern fn( *mut F ), callback, timeout ); + } +} diff --git a/src/webcore/callfn.rs b/src/webcore/callfn.rs new file mode 100644 index 00000000..7eb1925a --- /dev/null +++ b/src/webcore/callfn.rs @@ -0,0 +1,73 @@ +// Since the {Fn, FnMut, FnOnce} traits are unstable we need +// to define our own versions. +// +// I'd put this in a separate crate, but we need to keep it +// here to allow Rust to do a better job of checking impl exhaustiveness, +// otherwise if we pull these traits from another crate we'll have +// `conflicting implementations of trait` errors. +pub trait CallOnce< Args > { + type Output; + fn call_once( self, args: Args ) -> Self::Output; + fn expected_argument_count() -> usize; +} + +pub trait CallMut< Args >: CallOnce< Args > { + fn call_mut( &mut self, args: Args ) -> Self::Output; +} + +pub trait Call< Args >: CallMut< Args > { + fn call( &self, args: Args ) -> Self::Output; +} + +macro_rules! noop { + ($token:tt) => {} +} + +macro_rules! define { + ($next:tt => $($kind:ident),*) => { + impl< R, $($kind,)* F: FnOnce( $($kind,)* ) -> R > CallOnce< ($($kind,)*) > for F { + type Output = R; + #[inline] + fn call_once( self, args: ($($kind,)*) ) -> Self::Output { + #[allow(non_snake_case)] + let ($($kind,)*) = args; + self( $($kind),* ) + } + + #[inline] + fn expected_argument_count() -> usize { + let mut count = 0; + $( + // I'm too lazy to make a separate macro to count the tokens so we just do this. + count += 1; + noop!( $kind ); + )* + + $crate::private::noop( &mut count ); + count + } + } + + impl< R, $($kind,)* F: FnMut( $($kind,)* ) -> R > CallMut< ($($kind,)*) > for F { + #[inline] + fn call_mut( &mut self, args: ($($kind,)*) ) -> Self::Output { + #[allow(non_snake_case)] + let ($($kind,)*) = args; + self( $($kind),* ) + } + } + + impl< R, $($kind,)* F: Fn( $($kind,)* ) -> R > Call< ($($kind,)*) > for F { + #[inline] + fn call( &self, args: ($($kind,)*) ) -> Self::Output { + #[allow(non_snake_case)] + let ($($kind,)*) = args; + self( $($kind),* ) + } + } + + next! { $next } + } +} + +loop_through_identifiers!( define ); diff --git a/src/webcore/ffi.rs b/src/webcore/ffi.rs new file mode 100644 index 00000000..ac7c9698 --- /dev/null +++ b/src/webcore/ffi.rs @@ -0,0 +1,9 @@ +pub type CallbackFn = Option< unsafe extern "C" fn() >; + +extern "C" { + pub fn free( pointer: *const u8 ); + pub fn emscripten_asm_const_int( code: *const u8, ... ) -> i32; + pub fn emscripten_asm_const_double( code: *const u8, ... ) -> f64; + pub fn emscripten_pause_main_loop(); + pub fn emscripten_set_main_loop( callback: CallbackFn, fps: i32, simulate_infinite_loop: i32 ); +} diff --git a/src/webcore/initialization.rs b/src/webcore/initialization.rs new file mode 100644 index 00000000..be0460c6 --- /dev/null +++ b/src/webcore/initialization.rs @@ -0,0 +1,261 @@ +use std::panic; +use webcore::ffi; + +/// Initializes the library. +/// +/// Calling this is required for anything to work. +pub fn initialize() { + static mut INITIALIZED: bool = false; + unsafe { + if INITIALIZED { + return; + } + + INITIALIZED = true; + } + + js! { @(no_return) + Module.STDWEB = {}; + Module.STDWEB.to_js = function to_js( address ) { + var kind = HEAPU8[ address + 12 ]; + if( kind === 0 ) { + return undefined; + } else if( kind === 1 ) { + return null; + } else if( kind === 2 ) { + return HEAP32[ address / 4 ]; + } else if( kind === 3 ) { + return HEAPF64[ address / 8 ]; + } else if( kind === 4 ) { + var pointer = HEAPU32[ address / 4 ]; + var length = HEAPU32[ (address + 4) / 4 ]; + return Module.STDWEB.to_js_string( pointer, length ); + } else if( kind === 5 ) { + return false; + } else if( kind === 6 ) { + return true; + } else if( kind === 7 ) { + var pointer = HEAPU32[ address / 4 ]; + var length = HEAPU32[ (address + 4) / 4 ]; + var output = []; + for( var i = 0; i < length; ++i ) { + output.push( Module.STDWEB.to_js( pointer + i * 16 ) ); + } + return output; + } else if( kind === 8 ) { + var value_array_pointer = HEAPU32[ address / 4 ]; + var length = HEAPU32[ (address + 4) / 4 ]; + var key_array_pointer = HEAPU32[ (address + 8) / 4 ]; + var output = {}; + for( var i = 0; i < length; ++i ) { + var key_pointer = HEAPU32[ (key_array_pointer + i * 8) / 4 ]; + var key_length = HEAPU32[ (key_array_pointer + 4 + i * 8) / 4 ]; + var key = Module.STDWEB.to_js_string( key_pointer, key_length ); + var value = Module.STDWEB.to_js( value_array_pointer + i * 16 ); + output[ key ] = value; + } + return output; + } else if( kind === 9 ) { + return Module.STDWEB.acquire_js_reference( HEAP32[ address / 4 ] ); + } else if( kind === 10 ) { + var adapter_pointer = HEAPU32[ address / 4 ]; + var pointer = HEAPU32[ (address + 4) / 4 ]; + var deallocator_pointer = HEAPU32[ (address + 8) / 4 ]; + var output = function() { + var args = _malloc( 16 ); + Module.STDWEB.from_js( args, arguments ); + Runtime.dynCall( "vii", adapter_pointer, [pointer, args] ); + var result = Module.STDWEB.tmp; + Module.STDWEB.tmp = null; + + return result; + }; + + output.drop = function() { + output.drop = null; + Runtime.dynCall( "vi", deallocator_pointer, [pointer] ); + }; + + return output; + } + }; + }; + + js! { @(no_return) + Module.STDWEB.from_js = function from_js( address, value ) { + var kind = Object.prototype.toString.call( value ); + if( kind === "[object String]" ) { + var length = lengthBytesUTF8( value ); + var pointer = _malloc( length + 1 ); + stringToUTF8( value, pointer, length + 1 ); + HEAPU8[ address + 12 ] = 4; + HEAPU32[ address / 4 ] = pointer; + HEAPU32[ (address + 4) / 4 ] = length; + } else if( kind === "[object Number]" ) { + if( value === (value|0) ) { + HEAPU8[ address + 12 ] = 2; + HEAP32[ address / 4 ] = value; + } else { + HEAPU8[ address + 12 ] = 3; + HEAPF64[ address / 8 ] = value; + } + } else if( value === null ) { + HEAPU8[ address + 12 ] = 1; + } else if( value === undefined ) { + HEAPU8[ address + 12 ] = 0; + } else if( value === false ) { + HEAPU8[ address + 12 ] = 5; + } else if( value === true ) { + HEAPU8[ address + 12 ] = 6; + } else if( kind === "[object Array]" || kind === "[object Arguments]" ) { + var length = value.length; + var pointer = _malloc( length * 16 ); + HEAPU8[ address + 12 ] = 7; + HEAPU32[ address / 4 ] = pointer; + HEAPU32[ (address + 4) / 4 ] = length; + for( var i = 0; i < length; ++i ) { + Module.STDWEB.from_js( pointer + i * 16, value[ i ] ); + } + } else if( kind === "[object Object]" ) { + var keys = Object.keys( value ); + var length = keys.length; + var key_array_pointer = _malloc( length * 8 ); + var value_array_pointer = _malloc( length * 16 ); + HEAPU8[ address + 12 ] = 8; + HEAPU32[ address / 4 ] = value_array_pointer; + HEAPU32[ (address + 4) / 4 ] = length; + HEAPU32[ (address + 8) / 4 ] = key_array_pointer; + for( var i = 0; i < length; ++i ) { + var key = keys[ i ]; + var key_length = lengthBytesUTF8( key ); + var key_pointer = _malloc( key_length + 1 ); + stringToUTF8( key, key_pointer, key_length + 1 ); + + var key_address = key_array_pointer + i * 8; + HEAPU32[ key_address / 4 ] = key_pointer; + HEAPU32[ (key_address + 4) / 4 ] = key_length; + + Module.STDWEB.from_js( value_array_pointer + i * 16, value[ key ] ); + } + } else { + var refid = Module.STDWEB.acquire_rust_reference( value ); + HEAPU8[ address + 12 ] = 9; + HEAP32[ address / 4 ] = refid; + } + }; + }; + + js! { @(no_return) + // This is ported from Rust's stdlib; it's faster than + // the string conversion from Emscripten. + Module.STDWEB.to_js_string = function to_js_string( index, length ) { + index = index|0; + length = length|0; + var end = (index|0) + (length|0); + var output = ""; + while( index < end ) { + var x = HEAPU8[ index++ ]; + if( x < 128 ) { + output += String.fromCharCode( x ); + continue; + } + var init = (x & (0x7F >> 2)); + var y = 0; + if( index < end ) { + y = HEAPU8[ index++ ]; + } + var ch = (init << 6) | (y & 63); + if( x >= 0xE0 ) { + let z = 0; + if( index < end ) { + z = HEAPU8[ index++ ]; + } + let y_z = ((y & 63) << 6) | (z & 63); + ch = init << 12 | y_z; + if( x >= 0xF0 ) { + let w = 0; + if( index < end ) { + w = HEAPU8[ index++ ]; + } + ch = (init & 7) << 18 | ((y_z << 6) | (w & 63)); + } + } + output += String.fromCharCode( ch ); + continue; + } + return output; + }; + }; + + js! { @(no_return) + var id_to_ref_map = {}; + var id_to_refcount_map = {}; + var ref_to_id_map = new WeakMap(); + var last_refid = 1; + + Module.STDWEB.acquire_rust_reference = function( reference ) { + if( reference === undefined || reference === null ) { + return 0; + } + + var refid = ref_to_id_map.get( reference ); + if( refid === undefined ) { + refid = last_refid++; + ref_to_id_map.set( reference, refid ); + id_to_ref_map[ refid ] = reference; + id_to_refcount_map[ refid ] = 1; + } else { + id_to_refcount_map[ refid ]++; + } + + return refid; + }; + + Module.STDWEB.acquire_js_reference = function( refid ) { + return id_to_ref_map[ refid ]; + }; + + Module.STDWEB.increment_refcount = function( refid ) { + id_to_refcount_map[ refid ]++; + }; + + Module.STDWEB.decrement_refcount = function( refid ) { + id_to_refcount_map[ refid ]--; + if( id_to_refcount_map[ refid ] === 0 ) { + var reference = id_to_ref_map[ refid ]; + delete id_to_ref_map[ refid ]; + delete id_to_refcount_map[ refid ]; + ref_to_id_map.delete( reference ); + } + }; + } + + if cfg!( test ) == false { + panic::set_hook( Box::new( |info| { + em_asm_int!( "console.error( 'Encountered a panic!' );" ); + if let Some( value ) = info.payload().downcast_ref::< String >() { + em_asm_int!( "\ + console.error( 'Panic error message:', Module.STDWEB.to_js_string( $0, $1 ) );\ + ", value.as_ptr(), value.len() ); + } + if let Some( location ) = info.location() { + let file = location.file(); + em_asm_int!( "\ + console.error( 'Panic location:', Module.STDWEB.to_js_string( $0, $1 ) + ':' + $2 );\ + ", file.as_ptr(), file.len(), location.line() ); + } + })); + } +} + +/// Runs the event loop. +/// +/// You should call this before returning from `main()`, +/// otherwise bad things will happen. +pub fn event_loop() -> ! { + unsafe { + ffi::emscripten_set_main_loop( Some( ffi::emscripten_pause_main_loop ), 0, 1 ); + } + + unreachable!(); +} diff --git a/src/webcore/macros.rs b/src/webcore/macros.rs new file mode 100644 index 00000000..600a0166 --- /dev/null +++ b/src/webcore/macros.rs @@ -0,0 +1,542 @@ +macro_rules! next { + (empty) => {}; + + ((peel, $callback:tt, ($value:tt))) => { + $callback!( empty => ); + }; + + ((peel, $callback:tt, ($value:tt, $($other:tt),+))) => { + $callback!( (peel, $callback, ($($other),+)) => $($other),+ ); + }; +} + +macro_rules! foreach { + ($callback:tt => $($values:tt),*) => { + $callback!( (peel, $callback, ($($values),*)) => $($values),* ); + }; +} + +macro_rules! loop_through_identifiers { + ($callback:tt) => { + foreach!( $callback => A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12 ); + }; +} + +macro_rules! em_asm_int { + ($code:expr, $($arg:expr),*) => { + { + const CODE: &'static str = concat!( $code, "\0" ); + + #[allow(unused_unsafe)] + unsafe { + use webcore::ffi; + ffi::emscripten_asm_const_int( + CODE as *const _ as *const u8, + $($arg),* + ) + } + } + }; + ($code:expr) => { + { + const CODE: &'static str = concat!( $code, "\0" ); + unsafe { + use webcore::ffi; + ffi::emscripten_asm_const_int( + CODE as *const _ as *const u8 + ) + } + } + }; +} + +macro_rules! em_asm_double { + ($code:expr, $($arg:expr),*) => { + { + const CODE: &'static str = concat!( $code, "\0" ); + + #[allow(unused_unsafe)] + unsafe { + use webcore::ffi; + ffi::emscripten_asm_const_double( + CODE as *const _ as *const u8, + $($arg),* + ) + } + } + }; + ($code:expr) => { + { + const CODE: &'static str = concat!( $code, "\0" ); + unsafe { + use webcore::ffi; + ffi::emscripten_asm_const_double( + CODE as *const _ as *const u8 + ) + } + } + }; +} + +// Abandon all hope, ye who enter here! +// +// If there was a contest for the ugliest and most hacky macro ever written, +// I would enter this one. +// +// There is probably a more clever way to write this macro, but oh well. +#[doc(hidden)] +#[macro_export] +macro_rules! _js_impl { + (@_inc @_stringify "0" $($rest:tt)*) => { _js_impl!( @_stringify "1" $($rest)* ) }; + (@_inc @_stringify "1" $($rest:tt)*) => { _js_impl!( @_stringify "2" $($rest)* ) }; + (@_inc @_stringify "2" $($rest:tt)*) => { _js_impl!( @_stringify "3" $($rest)* ) }; + (@_inc @_stringify "3" $($rest:tt)*) => { _js_impl!( @_stringify "4" $($rest)* ) }; + (@_inc @_stringify "4" $($rest:tt)*) => { _js_impl!( @_stringify "5" $($rest)* ) }; + (@_inc @_stringify "5" $($rest:tt)*) => { _js_impl!( @_stringify "6" $($rest)* ) }; + (@_inc @_stringify "6" $($rest:tt)*) => { _js_impl!( @_stringify "7" $($rest)* ) }; + (@_inc @_stringify "7" $($rest:tt)*) => { _js_impl!( @_stringify "8" $($rest)* ) }; + (@_inc @_stringify "8" $($rest:tt)*) => { _js_impl!( @_stringify "9" $($rest)* ) }; + (@_inc @_stringify "9" $($rest:tt)*) => { _js_impl!( @_stringify "10" $($rest)* ) }; + + (@_stringify $arg_counter:tt [$terminator:tt $($terminator_rest:tt)*] [$($out:tt)*] -> [] $next:tt $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator_rest)*] [$($out)* ($terminator)] -> $next $($rest)* ) + }; + + (@_stringify $arg_counter:tt [] [$($out:tt)*] -> []) => { + concat!( $(concat! $out),* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [($($inner:tt)*) $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [")" $($terminator)*] [$($out)* ("(")] -> [$($inner)*] [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [[$($inner:tt)*] $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter ["]" $($terminator)*] [$($out)* ("[")] -> [$($inner)*] [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [{$($inner:tt)*} $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter ["}" $($terminator)*] [$($out)* ("{")] -> [$($inner)*] [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [@{$arg:expr} $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_inc @_stringify $arg_counter [$($terminator)*] [$($out)* ("Module.STDWEB.to_js($") ($arg_counter) (")")] -> [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [++ $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator)*] [$($out)* ("++")] -> [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [-- $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator)*] [$($out)* ("--")] -> [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [=== $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator)*] [$($out)* ("===")] -> [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [!== $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator)*] [$($out)* ("!==")] -> [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [$token:tt . $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator)*] [$($out)* (stringify!( $token )) (".")] -> [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [$token:tt] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator)*] [$($out)* (stringify!( $token ))] -> [] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [$token:tt $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator)*] [$($out)* (stringify!( $token )) (" ")] -> [$($remaining)*] $($rest)* ) + }; + + (@stringify [$($flags:tt)*] -> $($rest:tt)*) => { + _js_impl!( @if no_return in [$($flags)*] { + _js_impl!( @_stringify "0" [] [] -> [$($rest)*] ) + } else { + _js_impl!( @_stringify "1" [] [] -> [$($rest)*] ) + }) + }; + + (@if no_return in [no_return $($rest:tt)*] {$($true_case:tt)*} else {$($false_case:tt)*}) => { + $($true_case)* + }; + + (@if $condition:tt in [] {$($true_case:tt)*} else {$($false_case:tt)*}) => { + $($false_case)* + }; + + (@if $condition:tt in [$token:tt $($rest:tt)*] {$($true_case:tt)*} else {$($false_case:tt)*}) => { + _js_impl!( @if $condition in [$($rest)*] {$($true_case)*} else {$($false_case)*} ); + }; + + (@prepare $memory_required:ident [] [$($names:tt)*]) => {}; + (@prepare $memory_required:ident [$arg:tt $($rest_args:tt)*] [$name:tt $($rest_names:tt)*]) => { + let $name = $arg; + let $name = $crate::private::IntoNewtype::into_newtype( $name ); + $memory_required += $crate::private::JsSerializableOwned::memory_required_owned( &$name ); + _js_impl!( @prepare $memory_required [$($rest_args)*] [$($rest_names)*] ); + }; + + (@serialize $arena:ident [] [$($names:tt)*]) => {}; + (@serialize $arena:ident [$arg:tt $($rest_args:tt)*] [$name:tt $($rest_names:tt)*]) => { + let mut $name = Some( $name ); + let $name = $crate::private::JsSerializableOwned::into_js_owned( &mut $name, &$arena ); + let $name = &$name as *const _; + _js_impl!( @serialize $arena [$($rest_args)*] [$($rest_names)*] ); + }; + + (@call_emscripten [$a0:tt] [$a0_name:tt $($arg_names:tt)*]) => { + $crate::private::emscripten_asm_const_int( $a0_name ); + }; + + (@call_emscripten [$a0:tt $a1:tt] [$a0_name:tt $a1_name:tt $($arg_names:tt)*]) => { + $crate::private::emscripten_asm_const_int( $a0_name, $a1_name ); + }; + + (@call_emscripten [$a0:tt $a1:tt $a2:tt] [$a0_name:tt $a1_name:tt $a2_name:tt $($arg_names:tt)*]) => { + $crate::private::emscripten_asm_const_int( $a0_name, $a1_name, $a2_name ); + }; + + (@call_emscripten [$a0:tt $a1:tt $a2:tt $a3:tt] [$a0_name:tt $a1_name:tt $a2_name:tt $a3_name:tt $($arg_names:tt)*]) => { + $crate::private::emscripten_asm_const_int( $a0_name, $a1_name, $a2_name, $a3_name ); + }; + + (@call_emscripten [$a0:tt $a1:tt $a2:tt $a3:tt $a4:tt] [$a0_name:tt $a1_name:tt $a2_name:tt $a3_name:tt $a4_name:tt $($arg_names:tt)*]) => { + $crate::private::emscripten_asm_const_int( $a0_name, $a1_name, $a2_name, $a3_name, $a4_name ); + }; + + (@call_emscripten [$a0:tt $a1:tt $a2:tt $a3:tt $a4:tt $a5:tt] [$a0_name:tt $a1_name:tt $a2_name:tt $a3_name:tt $a4_name:tt $a5_name:tt $($arg_names:tt)*]) => { + $crate::private::emscripten_asm_const_int( $a0_name, $a1_name, $a2_name, $a3_name, $a4_name, $a5_name ); + }; + + (@call [$code:expr, [$($flags:tt)*]] [$($args:tt)*] ->) => { + // It'd be nice to put at least some of this inside a function inside the crate, + // but then it wouldn't work (I tried!) as the string with the code wouldn't be + // passed as a direct reference to a constant, and Emscripten needs that to actually + // use the JavaScript code we're passing to it. + { + if cfg!( test ) { + $crate::initialize(); + } + + const CODE_STR: &'static str = _js_impl!( + @if no_return in [$($flags)*] { + concat!( $code, "\0" ) + } else { + concat!( "Module.STDWEB.from_js($0, (function(){", $code, "})());\0" ) + } + ); + + #[allow(dead_code)] + const CODE: *const u8 = CODE_STR as *const _ as *const u8; + + let mut memory_required = 0; + _js_impl!( @prepare memory_required [$($args)*] [a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10] ); + + #[allow(unused_variables)] + let arena = $crate::private::PreallocatedArena::new( memory_required ); + + _js_impl!( @serialize arena [$($args)*] [a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10] ); + arena.assert_no_free_space_left(); + + $crate::private::noop( &mut memory_required ); + + #[allow(unused_unsafe)] + unsafe { + _js_impl!( + @if no_return in [$($flags)*] { + _js_impl!( @call_emscripten [CODE $($args)*] [CODE a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10] ); + } else {{ + let mut result: $crate::private::SerializedValue = Default::default(); + _js_impl!( @call_emscripten [CODE RESULT $($args)*] [CODE (&mut result as *mut _) a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10] ); + result.deserialize() + }} + ) + } + } + }; + + (@call [$code:expr, [$($flags:tt)*]] [$($args:tt)*] -> { $($inner:tt)* } $($rest:tt)*) => { + _js_impl!( @call [$code, [$($flags)*]] [$($args)*] -> $($inner)* $($rest)* ); + }; + + (@call [$code:expr, [$($flags:tt)*]] [$($args:tt)*] -> ( $($inner:tt)* ) $($rest:tt)*) => { + _js_impl!( @call [$code, [$($flags)*]] [$($args)*] -> $($inner)* $($rest)* ); + }; + + (@call [$code:expr, [$($flags:tt)*]] [$($args:tt)*] -> [ $($inner:tt)* ] $($rest:tt)*) => { + _js_impl!( @call [$code, [$($flags)*]] [$($args)*] -> $($inner)* $($rest)* ); + }; + + (@call [$code:expr, [$($flags:tt)*]] [$($args:tt)*] -> @{$arg:expr} $($rest:tt)*) => { + _js_impl!( @call [$code, [$($flags)*]] [$($args)* $arg] -> $($rest)* ); + }; + + (@call [$code:expr, [$($flags:tt)*]] [$($args:tt)*] -> $token:tt $($rest:tt)*) => { + _js_impl!( @call [$code, [$($flags)*]] [$($args)*] -> $($rest)* ); + }; +} + +/// Embeds JavaScript code into your Rust program. +/// +/// This macro supports normal JavaScript syntax, albeit with a few limitations: +/// +/// * String literals delimited with `'` are not supported. +/// * Semicolons are always required. +/// * The macro will hit the default recursion limit pretty fast, so you'll +/// probably want to increase it with `#![recursion_limit="500"]`. +/// (This is planned to be fixed once procedural macros land in stable Rust.) +/// * Any callbacks passed into JavaScript will **leak memory** by default! +/// You need to call `.drop()` on the callback from the JavaScript side to free it. +/// +/// You can pass Rust expressions into the JavaScript code with `@{...expr...}`. +/// The value returned by this macro is an instance of [Value](enum.Value.html). +/// +/// # Examples +/// +/// ``` +/// let name = "Bob"; +/// let result = js! { +/// console.log( "Hello " + @{name} + "!" ); +/// return 2 + 2; +/// }; +/// +/// println!( "2 + 2 = {:?}", result ); +/// ``` +#[macro_export] +macro_rules! js { + (@($($flags:tt),*) $($token:tt)*) => { + _js_impl!( @call [_js_impl!( @stringify [$($flags)*] -> $($token)* ), [$($flags)*]] [] -> $($token)* ) + }; + + ($($token:tt)*) => { + _js_impl!( @call [_js_impl!( @stringify [] -> $($token)* ), []] [] -> $($token)* ) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __js_serializable_boilerplate { + ($kind:tt) => { + __js_serializable_boilerplate!( () ($kind) () ); + }; + + (impl< $($impl_arg:tt),* > for $kind:ty where $($bounds:tt)*) => { + __js_serializable_boilerplate!( ($($impl_arg),*) ($kind) ($($bounds)*) ); + }; + + (impl< $($impl_arg:tt),* > for $kind:ty) => { + __js_serializable_boilerplate!( ($($impl_arg),*) ($kind) () ); + }; + + (($($impl_arg:tt)*) ($($kind_arg:tt)*) ($($bounds:tt)*)) => { + impl< $($impl_arg)* > $crate::private::JsSerializableOwned for $crate::private::Newtype< (), $($kind_arg)* > where $($bounds)* { + #[inline] + fn into_js_owned< 'x >( value: &'x mut Option< Self >, arena: &'x $crate::private::PreallocatedArena ) -> $crate::private::SerializedValue< 'x > { + $crate::private::JsSerializable::into_js( value.as_ref().unwrap().as_ref(), arena ) + } + + #[inline] + fn memory_required_owned( &self ) -> usize { + $crate::private::JsSerializable::memory_required( &**self ) + } + } + + impl< '_r, $($impl_arg)* > $crate::private::JsSerializableOwned for $crate::private::Newtype< (), &'_r $($kind_arg)* > where $($bounds)* { + #[inline] + fn into_js_owned< '_a >( value: &'_a mut Option< Self >, arena: &'_a $crate::private::PreallocatedArena ) -> $crate::private::SerializedValue< '_a > { + $crate::private::JsSerializable::into_js( value.as_ref().unwrap().as_ref(), arena ) + } + + #[inline] + fn memory_required_owned( &self ) -> usize { + $crate::private::JsSerializable::memory_required( &**self ) + } + } + + impl< $($impl_arg)* > $crate::private::JsSerializableOwned for $($kind_arg)* where $($bounds)* { + #[inline] + fn into_js_owned< '_a >( value: &'_a mut Option< Self >, arena: &'_a $crate::private::PreallocatedArena ) -> $crate::private::SerializedValue< '_a > { + $crate::private::JsSerializable::into_js( value.as_ref().unwrap(), arena ) + } + + #[inline] + fn memory_required_owned( &self ) -> usize { + $crate::private::JsSerializable::memory_required( self ) + } + } + + impl< '_r, $($impl_arg)* > $crate::private::JsSerializableOwned for &'_r $($kind_arg)* where $($bounds)* { + #[inline] + fn into_js_owned< '_a >( value: &'_a mut Option< Self >, arena: &'_a $crate::private::PreallocatedArena ) -> $crate::private::SerializedValue< '_a > { + $crate::private::JsSerializable::into_js( value.unwrap(), arena ) + } + + #[inline] + fn memory_required_owned( &self ) -> usize { + $crate::private::JsSerializable::memory_required( *self ) + } + } + }; +} + +macro_rules! __reference_boilerplate { + ($kind:ident, instanceof $js_name:ident $($rest:tt)*) => { + impl $crate::private::FromReference for $kind { + #[inline] + fn from_reference( reference: Reference ) -> Option< Self > { + if instanceof!( reference, $js_name ) { + Some( $kind( reference ) ) + } else { + None + } + } + } + + __reference_boilerplate!( $kind, $($rest)* ); + }; + + ($kind:ident, convertible to $base_kind:ident $($rest:tt)*) => { + impl From< $kind > for $base_kind { + #[inline] + fn from( value: $kind ) -> Self { + use $crate::private::FromReferenceUnchecked; + let reference: $crate::Reference = value.into(); + unsafe { + $base_kind::from_reference_unchecked( reference ) + } + } + } + + __reference_boilerplate!( $kind, $($rest)* ); + }; + + ($kind:ident,) => { + impl ::std::fmt::Debug for $kind { + fn fmt( &self, formatter: &mut ::std::fmt::Formatter ) -> ::std::fmt::Result { + write!( formatter, concat!( "<", stringify!( $kind ), ":{}>" ), self.0.as_raw() ) + } + } + + impl Clone for $kind { + #[inline] + fn clone( &self ) -> Self { + $kind( self.0.clone() ) + } + } + + impl AsRef< $crate::Reference > for $kind { + #[inline] + fn as_ref( &self ) -> &$crate::Reference { + &self.0 + } + } + + impl $crate::private::FromReferenceUnchecked for $kind { + #[inline] + unsafe fn from_reference_unchecked( reference: $crate::Reference ) -> Self { + $kind( reference ) + } + } + + impl From< $kind > for $crate::Reference { + #[inline] + fn from( value: $kind ) -> Self { + value.0 + } + } + + impl $crate::unstable::TryFrom< $kind > for $crate::Reference { + type Error = $crate::unstable::Void; + + #[inline] + fn try_from( value: $kind ) -> Result< Self, Self::Error > { + Ok( value.0 ) + } + } + + impl< T: $crate::unstable::TryInto< $crate::Reference > > $crate::unstable::TryFrom< T > for $kind + where >::Error: Into< Box< ::std::error::Error > > + { + type Error = Box< ::std::error::Error >; // TODO + + #[inline] + fn try_from( value: T ) -> Result< Self, Self::Error > { + value.try_into() + .map_err( |error| error.into() ) + .and_then( |reference: Reference| reference.downcast().ok_or_else( || "reference is of a different type".into() ) ) + } + } + + impl $crate::private::JsSerializable for $kind { + #[inline] + fn into_js< 'a >( &'a self, arena: &'a $crate::private::PreallocatedArena ) -> $crate::private::SerializedValue< 'a > { + self.0.into_js( arena ) + } + + #[inline] + fn memory_required( &self ) -> usize { + Reference::memory_required( &self.0 ) + } + } + + __js_serializable_boilerplate!( $kind ); + }; +} + +macro_rules! reference_boilerplate { + ($kind:ident, $($rest:tt)*) => { + __reference_boilerplate!( $kind, $($rest)* ); + } +} + +macro_rules! instanceof { + ($value:expr, $kind:ident) => {{ + use $crate::unstable::TryInto; + let reference: Option< &$crate::Reference > = (&$value).try_into().ok(); + reference.map( |reference| { + em_asm_int!( + concat!( "return (Module.STDWEB.acquire_js_reference( $0 ) instanceof ", stringify!( $kind ), ") | 0;" ), + reference.as_raw() + ) == 1 + }).unwrap_or( false ) + }}; +} + +#[cfg(test)] +mod tests { + macro_rules! stringify_js { + ($($token:tt)*) => { + _js_impl!( @stringify [] -> $($token)* ) + }; + } + + #[test] + fn stringify() { + assert_eq!( stringify_js! { console.log }, "console.log" ); + assert_eq!( stringify_js! { 1.0 }, "1.0" ); + assert_eq!( stringify_js! { [ 1.0 ] }, "[1.0]" ); + assert_eq!( stringify_js! { { 1.0 } }, "{1.0}" ); + assert_eq!( stringify_js! { ( 1.0 ) }, "(1.0)" ); + assert_eq!( stringify_js! { a b }, "a b" ); + assert_eq!( stringify_js! { === }, "===" ); + assert_eq!( stringify_js! { ++i }, "++i" ); + assert_eq!( stringify_js! { i++ }, "i ++" ); + assert_eq!( stringify_js! { --i }, "--i" ); + assert_eq!( stringify_js! { i-- }, "i --" ); + assert_eq!( stringify_js! { ( @{1} ); }.replace( " ", "" ), "(Module.STDWEB.to_js($1));" ); + assert_eq!( + stringify_js! { + console.log( "Hello!", @{1234i32} ); + }.replace( " ", "" ), + "console.log(\"Hello!\",Module.STDWEB.to_js($1));" + ); + assert_eq!( + stringify_js! { + @{a}.fn( @{b} ); + }.replace( " ", "" ), + "Module.STDWEB.to_js($1).fn(Module.STDWEB.to_js($2));" + ); + } +} diff --git a/src/webcore/mod.rs b/src/webcore/mod.rs new file mode 100644 index 00000000..66b1c14c --- /dev/null +++ b/src/webcore/mod.rs @@ -0,0 +1,11 @@ +#[macro_use] +pub mod macros; +pub mod initialization; +pub mod value; +pub mod number; +pub mod serialization; +pub mod ffi; +pub mod callfn; +pub mod newtype; +pub mod try_from; +pub mod void; diff --git a/src/webcore/newtype.rs b/src/webcore/newtype.rs new file mode 100644 index 00000000..7c49cee8 --- /dev/null +++ b/src/webcore/newtype.rs @@ -0,0 +1,77 @@ +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::fmt; + +// Unfortunately Rust doesn't allow something like this: +// impl< A, T: Trait< A > > AnotherTrait for T {} +// It will simply return `unconstrained type parameter` error. +// +// To work around this we create a dummy newtype which will +// artificially constraint the extra parameter. +#[doc(hidden)] +pub struct Newtype< A, T >( T, PhantomData< A > ); + +impl< A, T > Newtype< A, T > { + #[doc(hidden)] + #[inline] + pub fn unwrap_newtype( self ) -> T { + self.0 + } +} + +#[doc(hidden)] +pub trait IntoNewtype< A >: Sized { + fn into_newtype( self ) -> Newtype< A, Self >; +} + +impl< A, T > IntoNewtype< A > for T { + #[inline] + fn into_newtype( self ) -> Newtype< A, Self > { + Newtype( self, PhantomData ) + } +} + +impl< A, T > Deref for Newtype< A, T > { + type Target = T; + + #[inline] + fn deref( &self ) -> &Self::Target { + &self.0 + } +} + +impl< A, T > DerefMut for Newtype< A, T > { + #[inline] + fn deref_mut( &mut self ) -> &mut Self::Target { + &mut self.0 + } +} + +impl< A, T: Copy > Copy for Newtype< A, T > {} +impl< A, T: Clone > Clone for Newtype< A, T > { + #[inline] + fn clone( &self ) -> Self { + Newtype( self.0.clone(), PhantomData ) + } +} + +impl< A, T > AsRef< T > for Newtype< A, T > { + #[inline] + fn as_ref( &self ) -> &T { + &self.0 + } +} + +impl< A, T > AsMut< T > for Newtype< A, T > { + #[inline] + fn as_mut( &mut self ) -> &mut T { + &mut self.0 + } +} + +impl< A, T: fmt::Debug > fmt::Debug for Newtype< A, T > { + #[inline] + fn fmt( &self, formatter: &mut fmt::Formatter ) -> Result< (), fmt::Error > { + self.0.fmt( formatter ) + } +} diff --git a/src/webcore/number.rs b/src/webcore/number.rs new file mode 100644 index 00000000..f6cbefd9 --- /dev/null +++ b/src/webcore/number.rs @@ -0,0 +1,650 @@ +// TODO: Handle NaN and Infinity. + +use std::{i8, i16, i32, i64, u8, u16, u32, u64, f32, f64}; +use std::error; +use std::fmt; +use webcore::try_from::TryFrom; + +// 2^53 - 1 +const MAX_SAFE_INTEGER_F64: i64 = 9007199254740991; +const MIN_SAFE_INTEGER_F64: i64 = -9007199254740991; + +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum Storage { + // Technically JavaScript only has f64 numbers, however at least the V8 + // treats numbers which can be represented as 31-bit integers more optimally. + // + // Now, I have absolutely no idea if doing this is worth it as opposed to + // just sticking with always using f64; it's definitely worth investigating + // in the future. + I32( i32 ), + F64( f64 ) +} + +/// A type representing a JavaScript number. +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct Number( Storage ); + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum ConversionError { + OutOfRange, + NotAnInteger +} + +impl fmt::Display for ConversionError { + fn fmt( &self, formatter: &mut fmt::Formatter ) -> Result< (), fmt::Error > { + let message = error::Error::description( self ); + write!( formatter, "{}", message ) + } +} + +impl error::Error for ConversionError { + fn description( &self ) -> &str { + match *self { + ConversionError::OutOfRange => "number out of range", + ConversionError::NotAnInteger => "number not an integer" + } + } +} + +// We don't want to make the inner value public, hence this accessor. +#[inline] +pub fn get_storage( number: &Number ) -> &Storage { + &number.0 +} + +impl AsRef< Number > for Number { + #[inline] + fn as_ref( &self ) -> &Self { + self + } +} + +impl From< i8 > for Number { + #[inline] + fn from( value: i8 ) -> Self { + Number( Storage::I32( value as i32 ) ) + } +} + +impl From< i16 > for Number { + #[inline] + fn from( value: i16 ) -> Self { + Number( Storage::I32( value as i32 ) ) + } +} + +impl From< i32 > for Number { + #[inline] + fn from( value: i32 ) -> Self { + Number( Storage::I32( value ) ) + } +} + +impl TryFrom< i64 > for Number { + type Error = ConversionError; + + fn try_from( value: i64 ) -> Result< Self, Self::Error > { + if value >= i32::MIN as i64 && value <= i32::MAX as i64 { + Ok( Number( Storage::I32( value as i32 ) ) ) + } else if value >= MIN_SAFE_INTEGER_F64 && value <= MAX_SAFE_INTEGER_F64 { + Ok( Number( Storage::F64( value as f64 ) ) ) + } else { + Err( ConversionError::OutOfRange ) + } + } +} + +impl From< u8 > for Number { + #[inline] + fn from( value: u8 ) -> Self { + Number( Storage::I32( value as i32 ) ) + } +} + +impl From< u16 > for Number { + #[inline] + fn from( value: u16 ) -> Self { + Number( Storage::I32( value as i32 ) ) + } +} + +impl From< u32 > for Number { + #[inline] + fn from( value: u32 ) -> Self { + if value <= i32::MAX as u32 { + Number( Storage::I32( value as i32 ) ) + } else { + Number( Storage::F64( value as f64 ) ) + } + } +} + +impl TryFrom< u64 > for Number { + type Error = ConversionError; + + fn try_from( value: u64 ) -> Result< Self, Self::Error > { + if value <= i32::MAX as u64 { + Ok( Number( Storage::I32( value as i32 ) ) ) + } else if value <= MAX_SAFE_INTEGER_F64 as u64 { + Ok( Number( Storage::F64( value as f64 ) ) ) + } else { + Err( ConversionError::OutOfRange ) + } + } +} + +impl From< f32 > for Number { + #[inline] + fn from( value: f32 ) -> Self { + Number( Storage::F64( value as f64 ) ) + } +} + +impl From< f64 > for Number { + #[inline] + fn from( value: f64 ) -> Self { + Number( Storage::F64( value ) ) + } +} + +macro_rules! impl_trivial_try_from { + ($($kind:ty),+) => { + $( + impl TryFrom< $kind > for Number { + type Error = $crate::unstable::Void; + + #[inline] + fn try_from( value: $kind ) -> Result< Self, Self::Error > { + Ok( value.into() ) + } + } + )+ + } +} + +impl_trivial_try_from!( i8, i16, i32, u8, u16, u32, f32, f64 ); + +macro_rules! impl_conversion_into_rust_types { + ($(into $($kind:tt),+: { from i32: $integer_callback:ident, from f64: $float_callback:ident }),+) => { + $( + $( + impl TryFrom< Number > for $kind { + type Error = ConversionError; + + #[allow(trivial_numeric_casts)] + fn try_from( number: Number ) -> Result< Self, Self::Error > { + match number.0 { + Storage::I32( value ) => { + $integer_callback!( value, $kind ) + }, + Storage::F64( value ) => { + $float_callback!( value, $kind ) + } + } + } + } + )+ + )+ + } +} + +macro_rules! i32_to_small_integer { + ($value:expr, $kind:tt) => { + if $value <= $kind::MAX as i32 && $value >= $kind::MIN as i32 { + Ok( $value as $kind ) + } else { + Err( ConversionError::OutOfRange ) + } + } +} + +macro_rules! direct_cast { + ($value:expr, $kind:tt) => { + Ok( $value as $kind ) + } +} + +macro_rules! i32_to_big_unsigned_integer { + ($value:expr, $kind:tt) => { + if $value >= 0 { + Ok( $value as $kind ) + } else { + Err( ConversionError::OutOfRange ) + } + } +} + +macro_rules! f64_to_integer { + ($value:expr, $kind:tt) => {{ + if $value.floor() != $value { + return Err( ConversionError::NotAnInteger ); + } + + if $value <= $kind::MAX as f64 && $value >= $kind::MIN as f64 { + Ok( $value as $kind ) + } else { + Err( ConversionError::OutOfRange ) + } + }} +} + +impl_conversion_into_rust_types! { + into i8, i16, i32, u8, u16: { + from i32: i32_to_small_integer, + from f64: f64_to_integer + }, + into i64: { + from i32: direct_cast, + from f64: f64_to_integer + }, + into u32, u64: { + from i32: i32_to_big_unsigned_integer, + from f64: f64_to_integer + }, + into f64: { + from i32: direct_cast, + from f64: direct_cast + } +} + +impl PartialEq< i8 > for Number { + #[inline] + fn eq( &self, right: &i8 ) -> bool { + match self.0 { + Storage::I32( left ) => left == *right as i32, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< i16 > for Number { + #[inline] + fn eq( &self, right: &i16 ) -> bool { + match self.0 { + Storage::I32( left ) => left == *right as i32, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< i32 > for Number { + #[inline] + fn eq( &self, right: &i32 ) -> bool { + match self.0 { + Storage::I32( left ) => left == *right, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< i64 > for Number { + #[inline] + fn eq( &self, right: &i64 ) -> bool { + match self.0 { + Storage::I32( left ) => left as i64 == *right, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< u8 > for Number { + #[inline] + fn eq( &self, right: &u8 ) -> bool { + match self.0 { + Storage::I32( left ) => left == *right as i32, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< u16 > for Number { + #[inline] + fn eq( &self, right: &u16 ) -> bool { + match self.0 { + Storage::I32( left ) => left == *right as i32, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< u32 > for Number { + #[inline] + fn eq( &self, right: &u32 ) -> bool { + match self.0 { + Storage::I32( left ) => left as i64 == *right as i64, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< u64 > for Number { + #[inline] + fn eq( &self, right: &u64 ) -> bool { + match self.0 { + Storage::I32( left ) => { + if *right >= i32::MAX as u64 { + // The right side is not convertible to an i32. + return false; + } + left == *right as i32 + }, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< f32 > for Number { + #[inline] + fn eq( &self, right: &f32 ) -> bool { + match self.0 { + Storage::I32( left ) => left as f64 == *right as f64, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< f64 > for Number { + #[inline] + fn eq( &self, right: &f64 ) -> bool { + match self.0 { + Storage::I32( left ) => left as f64 == *right, + Storage::F64( left ) => left == *right + } + } +} + +macro_rules! impl_symmetric_partial_eq { + ( $($kind:tt),+ ) => { + $( + impl PartialEq< Number > for $kind { + #[inline] + fn eq( &self, right: &Number ) -> bool { + right == self + } + } + )+ + } +} + +impl_symmetric_partial_eq! { + u8, u16, u32, u64, + i8, i16, i32, i64, + f32, f64 +} + +#[cfg(test)] +mod tests { + use super::*; + use std::mem; + use std::u8; + + // 2^23 - 1 + const MAX_SAFE_INTEGER_F32: i32 = 8388607; + const MIN_SAFE_INTEGER_F32: i32 = -8388607; + + trait ExampleValues: Sized { + fn example_values() -> Vec< Self >; + } + + macro_rules! example_values { + ( + $($kind:tt => [$($value:expr),+]),+ + ) => { + $( + impl ExampleValues for $kind { + fn example_values() -> Vec< Self > { vec![ $($value),+ ] } + } + )+ + } + } + + example_values! { + u8 => [ 0, 1, u8::MAX - 1, u8::MAX ], + u16 => [ 0, 1, u16::MAX - 1, u16::MAX ], + u32 => [ 0, 1, u32::MAX - 1, u32::MAX ], + u64 => [ 0, 1, u64::MAX - 1, u64::MAX ], + i8 => [ i8::MIN, i8::MIN + 1, -1, 0, 1, i8::MAX - 1, i8::MAX ], + i16 => [ i16::MIN, i16::MIN + 1, -1, 0, 1, i16::MAX - 1, i16::MAX ], + i32 => [ i32::MIN, i32::MIN + 1, -1, 0, 1, i32::MAX - 1, i32::MAX ], + i64 => [ i64::MIN, i64::MIN + 1, -1, 0, 1, i64::MAX - 1, i64::MAX ], + f32 => [ + f32::MIN, f32::MIN + 1.0, -1.0, 0.0, 1.0, f32::MAX - 1.0, f32::MAX, + -0.33, 0.33, -3.33, 3.33, + MIN_SAFE_INTEGER_F32 as f32, + MIN_SAFE_INTEGER_F32 as f32 - 100.0, + MAX_SAFE_INTEGER_F32 as f32, + MAX_SAFE_INTEGER_F32 as f32 + 100.0 + ], + f64 => [ + f64::MIN, f64::MIN + 1.0, -1.0, 0.0, 1.0, f64::MAX - 1.0, f64::MAX, + -0.33, 0.33, -3.33, 3.33, + MIN_SAFE_INTEGER_F32 as f64, + MIN_SAFE_INTEGER_F32 as f64 - 100.0, + MAX_SAFE_INTEGER_F32 as f64, + MAX_SAFE_INTEGER_F32 as f64 + 100.0, + MIN_SAFE_INTEGER_F64 as f64, + MIN_SAFE_INTEGER_F64 as f64 - 100.0, + MAX_SAFE_INTEGER_F64 as f64, + MAX_SAFE_INTEGER_F64 as f64 + 100.0 + ] + } + + #[derive(Copy, Clone, PartialEq, Eq, Debug)] + enum Kind { + U, + I, + F + } + + macro_rules! type_kind { + (u8) => (U); + (u16) => (U); + (u32) => (U); + (u64) => (U); + (i8) => (I); + (i16) => (I); + (i32) => (I); + (i64) => (I); + (f32) => (F); + (f64) => (F); + } + + macro_rules! is_convertible { + ($value:expr, $src_type:tt => $dst_type:tt) => {{ + let src_size = mem::size_of::< $src_type >(); + let dst_size = mem::size_of::< $dst_type >(); + let src_kind = type_kind!( $src_type ); + let dst_kind = type_kind!( $dst_type ); + + use self::Kind::*; + match (src_kind, dst_kind) { + (F, F) => { + if dst_size >= src_size { + true + } else /* dst_size < src_size */ { + let value = $value as f64; + let in_range = match dst_size { + 4 => value <= MAX_SAFE_INTEGER_F32 as f64 && value >= MIN_SAFE_INTEGER_F32 as f64, + _ => unreachable!() + }; + + in_range && ($value as $dst_type) as $src_type == $value + } + }, + (U, U) | (I, I) => { + if dst_size >= src_size { + true + } else /* dst_size < src_size */ { + $value >= ($dst_type::MIN as $src_type) && + $value <= ($dst_type::MAX as $src_type) + } + }, + (U, I) => { + if dst_size > src_size { + true + } else /* dst_size <= src_size */ { + $value <= ($dst_type::MAX as $src_type) + } + }, + (I, U) => { + if $value < (0 as $src_type) { + false + } else if dst_size >= src_size { + true + } else /* dst_size < src_size */ { + $value <= ($dst_type::MAX as $src_type) + } + }, + (F, U) => { + ($value as f64).floor() == ($value as f64) && + ($value as f64) >= ($dst_type::MIN as f64) && + ($value as f64) <= ($dst_type::MAX as f64) + }, + (F, I) => { + ($value as f64).floor() == ($value as f64) && + ($value as f64) >= ($dst_type::MIN as f64) && + ($value as f64) <= ($dst_type::MAX as f64) + }, + (I, F) => { + let value = $value as i64; + match dst_size { + 4 => value <= MAX_SAFE_INTEGER_F32 as i64 && value >= MIN_SAFE_INTEGER_F32 as i64, + 8 => value <= MAX_SAFE_INTEGER_F64 as i64 && value >= MIN_SAFE_INTEGER_F64 as i64, + _ => unreachable!() + } + }, + (U, F) => { + let value = $value as u64; + match dst_size { + 4 => value <= MAX_SAFE_INTEGER_F32 as u64, + 8 => value <= MAX_SAFE_INTEGER_F64 as u64, + _ => unreachable!() + } + } + } + }} + } + + macro_rules! conversion_test_body { + ($value:expr, $src_type:tt, $dst_type:tt) => {{ + let is_convertible_into_number = is_convertible!( $value, $src_type => f64 ); + let number: Result< Number, _ > = $value.try_into(); + let number = match number { + Ok( number ) => { + if !is_convertible_into_number { + panic!( "Type {} should NOT be convertible into Number yet it is: {:?}", stringify!( $src_type ), $value ); + } + number + }, + Err( _ ) => { + if is_convertible_into_number { + panic!( "Type {} should be convertible into Number yet it isn't: {:?}", stringify!( $src_type ), $value ); + } + return; + } + }; + + let is_convertible = is_convertible!( $value, $src_type => $dst_type ); + let output = number.try_into(); + if is_convertible { + let result = output == Ok( $value as $dst_type ); + assert!( result, "Conversion should succeed yet it didn't for {:?}", $value ); + } else { + let result = output.is_err(); + assert!( result, "Conversion should NOT succeed yet it did for {:?}", $value ); + }; + }} + } + + macro_rules! conversion_test { + ($src_type:tt, $(($dst_type:tt, $test_name:ident)),+) => { + $( + #[allow(trivial_numeric_casts, const_err)] + #[test] + fn $test_name() { + for value in $src_type::example_values() { + conversion_test_body!( value, $src_type, $dst_type ); + } + } + )+ + } + } + + macro_rules! generate_conversion_tests { + ($raw_type:tt) => { + conversion_test! { + $raw_type, + (u8, into_u8), + (u16, into_u16), + (u32, into_u32), + (u64, into_u64), + (i8, into_i8), + (i16, into_i16), + (i32, into_i32), + (i64, into_i64), + // No conversion to f32. + (f64, into_f64) + } + } + } + + macro_rules! generate_number_tests { + ($(($raw_type:tt, $name:ident)),+) => { + $( + mod $name { + use super::*; + use webcore::try_from::TryInto; + + #[test] + fn round_trip() { + for left in $raw_type::example_values() { + let number: Number = left.into(); + let right: $raw_type = number.try_into().unwrap(); + assert!( left == right, "Failed for: {:?}", left ); + } + } + + #[test] + fn equality() { + for value in $raw_type::example_values() { + let number: Number = value.into(); + assert!( number == value, "Failed for: {:?}", value ); + assert!( value == number, "Failed for: {:?}", value ); + } + } + + generate_conversion_tests! { $raw_type } + } + )+ + } + } + + generate_number_tests! { + (u8, for_u8), + (u16, for_u16), + (u32, for_u32), + (i8, for_i8), + (i16, for_i16), + (i32, for_i32), + (f64, for_f64) + } + + mod for_f32 { + use super::*; + use webcore::try_from::TryInto; + generate_conversion_tests! { f32 } + } + + mod for_u64 { + use super::*; + use webcore::try_from::TryInto; + generate_conversion_tests! { u64 } + } + + mod for_i64 { + use super::*; + use webcore::try_from::TryInto; + generate_conversion_tests! { u64 } + } +} diff --git a/src/webcore/serialization.rs b/src/webcore/serialization.rs new file mode 100644 index 00000000..719039eb --- /dev/null +++ b/src/webcore/serialization.rs @@ -0,0 +1,1016 @@ +use std::mem; +use std::slice; +use std::i32; +use std::collections::BTreeMap; +use std::marker::PhantomData; +use std::cell::{Cell, UnsafeCell}; + +use webcore::ffi; +use webcore::callfn::CallMut; +use webcore::newtype::Newtype; +use webcore::try_from::{TryFrom, TryInto}; +use webcore::number::Number; + +use webcore::value::{ + Null, + Undefined, + Reference, + Value +}; + +#[repr(u8)] +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum Tag { + Undefined = 0, + Null = 1, + I32 = 2, + F64 = 3, + Str = 4, + False = 5, + True = 6, + Array = 7, + Object = 8, + Reference = 9, + Function = 10 +} + +impl Default for Tag { + #[inline] + fn default() -> Self { + Tag::Undefined + } +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct PreallocatedArena { + saved: UnsafeCell< Vec< Value > >, + buffer: UnsafeCell< Vec< u8 > >, + index: Cell< usize > +} + +impl PreallocatedArena { + #[doc(hidden)] + #[inline] + pub fn new( memory_required: usize ) -> Self { + let mut buffer = Vec::new(); + buffer.reserve( memory_required ); + unsafe { + buffer.set_len( memory_required ); + } + + PreallocatedArena { + saved: UnsafeCell::new( Vec::new() ), + buffer: UnsafeCell::new( buffer ), + index: Cell::new( 0 ) + } + } + + #[doc(hidden)] + #[inline] + pub fn reserve< T >( &self, count: usize ) -> &mut [T] { + let bytes = mem::size_of::< T >() * count; + let slice = unsafe { + let mut buffer = &mut *self.buffer.get(); + debug_assert!( self.index.get() + bytes <= buffer.len() ); + + slice::from_raw_parts_mut( + buffer.as_mut_ptr().offset( self.index.get() as isize ) as *mut T, + count + ) + }; + + self.index.set( self.index.get() + bytes ); + slice + } + + #[doc(hidden)] + #[inline] + pub fn save( &self, value: Value ) -> &Value { + unsafe { + let mut saved = &mut *self.saved.get(); + saved.push( value ); + &*(saved.last().unwrap() as *const Value) + } + } + + #[doc(hidden)] + #[inline] + pub fn assert_no_free_space_left( &self ) { + debug_assert!( self.index.get() == unsafe { &*self.buffer.get() }.len() ); + } +} + +#[doc(hidden)] +pub trait JsSerializableOwned: Sized { + fn into_js_owned< 'a >( value: &'a mut Option< Self >, arena: &'a PreallocatedArena ) -> SerializedValue< 'a >; + fn memory_required_owned( &self ) -> usize; +} + +#[doc(hidden)] +pub trait JsSerializable { + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a >; + fn memory_required( &self ) -> usize; +} + +// This is a generic structure for serializing every JavaScript value. +#[doc(hidden)] +#[repr(C)] +#[derive(Default, Debug)] +pub struct SerializedValue< 'a > { + data_1: u64, + data_2: u32, + tag: Tag, + phantom: PhantomData< &'a () > +} + +#[test] +fn test_serialized_value_size() { + assert_eq!( mem::size_of::< SerializedValue< 'static > >(), 16 ); +} + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedUndefined; + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedNull; + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedI32 { + value: i32 +} + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedF64 { + value: f64 +} + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedTrue {} + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedFalse {} + +#[repr(C)] +#[derive(Clone, Debug)] +struct SerializedUntaggedString { + pointer: u32, + length: u32 +} + +#[repr(C)] +#[derive(Clone, Debug)] +struct SerializedUntaggedArray { + pointer: u32, + length: u32 +} + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedObject { + value_pointer: u32, + length: u32, + key_pointer: u32 +} + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedReference { + refid: i32 +} + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedFunction { + adapter_pointer: u32, + pointer: u32, + deallocator_pointer: u32 +} + +impl SerializedUntaggedString { + #[inline] + fn deserialize( &self ) -> String { + let pointer = self.pointer as *mut u8; + let length = self.length as usize; + + unsafe { + let vector = Vec::from_raw_parts( pointer, length, length + 1 ); + String::from_utf8_unchecked( vector ) + } + } +} + +impl SerializedUntaggedArray { + #[inline] + fn deserialize( &self ) -> Vec< Value > { + let pointer = self.pointer as *const SerializedValue; + let length = self.length as usize; + let slice = unsafe { + slice::from_raw_parts( pointer, length ) + }; + + let vector = slice.iter().map( |value| value.deserialize() ).collect(); + unsafe { + ffi::free( pointer as *const u8 ); + } + + vector + } +} + +impl SerializedUntaggedObject { + #[inline] + fn deserialize( &self ) -> BTreeMap< String, Value > { + let length = self.length as usize; + let key_pointer = self.key_pointer as *const SerializedUntaggedString; + let value_pointer = self.value_pointer as *const SerializedValue; + + let key_slice = unsafe { slice::from_raw_parts( key_pointer, length ) }; + let value_slice = unsafe { slice::from_raw_parts( value_pointer, length ) }; + + let map = key_slice.iter().zip( value_slice.iter() ).map( |(key, value)| { + let key = key.deserialize(); + let value = value.deserialize(); + (key, value) + }).collect(); + + unsafe { + ffi::free( key_pointer as *const u8 ); + ffi::free( value_pointer as *const u8 ); + } + + map + } +} + +impl SerializedUntaggedReference { + #[inline] + fn deserialize( &self ) -> Reference { + unsafe { Reference::from_raw_unchecked( self.refid ) } + } +} + +macro_rules! untagged_boilerplate { + ($tests_namespace:ident, $reader_name:ident, $tag:expr, $untagged_type:ident) => { + impl< 'a > SerializedValue< 'a > { + #[allow(dead_code)] + #[inline] + fn $reader_name( &self ) -> &$untagged_type { + debug_assert_eq!( self.tag, $tag ); + unsafe { + &*(self as *const _ as *const $untagged_type) + } + } + } + + impl< 'a > From< $untagged_type > for SerializedValue< 'a > { + #[inline] + fn from( untagged: $untagged_type ) -> Self { + unsafe { + let mut value: SerializedValue = mem::uninitialized(); + *(&mut value as *mut SerializedValue as *mut $untagged_type) = untagged; + value.tag = $tag; + value + } + } + } + + #[cfg(test)] + mod $tests_namespace { + use super::*; + + #[test] + fn does_not_overlap_with_the_tag() { + let size = mem::size_of::< $untagged_type >(); + let tag_offset = unsafe { &(&*(0 as *const SerializedValue< 'static >)).tag as *const _ as usize }; + assert!( size <= tag_offset ); + } + } + } +} + +untagged_boilerplate!( test_undefined, as_undefined, Tag::Undefined, SerializedUntaggedUndefined ); +untagged_boilerplate!( test_null, as_null, Tag::Null, SerializedUntaggedNull ); +untagged_boilerplate!( test_i32, as_i32, Tag::I32, SerializedUntaggedI32 ); +untagged_boilerplate!( test_f64, as_f64, Tag::F64, SerializedUntaggedF64 ); +untagged_boilerplate!( test_true, as_true, Tag::True, SerializedUntaggedTrue ); +untagged_boilerplate!( test_false, as_false, Tag::False, SerializedUntaggedFalse ); +untagged_boilerplate!( test_object, as_object, Tag::Object, SerializedUntaggedObject ); +untagged_boilerplate!( test_string, as_string, Tag::Str, SerializedUntaggedString ); +untagged_boilerplate!( test_array, as_array, Tag::Array, SerializedUntaggedArray ); +untagged_boilerplate!( test_reference, as_reference, Tag::Reference, SerializedUntaggedReference ); +untagged_boilerplate!( test_function, as_function, Tag::Function, SerializedUntaggedFunction ); + +impl< 'a > SerializedValue< 'a > { + #[doc(hidden)] + #[inline] + pub fn deserialize( &self ) -> Value { + match self.tag { + Tag::Undefined => Value::Undefined, + Tag::Null => Value::Null, + Tag::I32 => self.as_i32().value.into(), + Tag::F64 => self.as_f64().value.into(), + Tag::Str => Value::String( self.as_string().deserialize() ), + Tag::False => Value::Bool( false ), + Tag::True => Value::Bool( true ), + Tag::Array => Value::Array( self.as_array().deserialize() ), + Tag::Object => Value::Object( self.as_object().deserialize() ), + Tag::Reference => self.as_reference().deserialize().into(), + Tag::Function => unreachable!() + } + } +} + +impl JsSerializable for () { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedUndefined.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( () ); + +impl JsSerializable for Undefined { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedUndefined.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( Undefined ); + +impl JsSerializable for Null { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedNull.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( Null ); + +impl JsSerializable for Reference { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedReference { + refid: self.as_raw() + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( Reference ); + +impl JsSerializable for bool { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + if *self { + SerializedUntaggedTrue {}.into() + } else { + SerializedUntaggedFalse {}.into() + } + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( bool ); + +impl JsSerializable for str { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedString { + pointer: self.as_ptr() as u32, + length: self.len() as u32 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( impl< 'a > for &'a str ); + +impl JsSerializable for String { + #[inline] + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a > { + self.as_str().into_js( arena ) + } + + #[inline] + fn memory_required( &self ) -> usize { + self.as_str().memory_required() + } +} + +__js_serializable_boilerplate!( String ); + +impl JsSerializable for i8 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedI32 { + value: *self as i32 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + (*self as i32).memory_required() + } +} + +__js_serializable_boilerplate!( i8 ); + +impl JsSerializable for i16 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedI32 { + value: *self as i32 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + (*self as i32).memory_required() + } +} + +__js_serializable_boilerplate!( i16 ); + +impl JsSerializable for i32 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedI32 { + value: *self + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( i32 ); + +impl JsSerializable for u8 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedI32 { + value: *self as i32 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + (*self as i32).memory_required() + } +} + +__js_serializable_boilerplate!( u8 ); + +impl JsSerializable for u16 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedI32 { + value: *self as i32 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + (*self as i32).memory_required() + } +} + +__js_serializable_boilerplate!( u16 ); + +impl JsSerializable for u32 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedF64 { + value: *self as f64 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + (*self as f64).memory_required() + } +} + +__js_serializable_boilerplate!( u32 ); + +impl JsSerializable for f32 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedF64 { + value: *self as f64 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + (*self as f64).memory_required() + } +} + +__js_serializable_boilerplate!( f32 ); + +impl JsSerializable for f64 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedF64 { + value: *self + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( f64 ); + +impl JsSerializable for Number { + #[inline] + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a > { + use webcore::number::{Storage, get_storage}; + match *get_storage( self ) { + Storage::I32( ref value ) => value.into_js( arena ), + Storage::F64( ref value ) => value.into_js( arena ) + } + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( Number ); + +impl< T: JsSerializable > JsSerializable for Option< T > { + #[inline] + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a > { + if let Some( value ) = self.as_ref() { + value.into_js( arena ) + } else { + SerializedUntaggedNull.into() + } + } + + #[inline] + fn memory_required( &self ) -> usize { + if let Some( value ) = self.as_ref() { + value.memory_required() + } else { + 0 + } + } +} + +__js_serializable_boilerplate!( impl< T > for Option< T > where T: JsSerializable ); + +impl< T: JsSerializable > JsSerializable for [T] { + #[inline] + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a > { + let output = arena.reserve( self.len() ); + for (value, output_value) in self.iter().zip( output.iter_mut() ) { + *output_value = value.into_js( arena ); + } + + SerializedUntaggedArray { + pointer: output.as_ptr() as u32, + length: output.len() as u32 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + mem::size_of::< SerializedValue >() * self.len() + + self.iter().fold( 0, |sum, value| sum + value.memory_required() ) + } +} + +__js_serializable_boilerplate!( impl< 'a, T > for &'a [T] where T: JsSerializable ); + +impl< T: JsSerializable > JsSerializable for BTreeMap< String, T > { + #[inline] + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a > { + let keys = arena.reserve( self.len() ); + let values = arena.reserve( self.len() ); + for (((key, value), output_key), output_value) in self.iter().zip( keys.iter_mut() ).zip( values.iter_mut() ) { + *output_key = key.into_js( arena ).as_string().clone(); + *output_value = value.into_js( arena ); + } + + SerializedUntaggedObject { + key_pointer: keys.as_ptr() as u32, + value_pointer: values.as_ptr() as u32, + length: keys.len() as u32 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + mem::size_of::< SerializedValue >() * self.len() + + mem::size_of::< SerializedUntaggedString >() * self.len() + + self.iter().fold( 0, |sum, (key, value)| sum + key.memory_required() + value.memory_required() ) + } +} + +__js_serializable_boilerplate!( impl< T > for BTreeMap< String, T > where T: JsSerializable ); + +impl JsSerializable for Value { + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a > { + match *self { + Value::Undefined => SerializedUntaggedUndefined.into(), + Value::Null => SerializedUntaggedNull.into(), + Value::Bool( ref value ) => value.into_js( arena ), + Value::Number( ref value ) => value.into_js( arena ), + Value::String( ref value ) => value.into_js( arena ), + Value::Array( ref value ) => value.as_slice().into_js( arena ), + Value::Object( ref value ) => value.into_js( arena ), + Value::Reference( ref value ) => value.into_js( arena ) + } + } + + fn memory_required( &self ) -> usize { + match *self { + Value::Undefined => Undefined.memory_required(), + Value::Null => Null.memory_required(), + Value::Bool( value ) => value.memory_required(), + Value::Number( value ) => value.memory_required(), + Value::String( ref value ) => value.memory_required(), + Value::Array( ref value ) => value.as_slice().memory_required(), + Value::Object( ref value ) => value.memory_required(), + Value::Reference( ref value ) => value.memory_required() + } + } +} + +__js_serializable_boilerplate!( Value ); + +macro_rules! impl_for_fn { + ($next:tt => $($kind:ident),*) => { + + impl< $($kind: TryFrom< Value >,)* F > JsSerializableOwned for Newtype< ($($kind,)*), F > + where F: CallMut< ($($kind,)*) > + 'static, F::Output: JsSerializableOwned + { + #[inline] + #[allow(unused_mut, unused_variables, non_snake_case)] + fn into_js_owned< 'a >( value: &'a mut Option< Self >, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + let callback: *mut F = Box::into_raw( Box::new( value.take().unwrap().unwrap_newtype() ) ); + + extern fn funcall_adapter< $($kind: TryFrom< Value >,)* F: CallMut< ($($kind,)*) > >( + callback: *mut F, + raw_arguments: *mut SerializedUntaggedArray + ) where F::Output: JsSerializableOwned + { + let callback = unsafe { &mut *callback }; + let mut arguments = unsafe { &*raw_arguments }.deserialize(); + + unsafe { + ffi::free( raw_arguments as *const u8 ); + } + + if arguments.len() != F::expected_argument_count() { + // TODO: Should probably throw an exception into the JS world or something like that. + panic!( "Expected {} arguments, got {}", F::expected_argument_count(), arguments.len() ); + } + + let mut arguments = arguments.drain( .. ); + let mut nth_argument = 0; + $( + let $kind = match arguments.next().unwrap().try_into() { + Ok( value ) => value, + Err( _ ) => { + panic!( "Argument #{} is not convertible", nth_argument + 1 ); + } + }; + + nth_argument += 1; + )* + + $crate::private::noop( &mut nth_argument ); + + let result = callback.call_mut( ($($kind,)*) ); + let mut arena = PreallocatedArena::new( result.memory_required_owned() ); + + let mut result = Some( result ); + let result = JsSerializableOwned::into_js_owned( &mut result, &mut arena ); + let result = &result as *const _; + + // This is kinda hacky but I'm not sure how else to do it at the moment. + em_asm_int!( "Module.STDWEB.tmp = Module.STDWEB.to_js( $0 );", result ); + } + + extern fn deallocator< $($kind: TryFrom< Value >,)* F: CallMut< ($($kind,)*) > >( callback: *mut F ) { + let callback = unsafe { + Box::from_raw( callback ) + }; + + drop( callback ); + } + + SerializedUntaggedFunction { + adapter_pointer: funcall_adapter::< $($kind,)* F > as u32, + pointer: callback as u32, + deallocator_pointer: deallocator::< $($kind,)* F > as u32 + }.into() + } + + #[inline] + fn memory_required_owned( &self ) -> usize { + 0 + } + } + + next! { $next } + } +} + +loop_through_identifiers!( impl_for_fn ); + +impl< 'a, T: ?Sized + JsSerializable > JsSerializable for &'a T { + #[inline] + fn into_js< 'x >( &'x self, arena: &'x PreallocatedArena ) -> SerializedValue< 'x > { + T::into_js( *self, arena ) + } + + #[inline] + fn memory_required( &self ) -> usize { + T::memory_required( *self ) + } +} + +#[cfg(test)] +mod test_deserialization { + use std::rc::Rc; + use std::cell::Cell; + use super::*; + + #[test] + fn i32() { + assert_eq!( js! { return 100; }, Value::Number( 100_i32.into() ) ); + } + + #[test] + fn f64() { + assert_eq!( js! { return 100.5; }, Value::Number( 100.5_f64.into() ) ); + } + + #[test] + fn bool_true() { + assert_eq!( js! { return true; }, Value::Bool( true ) ); + } + + #[test] + fn bool_false() { + assert_eq!( js! { return false; }, Value::Bool( false ) ); + } + + #[test] + fn undefined() { + assert_eq!( js! { return undefined; }, Value::Undefined ); + } + + #[test] + fn null() { + assert_eq!( js! { return null; }, Value::Null ); + } + + #[test] + fn string() { + assert_eq!( js! { return "Dog"; }, Value::String( "Dog".to_string() ) ); + } + + #[test] + fn array() { + assert_eq!( js! { return [1, 2]; }, Value::Array( vec![ Value::Number( 1.into() ), Value::Number( 2.into() ) ] ) ); + } + + #[test] + fn object() { + assert_eq!( js! { return {"one": 1, "two": 2}; }, Value::Object( [("one".to_string(), Value::Number(1.into())), ("two".to_string(), Value::Number(2.into()))].iter().cloned().collect() ) ); + } + + #[test] + fn reference() { + assert_eq!( js! { return new Date(); }.is_reference(), true ); + } + + #[test] + fn arguments() { + let value = js! { + return (function() { + return arguments; + })( 1, 2 ); + }; + + assert_eq!( value, Value::Array( vec![ Value::Number( 1.into() ), Value::Number( 2.into() ) ] ) ); + } + + #[test] + fn function() { + let value = Rc::new( Cell::new( 0 ) ); + let fn_value = value.clone(); + + js! { + var callback = @{move || { fn_value.set( 1 ); }}; + callback(); + callback.drop(); + }; + + assert_eq!( value.get(), 1 ); + } + + #[test] + fn function_returning_bool() { + let result = js! { + var callback = @{move || { return true }}; + var result = callback(); + callback.drop(); + + return result; + }; + + assert_eq!( result, Value::Bool( true ) ); + } + + #[test] + fn function_with_single_bool_argument() { + let value = Rc::new( Cell::new( false ) ); + let fn_value = value.clone(); + + js! { + var callback = @{move |value: bool| { fn_value.set( value ); }}; + callback( true ); + callback.drop(); + }; + + assert_eq!( value.get(), true ); + } +} + +#[cfg(test)] +mod test_serialization { + use super::*; + + #[test] + fn object() { + let object: BTreeMap< _, _ > = [ + ("number".to_string(), Value::Number( 123.into() )), + ("string".to_string(), Value::String( "Hello!".into() )) + ].iter().cloned().collect(); + + let result = js! { + var object = @{object}; + return object.number === 123 && object.string === "Hello!" && Object.keys( object ).length == 2; + }; + assert_eq!( result, Value::Bool( true ) ); + } + + #[test] + fn multiple() { + let reference: Reference = js! { + return new Date(); + }.try_into().unwrap(); + + let result = js! { + var callback = @{|| {}}; + var reference = @{&reference}; + var string = @{"Hello!"}; + return Object.prototype.toString.call( callback ) == "[object Function]" && + Object.prototype.toString.call( reference ) == "[object Date]" && + Object.prototype.toString.call( string ) == "[object String]" + }; + assert_eq!( result, Value::Bool( true ) ); + } +} + +#[cfg(test)] +mod test_reserialization { + use super::*; + + #[test] + fn i32() { + assert_eq!( js! { return @{100}; }, Value::Number( 100_i32.into() ) ); + } + + #[test] + fn f64() { + assert_eq!( js! { return @{100.5}; }, Value::Number( 100.5_f64.into() ) ); + } + + #[test] + fn bool_true() { + assert_eq!( js! { return @{true}; }, Value::Bool( true ) ); + } + + #[test] + fn bool_false() { + assert_eq!( js! { return @{false}; }, Value::Bool( false ) ); + } + + #[test] + fn undefined() { + assert_eq!( js! { return @{Undefined}; }, Value::Undefined ); + } + + #[test] + fn null() { + assert_eq!( js! { return @{Null}; }, Value::Null ); + } + + #[test] + fn string() { + assert_eq!( js! { return @{"Dog"}; }, Value::String( "Dog".to_string() ) ); + } + + #[test] + fn array() { + let array = vec![ Value::Number( 1.into() ), Value::Number( 2.into() ) ]; + assert_eq!( js! { return @{&[1, 2][..]}; }, Value::Array( array ) ); + } + + #[test] + fn object() { + let object = [("one".to_string(), Value::Number(1.into())), ("two".to_string(), Value::Number(2.into()))].iter().cloned().collect(); + assert_eq!( js! { return @{&object}; }, Value::Object( object ) ); + } + + #[test] + fn reference() { + let date = js! { return new Date(); }; + assert_eq!( js! { return Object.prototype.toString.call( @{date} ) }, "[object Date]" ); + } + + #[test] + fn reference_by_ref() { + let date = js! { return new Date(); }; + assert_eq!( js! { return Object.prototype.toString.call( @{&date} ) }, "[object Date]" ); + } + + #[test] + fn option_some() { + assert_eq!( js! { return @{Some( true )}; }, Value::Bool( true ) ); + } + + #[test] + fn option_none() { + let boolean_none: Option< bool > = None; + assert_eq!( js! { return @{boolean_none}; }, Value::Null ); + } + + #[test] + fn value() { + assert_eq!( js! { return @{Value::String( "Dog".to_string() )}; }, Value::String( "Dog".to_string() ) ); + } + + #[test] + fn closure_context() { + let constant: u32 = 0x12345678; + let callback = move || { + let value: Value = constant.into(); + value + }; + + let value = js! { + return @{callback}(); + }; + + assert_eq!( value, Value::Number( 0x12345678_i32.into() ) ); + } +} diff --git a/src/webcore/try_from.rs b/src/webcore/try_from.rs new file mode 100644 index 00000000..b0278525 --- /dev/null +++ b/src/webcore/try_from.rs @@ -0,0 +1,29 @@ +/// Attempt to construct Self via a conversion. +/// +/// This definition is only temporary until Rust's `TryFrom` is stabilized. +pub trait TryFrom< T >: Sized { + /// The type returned in the event of a conversion error. + type Error; + + /// Performs the conversion. + fn try_from( T ) -> Result< Self, Self::Error >; +} + +/// An attempted conversion that consumes self, which may or may not be expensive. +/// +/// his definition is only temporary until Rust's `TryInto` is stabilized. +pub trait TryInto< T >: Sized { + /// The type returned in the event of a conversion error. + type Error; + + /// Performs the conversion. + fn try_into( self ) -> Result< T, Self::Error >; +} + +impl< T, U > TryInto< U > for T where U: TryFrom< T > { + type Error = U::Error; + #[inline] + fn try_into( self ) -> Result< U, U::Error > { + U::try_from( self ) + } +} diff --git a/src/webcore/value.rs b/src/webcore/value.rs new file mode 100644 index 00000000..4ce44b88 --- /dev/null +++ b/src/webcore/value.rs @@ -0,0 +1,1137 @@ +use std::collections::{BTreeMap, HashMap}; +use std::hash::Hash; +use std::fmt; +use std::error; +use webcore::try_from::{TryFrom, TryInto}; +use webcore::number::{self, Number}; + +/// A unit type representing JavaScript's `undefined`. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub struct Undefined; + +/// A unit type representing JavaScript's `null`. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub struct Null; + +/// A type representing a reference to a JavaScript value. +#[repr(C)] +#[derive(PartialEq, Eq, Debug)] +pub struct Reference( i32 ); + +impl Reference { + #[doc(hidden)] + #[inline] + pub unsafe fn from_raw_unchecked( refid: i32 ) -> Reference { + em_asm_int!( "Module.STDWEB.increment_refcount( $0 );", refid ); + Reference( refid ) + } + + #[doc(hidden)] + #[inline] + pub fn as_raw( &self ) -> i32 { + self.0 + } + + /// Converts this reference into the given type `T`; checks whenever the reference + /// is really of type `T` and returns `None` if it's not. + #[inline] + pub fn downcast< T: FromReference >( self ) -> Option< T > { + T::from_reference( self ) + } +} + +impl Clone for Reference { + #[inline] + fn clone( &self ) -> Self { + unsafe { + Reference::from_raw_unchecked( self.as_raw() ) + } + } +} + +impl Drop for Reference { + #[inline] + fn drop( &mut self ) { + em_asm_int!( "Module.STDWEB.decrement_refcount( $0 );", self.0 ); + } +} + +impl AsRef< Reference > for Reference { + #[inline] + fn as_ref( &self ) -> &Self { + self + } +} + +macro_rules! __impl_infallible_try_from { + (($($impl_arg:tt)*) ($($src_arg:tt)*) ($($dst_arg:tt)*) ($($bounds:tt)*)) => { + impl< $($impl_arg)* > TryFrom< $($src_arg)* > for $($dst_arg)* where $($bounds)* { + type Error = $crate::unstable::Void; + + #[inline] + fn try_from( source: $($src_arg)* ) -> Result< Self, Self::Error > { + Ok( source.into() ) + } + } + }; +} + +macro_rules! impl_infallible_try_from { + (impl< $($impl_arg:tt),* > for $src:ty => $dst:ty where ($($bounds:tt)*); $($rest:tt)*) => { + __impl_infallible_try_from!( ($($impl_arg),*) ($src) ($dst) ($($bounds)*) ); + impl_infallible_try_from!( $($rest)* ); + }; + + (impl< $($impl_arg:tt),* > for $src:ty => $dst:ty; $($rest:tt)*) => { + __impl_infallible_try_from!( ($($impl_arg),*) ($src) ($dst) () ); + impl_infallible_try_from!( $($rest)* ); + }; + + ($src:ty => $dst:ty; $($rest:tt)*) => { + __impl_infallible_try_from!( () ($src) ($dst) () ); + impl_infallible_try_from!( $($rest)* ); + + }; + + () => {}; +} + +impl_infallible_try_from! { + Reference => Reference; + impl< 'a > for &'a Reference => &'a Reference; +} + +#[doc(hidden)] +pub trait FromReferenceUnchecked: Sized { + unsafe fn from_reference_unchecked( reference: Reference ) -> Self; + + #[inline] + unsafe fn from_value_unchecked( value: Value ) -> Option< Self > { + let reference: Option< Reference > = value.try_into().ok(); + reference.map( |reference| Self::from_reference_unchecked( reference ) ) + } +} + +#[doc(hidden)] +pub trait FromReference: FromReferenceUnchecked { + fn from_reference( reference: Reference ) -> Option< Self >; +} + +impl FromReferenceUnchecked for Reference { + #[inline] + unsafe fn from_reference_unchecked( reference: Reference ) -> Self { + reference + } +} + +/// A type representing a JavaScript value. +/// +/// This type implements a rich set of conversions +/// from and into standard Rust types, for example: +/// +/// ```rust +/// let v1: Value = "Hello world!".into(); +/// let v2: Value = true.into(); +/// let v3: Value = vec![ 1, 2, 3 ].into(); +/// let v4: Value = Null.into(); +/// let v5: Value = 123_u64.try_into().unwrap(); +/// +/// let v1_r: String = v1.try_into().unwrap(); +/// let v2_r: bool = v2.try_into().unwrap(); +/// let v3_r: Vec< i32 > = v3.try_into().unwrap(); +/// let v4_r: Option< String > = v4.try_into().unwrap(); // Will be `None`. +/// let v5_r: u64 = v5.try_into().unwrap(); +/// ``` +#[allow(missing_docs)] +#[derive(Clone, PartialEq, Debug)] +pub enum Value { + Undefined, + Null, + Bool( bool ), + Number( Number ), + String( String ), + Array( Vec< Value > ), + Object( BTreeMap< String, Value > ), // TODO: Use our own type instead of using BTreeMap directly. + Reference( Reference ) +} + +impl Value { + /// Checks whenever the Value is of the Reference variant. + #[inline] + pub fn is_reference( &self ) -> bool { + if let Value::Reference( _ ) = *self { + true + } else { + false + } + } + + /// Gets a reference to the [Reference](struct.Reference.html) inside this `Value`. + #[inline] + pub fn as_reference( &self ) -> Option< &Reference > { + match *self { + Value::Reference( ref reference ) => Some( reference ), + _ => None + } + } + + /// Returns the [Reference](struct.Reference.html) inside this `Value`. + #[inline] + pub fn into_reference( self ) -> Option< Reference > { + match self { + Value::Reference( reference ) => Some( reference ), + _ => None + } + } + + /// Converts a [Reference](struct.Reference.html) inside this `Value` into + /// the given type `T`; doesn't check whenever the reference is really of type `T`. + /// + /// In cases where the value is not a `Reference` a `None` is returned. + #[inline] + pub unsafe fn into_reference_unchecked< T: FromReferenceUnchecked >( self ) -> Option< T > { + T::from_value_unchecked( self ) + } + + /// Returns the `String` inside this `Value`. + #[inline] + pub fn into_string( self ) -> Option< String > { + match self { + Value::String( string ) => Some( string ), + _ => None + } + } + + /// Returns a borrow of the string inside this `Value`. + #[inline] + pub fn as_str( &self ) -> Option< &str > { + match *self { + Value::String( ref string ) => Some( string.as_str() ), + _ => None + } + } +} + +impl AsRef< Value > for Value { + #[inline] + fn as_ref( &self ) -> &Self { + self + } +} + +impl From< Undefined > for Value { + #[inline] + fn from( _: Undefined ) -> Self { + Value::Undefined + } +} + +impl< 'a > From< &'a Undefined > for Value { + #[inline] + fn from( _: &'a Undefined ) -> Self { + Value::Undefined + } +} + +impl< 'a > From< &'a mut Undefined > for Value { + #[inline] + fn from( _: &'a mut Undefined ) -> Self { + Value::Undefined + } +} + +impl From< Null > for Value { + #[inline] + fn from( _: Null ) -> Self { + Value::Null + } +} + +impl< 'a > From< &'a Null > for Value { + #[inline] + fn from( _: &'a Null ) -> Self { + Value::Null + } +} + +impl< 'a > From< &'a mut Null > for Value { + #[inline] + fn from( _: &'a mut Null ) -> Self { + Value::Null + } +} + +impl From< bool > for Value { + #[inline] + fn from( value: bool ) -> Self { + Value::Bool( value ) + } +} + +impl< 'a > From< &'a bool > for Value { + #[inline] + fn from( value: &'a bool ) -> Self { + Value::Bool( *value ) + } +} + +impl< 'a > From< &'a mut bool > for Value { + #[inline] + fn from( value: &'a mut bool ) -> Self { + (value as &bool).into() + } +} + +impl< 'a > From< &'a str > for Value { + #[inline] + fn from( value: &'a str ) -> Self { + Value::String( value.to_string() ) + } +} + +impl< 'a > From< &'a mut str > for Value { + #[inline] + fn from( value: &'a mut str ) -> Self { + (value as &str).into() + } +} + +impl From< String > for Value { + #[inline] + fn from( value: String ) -> Self { + Value::String( value ) + } +} + +impl< 'a > From< &'a String > for Value { + #[inline] + fn from( value: &'a String ) -> Self { + Value::String( value.clone() ) + } +} + +impl< 'a > From< &'a mut String > for Value { + #[inline] + fn from( value: &'a mut String ) -> Self { + (value as &String).into() + } +} + +impl From< char > for Value { + #[inline] + fn from( value: char ) -> Self { + let mut buffer: [u8; 4] = [0; 4]; + let string = value.encode_utf8( &mut buffer ); + string.to_owned().into() + } +} + +impl< 'a > From< &'a char > for Value { + #[inline] + fn from( value: &'a char ) -> Self { + (*value).into() + } +} + +impl< 'a > From< &'a mut char > for Value { + #[inline] + fn from( value: &'a mut char ) -> Self { + (*value).into() + } +} + +impl< T: Into< Value > > From< Vec< T > > for Value { + #[inline] + fn from( value: Vec< T > ) -> Self { + Value::Array( value.into_iter().map( |element| element.into() ).collect() ) + } +} + +impl< 'a, T > From< &'a Vec< T > > for Value where &'a T: Into< Value > { + #[inline] + fn from( value: &'a Vec< T > ) -> Self { + value[..].into() + } +} + +impl< 'a, T > From< &'a mut Vec< T > > for Value where &'a T: Into< Value > { + #[inline] + fn from( value: &'a mut Vec< T > ) -> Self { + value[..].into() + } +} + +impl< 'a, T > From< &'a [T] > for Value where &'a T: Into< Value > { + #[inline] + fn from( value: &'a [T] ) -> Self { + Value::Array( value.iter().map( |element| { + element.into() + }).collect() ) + } +} + +impl< 'a, T > From< &'a mut [T] > for Value where &'a T: Into< Value > { + #[inline] + fn from( value: &'a mut [T] ) -> Self { + (value as &[T]).into() + } +} + +// TODO: It would be nice to specialize this for values which are already of type Value. +impl< K: Into< String >, V: Into< Value > > From< BTreeMap< K, V > > for Value { + #[inline] + fn from( value: BTreeMap< K, V > ) -> Self { + let value = value.into_iter().map( |(key, value)| (key.into(), value.into()) ).collect(); + Value::Object( value ) + } +} + +impl< 'a, K, V > From< &'a BTreeMap< K, V > > for Value where &'a K: Into< String >, &'a V: Into< Value > { + #[inline] + fn from( value: &'a BTreeMap< K, V > ) -> Self { + let value = value.iter().map( |(key, value)| (key.into(), value.into()) ).collect(); + Value::Object( value ) + } +} + +impl< 'a, K, V > From< &'a mut BTreeMap< K, V > > for Value where &'a K: Into< String >, &'a V: Into< Value > { + #[inline] + fn from( value: &'a mut BTreeMap< K, V > ) -> Self { + let value: &BTreeMap< K, V > = value; + value.into() + } +} + +impl< K: Into< String > + Hash + Eq, V: Into< Value > > From< HashMap< K, V > > for Value { + #[inline] + fn from( value: HashMap< K, V > ) -> Self { + let value = value.into_iter().map( |(key, value)| (key.into(), value.into()) ).collect(); + Value::Object( value ) + } +} + +impl< 'a, K: Hash + Eq, V > From< &'a HashMap< K, V > > for Value where &'a K: Into< String >, &'a V: Into< Value > { + #[inline] + fn from( value: &'a HashMap< K, V > ) -> Self { + let value = value.iter().map( |(key, value)| (key.into(), value.into()) ).collect(); + Value::Object( value ) + } +} + +impl< 'a, K: Hash + Eq, V > From< &'a mut HashMap< K, V > > for Value where &'a K: Into< String >, &'a V: Into< Value > { + #[inline] + fn from( value: &'a mut HashMap< K, V > ) -> Self { + let value: &HashMap< K, V > = value; + value.into() + } +} + +impl From< Reference > for Value { + #[inline] + fn from( value: Reference ) -> Self { + Value::Reference( value ) + } +} + +impl< 'a > From< &'a Reference > for Value { + #[inline] + fn from( value: &'a Reference ) -> Self { + Value::Reference( value.clone() ) + } +} + +impl< 'a > From< &'a mut Reference > for Value { + #[inline] + fn from( value: &'a mut Reference ) -> Self { + (value as &Reference).into() + } +} + +macro_rules! impl_from_number { + ($($kind:ty)+) => { + $( + impl From< $kind > for Value { + #[inline] + fn from( value: $kind ) -> Self { + Value::Number( value.into() ) + } + } + + impl< 'a > From< &'a $kind > for Value { + #[inline] + fn from( value: &'a $kind ) -> Self { + Value::Number( (*value).into() ) + } + } + + impl< 'a > From< &'a mut $kind > for Value { + #[inline] + fn from( value: &'a mut $kind ) -> Self { + (value as &$kind).into() + } + } + + impl_infallible_try_from!( $kind => Value; ); + )+ + }; +} + +impl_from_number!( i8 i16 i32 u8 u16 u32 f32 f64 ); +impl_infallible_try_from! { + Undefined => Value; + impl< 'a > for &'a Undefined => Value; + impl< 'a > for &'a mut Undefined => Value; + Null => Value; + impl< 'a > for &'a Null => Value; + impl< 'a > for &'a mut Null => Value; + bool => Value; + impl< 'a > for &'a bool => Value; + impl< 'a > for &'a mut bool => Value; + impl< 'a > for &'a str => Value; + impl< 'a > for &'a mut str => Value; + String => Value; + impl< 'a > for &'a String => Value; + impl< 'a > for &'a mut String => Value; + char => Value; + impl< 'a > for &'a char => Value; + impl< 'a > for &'a mut char => Value; + impl< T > for Vec< T > => Value where (T: Into< Value >); + impl< 'a, T > for &'a Vec< T > => Value where (&'a T: Into< Value >); + impl< 'a, T > for &'a mut Vec< T > => Value where (&'a T: Into< Value >); + impl< 'a, T > for &'a [T] => Value where (&'a T: Into< Value >); + impl< 'a, T > for &'a mut [T] => Value where (&'a T: Into< Value >); + impl< K, V > for BTreeMap< K, V > => Value where (K: Into< String >, V: Into< Value >); + impl< 'a, K, V > for &'a BTreeMap< K, V > => Value where (&'a K: Into< String >, &'a V: Into< Value >); + impl< 'a, K, V > for &'a mut BTreeMap< K, V > => Value where (&'a K: Into< String >, &'a V: Into< Value >); + impl< K, V > for HashMap< K, V > => Value where (K: Into< String > + Hash + Eq, V: Into< Value >); + impl< 'a, K, V > for &'a HashMap< K, V > => Value where (K: Hash + Eq, &'a K: Into< String >, &'a V: Into< Value >); + impl< 'a, K, V > for &'a mut HashMap< K, V > => Value where (K: Hash + Eq, &'a K: Into< String >, &'a V: Into< Value >); + Reference => Value; +} + +macro_rules! impl_try_from_number { + ($($kind:ty)+) => { + $( + impl TryFrom< $kind > for Value { + type Error = >::Error; + + #[inline] + fn try_from( value: $kind ) -> Result< Self, Self::Error > { + Ok( Value::Number( value.try_into()? ) ) + } + } + )+ + }; +} + +impl_try_from_number!( i64 u64 ); + +impl PartialEq< Undefined > for Value { + #[inline] + fn eq( &self, _: &Undefined ) -> bool { + match *self { + Value::Undefined => true, + _ => false + } + } +} + +impl PartialEq< Null > for Value { + #[inline] + fn eq( &self, _: &Null ) -> bool { + match *self { + Value::Null => true, + _ => false + } + } +} + +impl PartialEq< bool > for Value { + #[inline] + fn eq( &self, right: &bool ) -> bool { + match *self { + Value::Bool( left ) => left == *right, + _ => false + } + } +} + +impl PartialEq< str > for Value { + #[inline] + fn eq( &self, right: &str ) -> bool { + match *self { + Value::String( ref left ) => left == right, + _ => false + } + } +} + +impl PartialEq< String > for Value { + #[inline] + fn eq( &self, right: &String ) -> bool { + match *self { + Value::String( ref left ) => left == right, + _ => false + } + } +} + +impl< T > PartialEq< [T] > for Value where Value: PartialEq< T > { + #[inline] + fn eq( &self, right: &[T] ) -> bool { + match *self { + Value::Array( ref left ) => left.iter().zip( right.iter() ).all( |(left, right)| left == right ), + _ => false + } + } +} + +impl< 'a, T > PartialEq< &'a [T] > for Value where Value: PartialEq< T > { + #[inline] + fn eq( &self, right: &&'a [T] ) -> bool { + >::eq( self, right ) + } +} + +impl PartialEq< Number > for Value { + #[inline] + fn eq( &self, right: &Number ) -> bool { + match *self { + Value::Number( left ) => left == *right, + _ => false + } + } +} + +impl< T: AsRef< Reference > > PartialEq< T > for Value { + #[inline] + fn eq( &self, right: &T ) -> bool { + match *self { + Value::Reference( ref left ) => left == right.as_ref(), + _ => false + } + } +} + +impl< 'a > PartialEq< Reference > for &'a Value { + #[inline] + fn eq( &self, right: &Reference ) -> bool { + (*self).eq( right ) + } +} + +impl PartialEq< Value > for Reference { + #[inline] + fn eq( &self, right: &Value ) -> bool { + right.eq( self ) + } +} + +impl< 'a > PartialEq< &'a Value > for Reference { + #[inline] + fn eq( &self, right: &&'a Value ) -> bool { + let right: &'a Value = right; + right.eq( self ) + } +} + +impl< 'a > PartialEq< Value > for &'a Reference { + #[inline] + fn eq( &self, right: &Value ) -> bool { + (*self).eq( right ) + } +} + +macro_rules! impl_partial_eq_boilerplate { + ( $( $kind:ty ),+ ) => { + $( + impl< 'a > PartialEq< &'a $kind > for Value { + #[inline] + fn eq( &self, right: &&'a $kind ) -> bool { + let right: &'a $kind = right; + self.eq( right ) + } + } + + impl< 'a > PartialEq< $kind > for &'a Value { + #[inline] + fn eq( &self, right: &$kind ) -> bool { + (*self).eq( right ) + } + } + + impl PartialEq< Value > for $kind { + #[inline] + fn eq( &self, right: &Value ) -> bool { + right == self + } + } + + impl< 'a > PartialEq< &'a Value > for $kind { + #[inline] + fn eq( &self, right: &&'a Value ) -> bool { + let right: &'a Value = right; + right == self + } + } + + impl< 'a > PartialEq< Value > for &'a $kind { + #[inline] + fn eq( &self, right: &Value ) -> bool { + (*self).eq( right ) + } + } + )+ + } +} + +macro_rules! impl_partial_eq_to_number { + ($($kind:ty)+) => { + $( + impl PartialEq< $kind > for Value { + #[inline] + fn eq( &self, right: &$kind ) -> bool { + match *self { + Value::Number( left ) => left == *right, + _ => false + } + } + } + + impl_partial_eq_boilerplate!( $kind ); + )+ + }; +} + +impl_partial_eq_to_number!( i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 ); + +impl_partial_eq_boilerplate! { + Undefined, + Null, + bool, + str, + String, + Number +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum ConversionError { + TypeMismatch { + actual_type: &'static str + }, + NumericConversionError( number::ConversionError ), + ValueConversionError( Box< ConversionError > ) +} + +fn value_type_name( value: &Value ) -> &'static str { + match *value { + Value::Undefined => "Undefined", + Value::Null => "Null", + Value::Bool( _ ) => "Bool", + Value::Number( _ ) => "Number", + Value::String( _ ) => "String", + Value::Array( _ ) => "Array", + Value::Object( _ ) => "Object", + Value::Reference( _ ) => "Reference" + } +} + +impl fmt::Display for ConversionError { + fn fmt( &self, formatter: &mut fmt::Formatter ) -> Result< (), fmt::Error > { + match *self { + ConversionError::TypeMismatch { actual_type } => write!( formatter, "type mismatch; actual type is {}", actual_type ), + ConversionError::NumericConversionError( ref inner ) => write!( formatter, "{}", inner ), + ConversionError::ValueConversionError( ref inner ) => write!( formatter, "value conversion error: {}", inner ) + } + } +} + +impl error::Error for ConversionError { + fn description( &self ) -> &str { + match *self { + ConversionError::TypeMismatch { .. } => "type mismatch", + ConversionError::NumericConversionError( ref inner ) => inner.description(), + ConversionError::ValueConversionError( _ ) => "value conversion error" + } + } +} + +impl From< number::ConversionError > for ConversionError { + fn from( inner: number::ConversionError ) -> Self { + ConversionError::NumericConversionError( inner ) + } +} + +impl ConversionError { + #[inline] + fn type_mismatch( actual_value: &Value ) -> Self { + ConversionError::TypeMismatch { + actual_type: value_type_name( actual_value ) + } + } + + #[inline] + fn value_conversion_error( inner: ConversionError ) -> Self { + ConversionError::ValueConversionError( Box::new( inner ) ) + } +} + +impl TryFrom< Value > for Undefined { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Undefined => Ok( Undefined ), + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl TryFrom< Value > for Null { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Null => Ok( Null ), + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl TryFrom< Value > for bool { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Bool( value ) => Ok( value ), + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +macro_rules! impl_try_into_number { + ($($kind:ty)+) => { + $( + impl TryFrom< Value > for $kind { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Number( value ) => { + let result: Result< Self, _ > = value.try_into(); + result.map_err( |error| error.into() ) + }, + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } + } + )+ + }; +} + +impl_try_into_number!( u8 u16 u32 u64 i8 i16 i32 i64 f64 ); + +impl< V: TryFrom< Value, Error = ConversionError > > TryFrom< Value > for BTreeMap< String, V > { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Object( object ) => { + let mut output = BTreeMap::new(); + for (key, value) in object { + let value = match value.try_into() { + Ok( value ) => value, + Err( error ) => return Err( ConversionError::value_conversion_error( error ) ) + }; + output.insert( key, value ); + } + Ok( output ) + }, + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl< V: TryFrom< Value, Error = ConversionError > > TryFrom< Value > for HashMap< String, V > { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Object( object ) => { + let mut output = HashMap::with_capacity( object.len() ); + for (key, value) in object { + let value = match value.try_into() { + Ok( value ) => value, + Err( error ) => return Err( ConversionError::value_conversion_error( error ) ) + }; + output.insert( key, value ); + } + Ok( output ) + }, + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl< T: TryFrom< Value, Error = ConversionError > > TryFrom< Value > for Vec< T > { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Array( array ) => { + let mut output = Vec::with_capacity( array.len() ); + for value in array { + let value = match value.try_into() { + Ok( value ) => value, + Err( error ) => return Err( ConversionError::value_conversion_error( error ) ) + }; + output.push( value ); + } + Ok( output ) + }, + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl TryFrom< Value > for String { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::String( value ) => Ok( value ), + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl TryFrom< Value > for Reference { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Reference( value ) => Ok( value ), + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl< 'a > TryFrom< &'a Value > for &'a str { + type Error = ConversionError; + + #[inline] + fn try_from( value: &'a Value ) -> Result< Self, Self::Error > { + match *value { + Value::String( ref value ) => Ok( value ), + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl< 'a > TryFrom< &'a Value > for &'a Reference { + type Error = ConversionError; + + #[inline] + fn try_from( value: &'a Value ) -> Result< Self, Self::Error > { + match *value { + Value::Reference( ref value ) => Ok( value ), + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +macro_rules! __impl_nullable_try_from_value { + (($($impl_arg:tt)*) ($($dst_arg:tt)*) ($($bounds:tt)*)) => { + impl< $($impl_arg)* > TryFrom< Value > for Option< $($dst_arg)* > where $($bounds)* { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Undefined | Value::Null => Ok( None ), + value => value.try_into().map( Some ) + } + } + } + }; +} + +macro_rules! impl_nullable_try_from_value { + (impl< $($impl_arg:tt),* > $dst:ty where ($($bounds:tt)*); $($rest:tt)*) => { + __impl_nullable_try_from_value!( ($($impl_arg),*) ($dst) ($($bounds)*) ); + impl_nullable_try_from_value!( $($rest)* ); + }; + + (impl< $($impl_arg:tt),* > $dst:ty; $($rest:tt)*) => { + __impl_nullable_try_from_value!( ($($impl_arg),*) ($dst) () ); + impl_nullable_try_from_value!( $($rest)* ); + }; + + ($dst:ty; $($rest:tt)*) => { + __impl_nullable_try_from_value!( () ($dst) () ); + impl_nullable_try_from_value!( $($rest)* ); + + }; + + () => {}; +} + +impl_nullable_try_from_value! { + bool; + u8; + u16; + u32; + u64; + i8; + i16; + i32; + i64; + f64; + impl< V > BTreeMap< String, V > where (V: TryFrom< Value, Error = ConversionError >); + impl< V > HashMap< String, V > where (V: TryFrom< Value, Error = ConversionError >); + impl< T > Vec< T > where (T: TryFrom< Value, Error = ConversionError >); + String; + Reference; +} + +impl< 'a > TryFrom< &'a Value > for Option< &'a str > { + type Error = ConversionError; + + #[inline] + fn try_from( value: &'a Value ) -> Result< Self, Self::Error > { + match *value { + Value::String( ref value ) => Ok( Some( value ) ), + ref value => value.try_into().map( Some ) + } + } +} + +impl< 'a > TryFrom< &'a Value > for Option< &'a Reference > { + type Error = ConversionError; + + #[inline] + fn try_from( value: &'a Value ) -> Result< Self, Self::Error > { + match *value { + Value::Reference( ref value ) => Ok( Some( value ) ), + ref value => value.try_into().map( Some ) + } + } +} + +#[cfg(test)] +mod tests { + use super::{Value, Reference}; + use webcore::try_from::TryInto; + + #[test] + fn string_equality() { + let value = Value::String( "Hello!".to_owned() ); + assert!( value == "Hello!" ); + assert!( &value == "Hello!" ); + assert!( value == "Hello!".to_owned() ); + assert!( &value == "Hello!".to_owned() ); + assert!( value == &"Hello!".to_owned() ); + assert!( &value == &"Hello!".to_owned() ); + assert!( "Hello!" == value ); + assert!( "Hello!" == &value ); + assert!( "Hello!".to_owned() == value ); + assert!( "Hello!".to_owned() == &value ); + assert!( &"Hello!".to_owned() == value ); + assert!( &"Hello!".to_owned() == &value ); + + assert!( value != "Bob" ); + } + + #[test] + fn array_equality() { + let value = Value::Array( vec![ Value::Bool( true ), Value::Bool( false ) ] ); + assert!( value == &[true, false][..] ); + assert!( value != &[true, true][..] ); + // Looks like it's not possible to define a symmetric PartialEq for arrays. ); + } + + #[test] + fn reference_equality() { + let value = js! { return new Date() }; + let reference: Reference = value.clone().try_into().unwrap(); + + assert!( value == reference ); + assert!( &value == reference ); + assert!( value == &reference ); + assert!( &value == &reference ); + assert!( reference == value ); + assert!( &reference == value ); + assert!( reference == &value ); + assert!( &reference == &value ); + } + + pub struct Error( Reference ); + reference_boilerplate! { + Error, + instanceof Error + } + + pub struct ReferenceError( Reference ); + reference_boilerplate! { + ReferenceError, + instanceof ReferenceError + convertible to Error + } + + pub struct TypeError( Reference ); + reference_boilerplate! { + TypeError, + instanceof TypeError + convertible to Error + } + + #[test] + fn reference_downcast() { + let reference = js! { return new ReferenceError(); }.into_reference().unwrap(); + assert!( reference.clone().downcast::< Error >().is_some() ); + assert!( reference.clone().downcast::< ReferenceError >().is_some() ); + assert!( reference.clone().downcast::< TypeError >().is_none() ); + } + + #[test] + fn reference_try_into_downcast_from_reference() { + let reference = js! { return new ReferenceError(); }.into_reference().unwrap(); + let typed_reference: Result< Error, _ > = reference.clone().try_into(); + assert!( typed_reference.is_ok() ); + + let typed_reference: Result< ReferenceError, _ > = reference.clone().try_into(); + assert!( typed_reference.is_ok() ); + + let typed_reference: Result< TypeError, _ > = reference.clone().try_into(); + assert!( typed_reference.is_err() ); + } + + #[test] + fn reference_try_into_downcast_from_value() { + let value = js! { return new ReferenceError(); }; + let typed_reference: Result< Error, _ > = value.clone().try_into(); + assert!( typed_reference.is_ok() ); + + let typed_reference: Result< ReferenceError, _ > = value.clone().try_into(); + assert!( typed_reference.is_ok() ); + + let typed_reference: Result< TypeError, _ > = value.clone().try_into(); + assert!( typed_reference.is_err() ); + } + + #[test] + fn reference_into_upcast() { + let reference: ReferenceError = js! { return new ReferenceError(); }.into_reference().unwrap().downcast().unwrap(); + let _: Error = reference.clone().into(); + let _: Reference = reference.clone().into(); + } +} diff --git a/src/webcore/void.rs b/src/webcore/void.rs new file mode 100644 index 00000000..7c868621 --- /dev/null +++ b/src/webcore/void.rs @@ -0,0 +1,25 @@ +use std::fmt; +use std::error; + +/// An uninhabited type for use in statically impossible cases. +/// +/// Will be replaced by Rust's `!` type once that stabilizes. +pub enum Void {} + +impl fmt::Debug for Void { + fn fmt( &self, _: &mut fmt::Formatter ) -> Result< (), fmt::Error > { + unreachable!(); + } +} + +impl fmt::Display for Void { + fn fmt( &self, _: &mut fmt::Formatter ) -> Result< (), fmt::Error > { + unreachable!(); + } +} + +impl error::Error for Void { + fn description( &self ) -> &str { + unreachable!(); + } +}