Skip to content

Commit

Permalink
feat(neon): Add neon::types::JsMap
Browse files Browse the repository at this point in the history
  • Loading branch information
touilleMan committed Oct 11, 2024
1 parent 1efcd4e commit 0d8969e
Show file tree
Hide file tree
Showing 8 changed files with 461 additions and 3 deletions.
2 changes: 1 addition & 1 deletion crates/neon/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
3 changes: 3 additions & 0 deletions crates/neon/src/sys/bindings/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
36 changes: 36 additions & 0 deletions crates/neon/src/sys/tag.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::sys;

use super::{
bindings as napi,
raw::{Env, Local},
Expand Down Expand Up @@ -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
}
163 changes: 163 additions & 0 deletions crates/neon/src/types_impl/map.rs
Original file line number Diff line number Diff line change
@@ -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<Handle<'cx, Self>>
where
C: Context<'cx>,
{
let map = cx
.global::<JsFunction>("Map")?
.construct_with(cx)
.apply::<JsObject, _>(cx)?;

Ok(map.downcast_or_throw(cx)?)
}

pub fn size<'a, C: Context<'a>>(&self, cx: &mut C) -> NeonResult<Handle<'a, JsNumber>> {
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<Handle<'a, JsUndefined>> {
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<bool> {
Object::call_method_with(self, cx, "delete")?
.arg(key)
.apply::<JsBoolean, _>(cx)
.map(|v| v.value(cx))
}

pub fn entries<'a, C: Context<'a>>(&self, cx: &mut C) -> NeonResult<Handle<'a, JsObject>> {
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<Handle<'a, JsUndefined>> {
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<Handle<'a, R>> {
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<bool> {
Object::call_method_with(self, cx, "has")?
.arg(key)
.apply::<JsBoolean, _>(cx)
.map(|v| v.value(cx))
}

pub fn keys<'a, C: Context<'a>, R: Value>(&self, cx: &mut C) -> NeonResult<Handle<'a, R>> {
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<Handle<'a, JsMap>> {
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<Handle<'a, R>> {
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<Handle<'a, R>> {
// 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::<JsObject>("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<Other: Value>(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 {}
2 changes: 2 additions & 0 deletions crates/neon/src/types_impl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,6 +48,7 @@ pub use self::{
JsUint8Array,
},
error::JsError,
map::JsMap,
promise::{Deferred, JsPromise},
};

Expand Down
151 changes: 151 additions & 0 deletions test/napi/lib/map.js
Original file line number Diff line number Diff line change
@@ -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 },
],
],
])
);
});
});
Loading

0 comments on commit 0d8969e

Please sign in to comment.