From d10fde33a00974de068c7af640709133a55c412b Mon Sep 17 00:00:00 2001 From: "K.J. Valencik" Date: Thu, 3 Oct 2024 12:31:36 -0400 Subject: [PATCH] feat(neon): Implement TryIntoJs/TryFromJs for either::Either --- Cargo.lock | 6 +- crates/neon/Cargo.toml | 1 + crates/neon/src/types_impl/extract/either.rs | 115 +++++++++++++++++++ crates/neon/src/types_impl/extract/mod.rs | 1 + test/napi/Cargo.toml | 1 + test/napi/lib/extract.js | 16 +++ test/napi/src/js/extract.rs | 9 ++ 7 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 crates/neon/src/types_impl/extract/either.rs diff --git a/Cargo.lock b/Cargo.lock index 8df7b86df..4bfa3fc83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,9 +157,9 @@ checksum = "10936778145f3bea71fd9bf61332cce28c28e96a380714f7ab34838b80733fd6" [[package]] name = "either" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "electron-tests" @@ -385,6 +385,7 @@ dependencies = [ name = "napi-tests" version = "0.1.0" dependencies = [ + "either", "neon", "num-bigint-dig", "once_cell", @@ -399,6 +400,7 @@ dependencies = [ "aquamarine", "doc-comment", "easy-cast", + "either", "getrandom", "itertools", "libloading 0.8.1", diff --git a/crates/neon/Cargo.toml b/crates/neon/Cargo.toml index 4fa442453..7d4ad11dd 100644 --- a/crates/neon/Cargo.toml +++ b/crates/neon/Cargo.toml @@ -24,6 +24,7 @@ easy-cast = "0.5.2" # used for a doc example nodejs-sys = "0.15.0" [dependencies] +either = "1.13.0" getrandom = { version = "0.2.11", optional = true } libloading = "0.8.1" linkme = "0.3.25" diff --git a/crates/neon/src/types_impl/extract/either.rs b/crates/neon/src/types_impl/extract/either.rs new file mode 100644 index 000000000..7eda6c9dd --- /dev/null +++ b/crates/neon/src/types_impl/extract/either.rs @@ -0,0 +1,115 @@ +use std::{any, error, fmt}; + +use either::Either; + +use crate::{ + context::Cx, + handle::Handle, + object::Object, + result::{JsResult, NeonResult}, + types::{ + extract::{private, TryFromJs, TryIntoJs}, + JsError, JsValue, + }, +}; + +impl<'cx, L, R> TryFromJs<'cx> for Either +where + L: TryFromJs<'cx>, + R: TryFromJs<'cx>, +{ + type Error = Error; + + fn try_from_js( + cx: &mut Cx<'cx>, + v: Handle<'cx, JsValue>, + ) -> NeonResult> { + let left = match L::try_from_js(cx, v)? { + Ok(l) => return Ok(Ok(Either::Left(l))), + Err(l) => l, + }; + + let right = match R::try_from_js(cx, v)? { + Ok(r) => return Ok(Ok(Either::Right(r))), + Err(r) => r, + }; + + Ok(Err(Error::new::(left, right))) + } +} + +impl<'cx, L, R> TryIntoJs<'cx> for Either +where + L: TryIntoJs<'cx>, + R: TryIntoJs<'cx>, +{ + type Value = JsValue; + + fn try_into_js(self, cx: &mut Cx<'cx>) -> JsResult<'cx, Self::Value> { + match self { + Either::Left(v) => v.try_into_js(cx).map(|v| v.upcast()), + Either::Right(v) => v.try_into_js(cx).map(|v| v.upcast()), + } + } +} + +impl private::Sealed for Either {} + +#[derive(Debug)] +pub struct Error { + left: (&'static str, L), + right: (&'static str, R), +} + +impl<'cx, L, R> Error { + fn new(left: L, right: R) -> Self + where + LT: TryFromJs<'cx, Error = L>, + RT: TryFromJs<'cx, Error = R>, + { + Self { + left: (any::type_name::(), left), + right: (any::type_name::(), right), + } + } +} + +impl fmt::Display for Error +where + L: fmt::Display, + R: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "Either::Left: {}", self.left.1)?; + write!(f, "Either::Right: {}", self.right.1) + } +} + +impl error::Error for Error +where + L: error::Error, + R: error::Error, +{ +} + +impl<'cx, L, R> TryIntoJs<'cx> for Error +where + L: TryIntoJs<'cx>, + R: TryIntoJs<'cx>, +{ + type Value = JsError; + + fn try_into_js(self, cx: &mut Cx<'cx>) -> JsResult<'cx, Self::Value> { + let err = JsError::type_error( + cx, + format!("expected either {} or {}", self.left.0, self.right.0,), + )?; + + err.prop(cx, "left").set(self.left.1)?; + err.prop(cx, "right").set(self.right.1)?; + + Ok(err) + } +} + +impl private::Sealed for Error {} diff --git a/crates/neon/src/types_impl/extract/mod.rs b/crates/neon/src/types_impl/extract/mod.rs index 6735a15dc..48957cb54 100644 --- a/crates/neon/src/types_impl/extract/mod.rs +++ b/crates/neon/src/types_impl/extract/mod.rs @@ -121,6 +121,7 @@ pub use self::json::Json; pub mod json; mod boxed; +mod either; mod error; mod private; mod try_from_js; diff --git a/test/napi/Cargo.toml b/test/napi/Cargo.toml index a9236a668..9357fac61 100644 --- a/test/napi/Cargo.toml +++ b/test/napi/Cargo.toml @@ -10,6 +10,7 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] +either = "1.13.0" num-bigint-dig = "0.8.4" once_cell = "1.18.0" tokio = { version = "1.34.0", features = ["rt-multi-thread"] } diff --git a/test/napi/lib/extract.js b/test/napi/lib/extract.js index 7eb51a53f..35c0ef338 100644 --- a/test/napi/lib/extract.js +++ b/test/napi/lib/extract.js @@ -62,4 +62,20 @@ describe("Extractors", () => { assert.strictEqual(addon.extract_json_sum([1, 2, 3, 4]), 10); assert.strictEqual(addon.extract_json_sum([8, 16, 18]), 42); }); + + it("Either", () => { + assert.strictEqual(addon.extract_either("hello"), "String: hello"); + assert.strictEqual(addon.extract_either(42), "Number: 42"); + + assert.throws( + () => addon.extract_either({}), + (err) => { + assert.match(err.message, /expected either.*String.*f64/); + assert.match(err.left.message, /expected string/); + assert.match(err.right.message, /expected number/); + + return true; + } + ); + }); }); diff --git a/test/napi/src/js/extract.rs b/test/napi/src/js/extract.rs index 5e0a1246c..a3c1da5dd 100644 --- a/test/napi/src/js/extract.rs +++ b/test/napi/src/js/extract.rs @@ -1,3 +1,4 @@ +use either::Either; use neon::{prelude::*, types::extract::*}; pub fn extract_values(mut cx: FunctionContext) -> JsResult { @@ -136,3 +137,11 @@ pub fn extract_single_add_one(mut cx: FunctionContext) -> JsResult { Ok(cx.number(n + 1.0)) } + +#[neon::export] +pub fn extract_either(either: Either) -> String { + match either { + Either::Left(s) => format!("String: {s}"), + Either::Right(n) => format!("Number: {n}"), + } +}