Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(neon-macros): Export Macro #1025

Merged
merged 11 commits into from
May 10, 2024
178 changes: 178 additions & 0 deletions crates/neon/src/types_impl/extract/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use std::{error, fmt};

use crate::{
context::Context,
handle::Handle,
object::Object,
result::{JsResult, NeonResult, ResultExt},
types::{
extract::{TryFromJs, TryIntoJs, TypeExpected},
JsError, JsString, JsValue,
},
};

type BoxError = Box<dyn error::Error + Send + Sync + 'static>;

#[derive(Debug)]
/// Error that implements [`TryFromJs`] and [`TryIntoJs`] and can produce specific error types
///
/// **Note**: Extracting an [`Error`] from a [`JsValue`] with [`TryFromJs`] and converting
/// back to a [`JsError`] with [`TryIntoJs`] is _lossy_. It is not guaranteed that the same
/// type will be returned.
pub struct Error {
kjvalencik marked this conversation as resolved.
Show resolved Hide resolved
cause: BoxError,
kind: Option<ErrorKind>,
}

#[derive(Debug)]
enum ErrorKind {
Error,
RangeError,
TypeError,
}

impl Error {
/// Create a new [`Error`] from a `cause`
pub fn new<E>(cause: E) -> Self
where
E: Into<BoxError>,
{
Self::create(ErrorKind::Error, cause)
}

/// Create a `RangeError`
pub fn range_error<E>(cause: E) -> Self
where
E: Into<BoxError>,
{
Self::create(ErrorKind::RangeError, cause)
}

/// Create a `TypeError`
pub fn type_error<E>(cause: E) -> Self
where
E: Into<BoxError>,
{
Self::create(ErrorKind::TypeError, cause)
}

/// Check if error is a `RangeError`
pub fn is_range_error(&self) -> bool {
matches!(self.kind, Some(ErrorKind::RangeError))
}

/// Check if error is a `TypeError`
pub fn is_type_error(&self) -> bool {
matches!(self.kind, Some(ErrorKind::TypeError))
}

/// Get a reference to the underlying `cause`
pub fn cause(&self) -> &BoxError {
&self.cause
}

/// Extract the `std::error::Error` cause
pub fn into_cause(self) -> BoxError {
self.cause
}

fn create<E>(kind: ErrorKind, cause: E) -> Self
where
E: Into<BoxError>,
{
Self {
cause: cause.into(),
kind: Some(kind),
}
}
}

// Blanket impl allow for ergonomic `?` error handling from typical error types (including `anyhow`)
impl<E> From<E> for Error
where
E: Into<BoxError>,
{
fn from(cause: E) -> Self {
Self::new(cause)
}
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}: {}", self.kind, self.cause)
}
}

// `TryFromJs` followed by `TryIntoJs` is *lossy*. We cannot guarantee the same type
// will be created.
impl<'cx> TryIntoJs<'cx> for Error {
type Value = JsError;

fn try_into_js<C>(self, cx: &mut C) -> JsResult<'cx, Self::Value>
where
C: Context<'cx>,
{
let message = self.cause.to_string();

match self.kind {
Some(ErrorKind::RangeError) => cx.range_error(message),
Some(ErrorKind::TypeError) => cx.type_error(message),
_ => cx.error(message),
}
}
}

impl<'cx> TryFromJs<'cx> for Error {
type Error = TypeExpected<JsError>;

fn try_from_js<C>(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult<Result<Self, Self::Error>>
where
C: Context<'cx>,
{
let err = match v.downcast::<JsError, _>(cx) {
Ok(err) => err,
Err(_) => return Ok(Err(Self::Error::new())),
};

Ok(Ok(Self {
cause: get_message(cx, err)?.into(),
kind: get_kind(cx, err)?,
}))
}

fn from_js<C>(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult<Self>
where
C: Context<'cx>,
{
Self::try_from_js(cx, v)?.or_throw(cx)
}
}

fn get_message<'cx, C>(cx: &mut C, err: Handle<JsError>) -> NeonResult<String>
where
C: Context<'cx>,
{
let message = err
.get_value(cx, "message")?
.downcast::<JsString, _>(cx)
.map(|v| v.value(cx))
.unwrap_or_default();

Ok(message)
}

fn get_kind<'cx, C>(cx: &mut C, err: Handle<JsError>) -> NeonResult<Option<ErrorKind>>
where
C: Context<'cx>,
{
let name = match err.get_value(cx, "name")?.downcast::<JsString, _>(cx) {
Ok(v) => v.value(cx),
Err(_) => return Ok(None),
};

Ok(Some(match name.as_str() {
"TypeError" => ErrorKind::TypeError,
kjvalencik marked this conversation as resolved.
Show resolved Hide resolved
"RangeError" => ErrorKind::RangeError,
_ => return Ok(None),
}))
}
57 changes: 55 additions & 2 deletions crates/neon/src/types_impl/extract/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
object::Object,
result::{JsResult, NeonResult},
types::{
extract::{private, TryFromJs},
extract::{private, TryFromJs, TryIntoJs},
JsFunction, JsObject, JsString, JsValue,
},
};
Expand Down Expand Up @@ -53,7 +53,44 @@ where
.map(|s| s.value(cx))
}

/// Extract a value by serializing to JSON
fn global_json_parse<'cx, C>(cx: &mut C) -> JsResult<'cx, JsFunction>
where
C: Context<'cx>,
{
cx.global::<JsObject>("JSON")?.get(cx, "parse")
}

#[cfg(not(feature = "napi-6"))]
fn json_parse<'cx, C>(cx: &mut C) -> JsResult<'cx, JsFunction>
where
C: Context<'cx>,
{
global_json_parse(cx)
}

#[cfg(feature = "napi-6")]
fn json_parse<'cx, C>(cx: &mut C) -> JsResult<'cx, JsFunction>
where
C: Context<'cx>,
{
static PARSE: LocalKey<Root<JsFunction>> = LocalKey::new();

PARSE
.get_or_try_init(cx, |cx| global_json_parse(cx).map(|f| f.root(cx)))
.map(|f| f.to_inner(cx))
}

fn parse<'cx, C>(cx: &mut C, s: &str) -> JsResult<'cx, JsValue>
where
C: Context<'cx>,
{
let s = cx.string(s).upcast();

json_parse(cx)?.call(cx, s, [s])
}

/// Wrapper for converting between `T` and [`JsValue`](crate::types::JsValue) by
/// serializing with JSON.
pub struct Json<T>(pub T);

impl<'cx, T> TryFromJs<'cx> for Json<T>
Expand All @@ -77,4 +114,20 @@ where
}
}

impl<'cx, T> TryIntoJs<'cx> for Json<T>
where
T: serde::Serialize,
{
type Value = JsValue;

fn try_into_js<C>(self, cx: &mut C) -> JsResult<'cx, Self::Value>
where
C: Context<'cx>,
{
let s = serde_json::to_string(&self.0).or_else(|err| cx.throw_error(err.to_string()))?;

parse(cx, &s)
}
}

impl<T> private::Sealed for Json<T> {}
87 changes: 70 additions & 17 deletions crates/neon/src/types_impl/extract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,40 +99,105 @@
//! Note well, in this example, type annotations are not required on the tuple because
//! Rust is able to infer it from the type arguments on `add` and `concat`.

use std::{fmt, marker::PhantomData};

use crate::{
context::{Context, FunctionContext},
handle::Handle,
result::NeonResult,
types::JsValue,
result::{JsResult, NeonResult, ResultExt},
types::{JsValue, Value},
};

pub use self::error::Error;
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
pub use self::json::*;
pub use self::types::*;
pub use self::json::Json;

mod error;
#[cfg(feature = "serde")]
mod json;
mod types;
mod private;
mod try_from_js;
mod try_into_js;

/// Extract Rust data from a JavaScript value
pub trait TryFromJs<'cx>
where
Self: private::Sealed + Sized,
{
/// Error indicating non-JavaScript exception failure when extracting
// Consider adding a trait bound prior to unsealing `TryFromjs`
// https://github.com/neon-bindings/neon/issues/1026
type Error;

/// Extract this Rust type from a JavaScript value
fn try_from_js<C>(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult<Result<Self, Self::Error>>
where
C: Context<'cx>;

/// Same as [`TryFromJs`], but all errors are converted to JavaScript exceptions
fn from_js<C>(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult<Self>
where
C: Context<'cx>;
}

/// Convert Rust data into a JavaScript value
pub trait TryIntoJs<'cx>
where
Self: private::Sealed,
{
/// The type of JavaScript value that will be created
type Value: Value;

/// Convert `self` into a JavaScript value
fn try_into_js<C>(self, cx: &mut C) -> JsResult<'cx, Self::Value>
where
C: Context<'cx>;
}

#[cfg_attr(docsrs, doc(cfg(feature = "napi-5")))]
#[cfg(feature = "napi-5")]
/// Wrapper for converting between [`f64`] and [`JsDate`](super::JsDate)
pub struct Date(pub f64);

/// Wrapper for converting between [`Vec<u8>`] and [`JsArrayBuffer`](super::JsArrayBuffer)
pub struct ArrayBuffer(pub Vec<u8>);

/// Wrapper for converting between [`Vec<u8>`] and [`JsBuffer`](super::JsBuffer)
pub struct Buffer(pub Vec<u8>);

/// Error returned when a JavaScript value is not the type expected
pub struct TypeExpected<T: Value>(PhantomData<T>);

impl<T: Value> TypeExpected<T> {
fn new() -> Self {
Self(PhantomData)
}
}

impl<T: Value> fmt::Display for TypeExpected<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "expected {}", T::name())
}
}

impl<T: Value> fmt::Debug for TypeExpected<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("TypeExpected").field(&T::name()).finish()
}
}

impl<T: Value> std::error::Error for TypeExpected<T> {}

impl<T, U: Value> ResultExt<T> for Result<T, TypeExpected<U>> {
fn or_throw<'a, C: Context<'a>>(self, cx: &mut C) -> NeonResult<T> {
match self {
Ok(v) => Ok(v),
Err(_) => cx.throw_type_error(format!("expected {}", U::name())),
}
}
}

/// Trait specifying values that may be extracted from function arguments.
///
/// **Note:** This trait is implemented for tuples of up to 32 values, but for
Expand Down Expand Up @@ -221,15 +286,3 @@ from_args!(
T27, T28, T29, T30, T31, T32
]
);

mod private {
use crate::{context::FunctionContext, result::NeonResult};

pub trait Sealed {}

pub trait FromArgsInternal<'cx>: Sized {
fn from_args(cx: &mut FunctionContext<'cx>) -> NeonResult<Self>;

fn from_args_opt(cx: &mut FunctionContext<'cx>) -> NeonResult<Option<Self>>;
}
}
Loading