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/types_impl/map.rs b/crates/neon/src/types_impl/map.rs new file mode 100644 index 000000000..554463672 --- /dev/null +++ b/crates/neon/src/types_impl/map.rs @@ -0,0 +1,170 @@ +use crate::{ + context::{internal::Env, Context, Cx}, handle::{internal::TransparentNoCopyWrapper, Handle, Root}, object::Object, result::{JsResult, NeonResult}, sys::raw, thread::LocalKey, types::{private, JsFunction, JsObject, Value} +}; + +use super::extract::{TryFromJs, TryIntoJs}; + +#[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>(cx: &mut Cx<'cx>) -> NeonResult> { + let map = cx + .global::("Map")? + .construct_with(cx) + .apply::(cx)?; + + Ok(map.downcast_or_throw(cx)?) + } + + pub fn size(&self, cx: &mut Cx) -> NeonResult { + self.prop(cx, "size").get() + } + + pub fn clear(&self, cx: &mut Cx) -> NeonResult<()> { + self.method(cx, "clear")?.call() + } + + pub fn delete<'cx, K>( + &self, + cx: &mut Cx<'cx>, + key: K, + ) -> NeonResult + where K: TryIntoJs<'cx> { + self.method(cx, "delete")?.arg(key)?.call() + } + + pub fn entries<'cx, R>(&self, cx: &mut Cx<'cx>) -> NeonResult + where R: TryFromJs<'cx> + { + self.method(cx, "entries")?.call() + } + + pub fn for_each<'cx, F, R>( + &self, + cx: &mut Cx<'cx>, + cb: F, + ) -> NeonResult + where F: TryIntoJs<'cx>, R: TryFromJs<'cx> + { + self.method(cx, "forEach")?.arg(cb)?.call() + } + + pub fn get<'cx, K, R>( + &self, + cx: &mut Cx<'cx>, + key: K, + ) -> NeonResult + where + K: TryIntoJs<'cx>, + R: TryFromJs<'cx> + { + self.method(cx, "get")?.arg(key)?.call() + } + + pub fn has<'cx, K>( + &self, + cx: &mut Cx<'cx>, + key: K, + ) -> NeonResult + where + K: TryIntoJs<'cx>, + { + self.method(cx, "has")?.arg(key)?.call() + } + + pub fn keys<'cx, R>(&self, cx: &mut Cx<'cx>) -> NeonResult + where + R: TryFromJs<'cx> + { + self.method(cx, "keys")?.call() + } + + pub fn set<'cx, K, V>( + &self, + cx: &mut Cx<'cx>, + key: K, + value: V, + ) -> NeonResult> + where + K: TryIntoJs<'cx>, + V: TryIntoJs<'cx> + { + self.method(cx, "set")?.arg(key)?.arg(value)?.call() + } + + pub fn values<'cx, R>(&self, cx: &mut Cx<'cx>) -> NeonResult + where + R: TryFromJs<'cx> + { + self.method(cx, "values")?.call() + } + + pub fn group_by<'cx, A, B, R>( + cx: &mut Cx<'cx>, + elements: A, + cb: B, + ) -> NeonResult + where + A: TryIntoJs<'cx>, + B: TryIntoJs<'cx>, + R: TryFromJs<'cx> + { + // 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")? + .method(cx, "groupBy")? + .arg(elements)? + .arg(cb)? + .call() + } +} + +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 { + Cx::with_context(env, |mut cx| { + let ctor = map_constructor(&mut cx).unwrap(); + other.instance_of(&mut cx, &*ctor) + }) + } + + 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 {} + +fn global_map_constructor<'cx>(cx: &mut Cx<'cx>) -> JsResult<'cx, JsFunction> { + cx.global::("Map") +} + +fn map_constructor<'cx>(cx: &mut Cx<'cx>) -> JsResult<'cx, JsFunction> { + static MAP_CONSTRUCTOR: LocalKey> = LocalKey::new(); + + MAP_CONSTRUCTOR + .get_or_try_init(cx, |cx| global_map_constructor(cx).map(|f| f.root(cx))) + .map(|f| f.to_inner(cx)) +} diff --git a/crates/neon/src/types_impl/mod.rs b/crates/neon/src/types_impl/mod.rs index e37cf39ab..16680cfae 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; @@ -24,7 +25,7 @@ use private::prepare_call; use smallvec::smallvec; use crate::{ - context::{internal::Env, Context, Cx, FunctionContext}, + context::{internal::{ContextInternal, Env}, Context, Cx, FunctionContext}, handle::{ internal::{SuperType, TransparentNoCopyWrapper}, Handle, @@ -47,6 +48,7 @@ pub use self::{ JsUint8Array, }, error::JsError, + map::JsMap, promise::{Deferred, JsPromise}, }; @@ -98,6 +100,15 @@ pub trait Value: ValueInternal { JsValue::new_internal(self.to_local()) } + fn instance_of(&self, cx: &mut Cx, ctor: &V) -> bool { + let mut result = false; + assert_eq!( + unsafe { sys::bindings::instanceof(cx.env().to_raw(), self.to_local(), ctor.to_local(), &mut result) }, + sys::bindings::Status::Ok + ); + result + } + #[cfg(feature = "sys")] #[cfg_attr(docsrs, doc(cfg(feature = "sys")))] /// Get a raw reference to the wrapped Node-API value. 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/lib/types.js b/test/napi/lib/types.js index a4f32d5c9..4beff33d6 100644 --- a/test/napi/lib/types.js +++ b/test/napi/lib/types.js @@ -90,4 +90,15 @@ describe("type checks", function () { assert(!addon.strict_equals(o1, o2)); assert(!addon.strict_equals(o1, 17)); }); + + it("instance_of", function () { + assert(addon.instance_of(new Error(), Error)); + assert(!addon.instance_of(new Error(), Map)); + assert(addon.instance_of(new Map(), Map)); + + function Car() {} + function Bike() {} + assert(addon.instance_of(new Car(), Car)); + assert(!addon.instance_of(new Car(), Bike)); + }); }); diff --git a/test/napi/src/js/map.rs b/test/napi/src/js/map.rs new file mode 100644 index 000000000..97f0ca655 --- /dev/null +++ b/test/napi/src/js/map.rs @@ -0,0 +1,97 @@ +use neon::prelude::*; + +#[neon::export] +pub fn return_js_map<'cx>(cx: &mut FunctionContext<'cx>) -> JsResult<'cx, JsMap> { + JsMap::new(cx) +} + +#[neon::export] +pub fn return_js_map_with_number_as_keys_and_values<'cx>( + cx: &mut FunctionContext<'cx>, +) -> JsResult<'cx, JsMap> { + let map = JsMap::new(cx)?; + { + let key = cx.number(1); + let val = cx.number(1000); + map.set(cx, key, val)?; + } + { + let key = cx.number(-1); + let val = cx.number(-1000); + map.set(cx, key, val)?; + } + Ok(map) +} + +#[neon::export] +pub fn return_js_map_with_heterogeneous_keys_and_values<'cx>( + cx: &mut FunctionContext<'cx>, +) -> JsResult<'cx, JsMap> { + let map = JsMap::new(cx)?; + { + let key = cx.string("a"); + let val = cx.number(1); + map.set(cx, key, val)?; + } + { + let key = cx.number(26); + let val = cx.string("z"); + map.set(cx, key, val)?; + } + Ok(map) +} + +#[neon::export] +pub fn read_js_map<'cx>(cx: &mut FunctionContext<'cx>) -> JsResult<'cx, JsValue> { + let map = cx.argument::(0)?; + let key = cx.argument::(1)?; + map.get(cx, key) +} + +#[neon::export] +pub fn get_js_map_size<'cx>(cx: &mut FunctionContext<'cx>) -> JsResult<'cx, JsNumber> { + let map = cx.argument::(0)?; + map.size(cx).map(|x| cx.number(x)) +} + +#[neon::export] +pub fn modify_js_map<'cx>(cx: &mut FunctionContext<'cx>) -> JsResult<'cx, JsMap> { + let map = cx.argument::(0)?; + let key = cx.argument::(1)?; + let value = cx.argument::(2)?; + map.set(cx, key, value) +} + +#[neon::export] +pub fn clear_js_map<'cx>(cx: &mut FunctionContext<'cx>) -> JsResult<'cx, JsUndefined> { + let map = cx.argument::(0)?; + map.clear(cx).map(|_| cx.undefined()) +} + +#[neon::export] +pub fn delete_js_map<'cx>(cx: &mut FunctionContext<'cx>) -> JsResult<'cx, JsBoolean> { + let map = cx.argument::(0)?; + let key = cx.argument::(1)?; + map.delete(cx, key).map(|v| cx.boolean(v)) +} + +#[neon::export] +pub fn has_js_map<'cx>(cx: &mut FunctionContext<'cx>) -> JsResult<'cx, JsBoolean> { + let map = cx.argument::(0)?; + let key = cx.argument::(1)?; + map.has(cx, key).map(|v| cx.boolean(v)) +} + +#[neon::export] +pub fn for_each_js_map<'cx>(cx: &mut FunctionContext<'cx>) -> JsResult<'cx, JsUndefined> { + let map = cx.argument::(0)?; + let cb: Handle<'_, JsValue> = cx.argument::(1)?; + map.for_each(cx, cb) +} + +#[neon::export] +pub fn group_by_js_map<'cx>(cx: &mut FunctionContext<'cx>) -> JsResult<'cx, JsMap> { + let elements = cx.argument::(0)?; + let cb = cx.argument::(1)?; + JsMap::group_by(cx, elements, cb) +} diff --git a/test/napi/src/js/types.rs b/test/napi/src/js/types.rs index 6115a1f72..f2860a7cc 100644 --- a/test/napi/src/js/types.rs +++ b/test/napi/src/js/types.rs @@ -72,3 +72,11 @@ pub fn strict_equals(mut cx: FunctionContext) -> JsResult { let eq = v1.strict_equals(&mut cx, v2); Ok(cx.boolean(eq)) } + +#[neon::export] +pub fn instance_of<'cx>(cx: &mut FunctionContext<'cx>) -> JsResult<'cx, JsBoolean> { + let val: Handle = cx.argument(0)?; + let ctor: Handle = cx.argument(1)?; + let result = val.instance_of(cx, &*ctor); + Ok(cx.boolean(result)) +} diff --git a/test/napi/src/lib.rs b/test/napi/src/lib.rs index 3568e7842..1ce6b0937 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -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;