diff --git a/crates/neon/src/prelude.rs b/crates/neon/src/prelude.rs index 055ae1f9d..801f8c90a 100644 --- a/crates/neon/src/prelude.rs +++ b/crates/neon/src/prelude.rs @@ -9,7 +9,7 @@ pub use crate::{ types::{ boxed::{Finalize, JsBox}, JsArray, JsArrayBuffer, JsBigInt64Array, JsBigUint64Array, JsBoolean, JsBuffer, JsError, - JsFloat32Array, JsFloat64Array, JsFunction, JsInt16Array, JsInt32Array, JsInt8Array, + JsFloat32Array, JsFloat64Array, JsFunction, JsInt16Array, JsInt32Array, JsInt8Array, JsMap, JsNull, JsNumber, JsObject, JsPromise, JsString, JsTypedArray, JsUint16Array, JsUint32Array, JsUint8Array, JsUndefined, JsValue, Value, }, diff --git a/crates/neon/src/sys/bindings/functions.rs b/crates/neon/src/sys/bindings/functions.rs index 55bc5038b..b8f7ff0ef 100644 --- a/crates/neon/src/sys/bindings/functions.rs +++ b/crates/neon/src/sys/bindings/functions.rs @@ -51,6 +51,9 @@ mod napi1 { fn close_handle_scope(env: Env, scope: HandleScope) -> Status; + fn instanceof(env: Env, object: Value, constructor: Value, result: *mut bool) + -> Status; + fn is_arraybuffer(env: Env, value: Value, result: *mut bool) -> Status; fn is_typedarray(env: Env, value: Value, result: *mut bool) -> Status; fn is_buffer(env: Env, value: Value, result: *mut bool) -> Status; diff --git a/crates/neon/src/sys/tag.rs b/crates/neon/src/sys/tag.rs index 91b2266a6..6d5e1853c 100644 --- a/crates/neon/src/sys/tag.rs +++ b/crates/neon/src/sys/tag.rs @@ -1,3 +1,5 @@ +use crate::sys; + use super::{ bindings as napi, raw::{Env, Local}, @@ -137,3 +139,37 @@ pub unsafe fn check_object_type_tag(env: Env, object: Local, tag: &super::TypeTa pub unsafe fn is_bigint(env: Env, val: Local) -> bool { is_type(env, val, napi::ValueType::BigInt) } + +pub unsafe fn is_map(env: Env, val: Local) -> bool { + let global_object = unsafe { + let mut out: super::raw::Local = std::mem::zeroed(); + super::scope::get_global(env, &mut out); + out + }; + + let map_literal = { + let mut out: super::raw::Local = std::mem::zeroed(); + let literal = b"Map"; + assert_eq!( + super::string::new(&mut out, env, literal.as_ptr(), literal.len() as _), + true + ); + out + }; + + let map_constructor = { + let mut out: super::raw::Local = std::mem::zeroed(); + assert_eq!( + sys::object::get(&mut out, env, global_object, map_literal), + true + ); + out + }; + + let mut result = false; + assert_eq!( + napi::instanceof(env, val, map_constructor, &mut result), + napi::Status::Ok + ); + result +} diff --git a/crates/neon/src/types_impl/map.rs b/crates/neon/src/types_impl/map.rs new file mode 100644 index 000000000..0c0c990e4 --- /dev/null +++ b/crates/neon/src/types_impl/map.rs @@ -0,0 +1,163 @@ +use std::{error, fmt, mem::MaybeUninit}; + +use crate::{ + context::{internal::Env, Context}, + handle::{internal::TransparentNoCopyWrapper, root::NapiRef, Handle}, + macro_internal::NeonResultTag, + object::Object, + result::{JsResult, NeonResult, ResultExt}, + sys::{self, raw}, + types::{private, JsFunction, JsObject, Value}, +}; + +use super::{JsBoolean, JsNumber, JsUndefined}; + +#[derive(Debug)] +#[repr(transparent)] +/// The type of JavaScript +/// [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) +/// objects. +pub struct JsMap(raw::Local); + +impl JsMap { + pub fn new<'cx, C>(cx: &mut C) -> NeonResult> + where + C: Context<'cx>, + { + let map = cx + .global::("Map")? + .construct_with(cx) + .apply::(cx)?; + + Ok(map.downcast_or_throw(cx)?) + } + + pub fn size<'a, C: Context<'a>>(&self, cx: &mut C) -> NeonResult> { + Object::get(self, cx, "size") + } + + // TODO: is the return type important here ? + // I see three possibilities here: + // 1. Stick to the JS and return the `undefined` (this is what we do now) + // 2. Check we get an `undefined` and return `Ok(())` + // 3. Just discard the return value and return `Ok(())` + // Solutions 2 & 3 are more user-friendly, but make more assumptions (though it + // should be fine given `Map` is not expected to be overridden ?). + pub fn clear<'a, C: Context<'a>>(&self, cx: &mut C) -> NeonResult> { + Object::call_method_with(self, cx, "clear")?.apply(cx) + } + + pub fn delete<'a, C: Context<'a>, K: Value>( + &self, + cx: &mut C, + key: Handle<'a, K>, + ) -> NeonResult { + Object::call_method_with(self, cx, "delete")? + .arg(key) + .apply::(cx) + .map(|v| v.value(cx)) + } + + pub fn entries<'a, C: Context<'a>>(&self, cx: &mut C) -> NeonResult> { + Object::call_method_with(self, cx, "entries")?.apply(cx) + } + + pub fn for_each<'a, C: Context<'a>, F: Value>( + &self, + cx: &mut C, + cb: Handle<'a, F>, + ) -> NeonResult> { + Object::call_method_with(self, cx, "forEach")? + .arg(cb) + .apply(cx) + } + + pub fn get<'a, C: Context<'a>, K: Value, R: Value>( + &self, + cx: &mut C, + key: Handle<'a, K>, + ) -> NeonResult> { + Object::call_method_with(self, cx, "get")? + .arg(key) + .apply(cx) + } + + pub fn has<'a, C: Context<'a>, K: Value>( + &self, + cx: &mut C, + key: Handle<'a, K>, + ) -> NeonResult { + Object::call_method_with(self, cx, "has")? + .arg(key) + .apply::(cx) + .map(|v| v.value(cx)) + } + + pub fn keys<'a, C: Context<'a>, R: Value>(&self, cx: &mut C) -> NeonResult> { + Object::call_method_with(self, cx, "keys")?.apply(cx) + } + + pub fn set<'a, C: Context<'a>, K: Value, V: Value>( + &self, + cx: &mut C, + key: Handle<'a, K>, + value: Handle<'a, V>, + ) -> NeonResult> { + Object::call_method_with(self, cx, "set")? + .arg(key) + .arg(value) + .apply(cx) + } + + pub fn values<'a, C: Context<'a>, R: Value>(&self, cx: &mut C) -> NeonResult> { + Object::call_method_with(self, cx, "values")?.apply(cx) + } + + pub fn group_by<'a, C: Context<'a>, A: Value, B: Value, R: Value>( + cx: &mut C, + elements: Handle<'a, A>, + cb: Handle<'a, B>, + ) -> NeonResult> { + // TODO: This is broken and leads to a `failed to downcast any to object` error + // when trying to downcast `Map.groupBy` into a `JsFunction`... + cx.global::("Map")? + .call_method_with(cx, "groupBy")? + .arg(elements) + .arg(cb) + .apply(cx) + } + + // TODO: should we implementd those as well ? + // Map[Symbol.species] + // Map.prototype[Symbol.iterator]() +} + +unsafe impl TransparentNoCopyWrapper for JsMap { + type Inner = raw::Local; + + fn into_inner(self) -> Self::Inner { + self.0 + } +} + +impl private::ValueInternal for JsMap { + fn name() -> &'static str { + "Map" + } + + fn is_typeof(env: Env, other: &Other) -> bool { + unsafe { sys::tag::is_map(env.to_raw(), other.to_local()) } + } + + fn to_local(&self) -> raw::Local { + self.0 + } + + unsafe fn from_local(_env: Env, h: raw::Local) -> Self { + Self(h) + } +} + +impl Value for JsMap {} + +impl Object for JsMap {} diff --git a/crates/neon/src/types_impl/mod.rs b/crates/neon/src/types_impl/mod.rs index e37cf39ab..3d60bd163 100644 --- a/crates/neon/src/types_impl/mod.rs +++ b/crates/neon/src/types_impl/mod.rs @@ -10,6 +10,7 @@ pub(crate) mod date; pub(crate) mod error; pub mod extract; pub mod function; +pub(crate) mod map; pub(crate) mod promise; pub(crate) mod private; @@ -47,6 +48,7 @@ pub use self::{ JsUint8Array, }, error::JsError, + map::JsMap, promise::{Deferred, JsPromise}, }; diff --git a/test/napi/lib/map.js b/test/napi/lib/map.js new file mode 100644 index 000000000..a8547c3da --- /dev/null +++ b/test/napi/lib/map.js @@ -0,0 +1,151 @@ +var addon = require(".."); +var assert = require("chai").assert; + +describe("JsMap", function () { + it("return a JsMap built in Rust", function () { + assert.deepEqual(new Map(), addon.return_js_map()); + }); + + it("return a JsMap with a number as keys and values", function () { + assert.deepEqual( + new Map([ + [1, 1000], + [-1, -1000], + ]), + addon.return_js_map_with_number_as_keys_and_values() + ); + }); + + it("return a JsMap with heterogeneous keys/values", function () { + assert.deepEqual( + new Map([ + ["a", 1], + [26, "z"], + ]), + addon.return_js_map_with_heterogeneous_keys_and_values() + ); + }); + + it("can read from a JsMap", function () { + const map = new Map([ + [1, "a"], + [2, "b"], + ]); + assert.strictEqual(addon.read_js_map(map, 2), "b"); + }); + + it("can get size from a JsMap", function () { + const map = new Map([ + [1, "a"], + [2, "b"], + ]); + assert.strictEqual(addon.get_js_map_size(map), 2); + assert.strictEqual(addon.get_js_map_size(new Map()), 0); + }); + + it("can modify a JsMap", function () { + const map = new Map([[1, "a"]]); + addon.modify_js_map(map, 2, "b"); + assert.deepEqual( + map, + new Map([ + [1, "a"], + [2, "b"], + ]) + ); + }); + + it("returns undefined when accessing outside JsMap bounds", function () { + assert.strictEqual(addon.read_js_map(new Map(), "x"), undefined); + }); + + it("can clear a JsMap", function () { + const map = new Map([[1, "a"]]); + addon.clear_js_map(map); + assert.deepEqual(map, new Map()); + }); + + it("can delete key from JsMap", function () { + const map = new Map([ + [1, "a"], + ["z", 26], + ]); + + assert.strictEqual(addon.delete_js_map(map, "unknown"), false); + assert.deepEqual( + map, + new Map([ + [1, "a"], + ["z", 26], + ]) + ); + + assert.strictEqual(addon.delete_js_map(map, 1), true); + assert.deepEqual(map, new Map([["z", 26]])); + + assert.strictEqual(addon.delete_js_map(map, "z"), true); + assert.deepEqual(map, new Map()); + }); + + it("can use `has` on JsMap", function () { + const map = new Map([ + [1, "a"], + ["z", 26], + ]); + + assert.strictEqual(addon.has_js_map(map, 1), true); + assert.strictEqual(addon.has_js_map(map, "z"), true); + assert.strictEqual(addon.has_js_map(map, "unknown"), false); + }); + + it("can use `forEach` on JsMap", function () { + const map = new Map([ + [1, "a"], + ["z", 26], + ]); + const collected = []; + + assert.strictEqual( + addon.for_each_js_map(map, (value, key, map) => { + collected.push([key, value, map]); + }), + undefined + ); + + assert.deepEqual(collected, [ + [1, "a", map], + ["z", 26, map], + ]); + }); + + it("can use `groupBy` on JsMap", function () { + const inventory = [ + { name: "asparagus", type: "vegetables", quantity: 9 }, + { name: "bananas", type: "fruit", quantity: 5 }, + { name: "goat", type: "meat", quantity: 23 }, + { name: "cherries", type: "fruit", quantity: 12 }, + { name: "fish", type: "meat", quantity: 22 }, + ]; + + const restock = { restock: true }; + const sufficient = { restock: false }; + const result = addon.group_by_js_map(inventory, ({ quantity }) => + quantity < 6 ? restock : sufficient + ); + assert.deepEqual( + result, + new Map([ + [restock, [{ name: "bananas", type: "fruit", quantity: 5 }]], + [ + sufficient, + [ + { name: "asparagus", type: "vegetables", quantity: 9 }, + { name: "goat", type: "meat", quantity: 23 }, + { name: "cherries", type: "fruit", quantity: 12 }, + { name: "fish", type: "meat", quantity: 22 }, + ], + ], + ]) + ); + }); +}); diff --git a/test/napi/src/js/map.rs b/test/napi/src/js/map.rs new file mode 100644 index 000000000..7f5881bfe --- /dev/null +++ b/test/napi/src/js/map.rs @@ -0,0 +1,84 @@ +use neon::prelude::*; + +pub fn return_js_map(mut cx: FunctionContext) -> JsResult { + JsMap::new(&mut cx) +} + +pub fn return_js_map_with_number_as_keys_and_values(mut cx: FunctionContext) -> JsResult { + let map = JsMap::new(&mut cx)?; + { + let key = cx.number(1); + let val = cx.number(1000); + map.set(&mut cx, key, val)?; + } + { + let key = cx.number(-1); + let val = cx.number(-1000); + map.set(&mut cx, key, val)?; + } + Ok(map) +} + +pub fn return_js_map_with_heterogeneous_keys_and_values( + mut cx: FunctionContext, +) -> JsResult { + let map = JsMap::new(&mut cx)?; + { + let key = cx.string("a"); + let val = cx.number(1); + map.set(&mut cx, key, val)?; + } + { + let key = cx.number(26); + let val = cx.string("z"); + map.set(&mut cx, key, val)?; + } + Ok(map) +} + +pub fn read_js_map(mut cx: FunctionContext) -> JsResult { + let map = cx.argument::(0)?; + let key = cx.argument::(1)?; + map.get(&mut cx, key) +} + +pub fn get_js_map_size(mut cx: FunctionContext) -> JsResult { + let map = cx.argument::(0)?; + map.size(&mut cx) +} + +pub fn modify_js_map(mut cx: FunctionContext) -> JsResult { + let map = cx.argument::(0)?; + let key = cx.argument::(1)?; + let value = cx.argument::(2)?; + map.set(&mut cx, key, value) +} + +pub fn clear_js_map(mut cx: FunctionContext) -> JsResult { + let map = cx.argument::(0)?; + map.clear(&mut cx) +} + +pub fn delete_js_map(mut cx: FunctionContext) -> JsResult { + let map = cx.argument::(0)?; + let key = cx.argument::(1)?; + map.delete(&mut cx, key).map(|v| cx.boolean(v)) +} + +pub fn has_js_map(mut cx: FunctionContext) -> JsResult { + let map = cx.argument::(0)?; + let key = cx.argument::(1)?; + map.has(&mut cx, key).map(|v| cx.boolean(v)) +} + +pub fn for_each_js_map(mut cx: FunctionContext) -> JsResult { + let map = cx.argument::(0)?; + let cb: Handle<'_, JsValue> = cx.argument::(1)?; + map.for_each(&mut cx, cb) +} + +pub fn group_by_js_map(mut cx: FunctionContext) -> JsResult { + let elements = cx.argument::(0)?; + let cb = cx.argument::(1)?; + JsMap::group_by(&mut cx, elements, cb) +} diff --git a/test/napi/src/lib.rs b/test/napi/src/lib.rs index 3568e7842..d546586e2 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -3,8 +3,8 @@ use once_cell::sync::OnceCell; use tokio::runtime::Runtime; use crate::js::{ - arrays::*, boxed::*, coercions::*, date::*, errors::*, functions::*, numbers::*, objects::*, - strings::*, threads::*, typedarrays::*, types::*, + arrays::*, boxed::*, coercions::*, date::*, errors::*, functions::*, map::*, numbers::*, + objects::*, strings::*, threads::*, typedarrays::*, types::*, }; mod js { @@ -18,6 +18,7 @@ mod js { pub mod extract; pub mod functions; pub mod futures; + pub mod map; pub mod numbers; pub mod objects; pub mod strings; @@ -233,6 +234,24 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("return_js_array_with_string", return_js_array_with_string)?; cx.export_function("read_js_array", read_js_array)?; + cx.export_function("return_js_map", return_js_map)?; + cx.export_function( + "return_js_map_with_number_as_keys_and_values", + return_js_map_with_number_as_keys_and_values, + )?; + cx.export_function( + "return_js_map_with_heterogeneous_keys_and_values", + return_js_map_with_heterogeneous_keys_and_values, + )?; + cx.export_function("read_js_map", read_js_map)?; + cx.export_function("get_js_map_size", get_js_map_size)?; + cx.export_function("modify_js_map", modify_js_map)?; + cx.export_function("clear_js_map", clear_js_map)?; + cx.export_function("delete_js_map", delete_js_map)?; + cx.export_function("has_js_map", has_js_map)?; + cx.export_function("for_each_js_map", for_each_js_map)?; + cx.export_function("group_by_js_map", group_by_js_map)?; + cx.export_function("to_string", to_string)?; cx.export_function("return_js_global_object", return_js_global_object)?;