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

Implement strongly typed cast from/to JSValue #196

Merged
merged 13 commits into from
Aug 8, 2024
80 changes: 80 additions & 0 deletions src/NodeApi/IJSValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;

#if !NET7_0_OR_GREATER
using System.Reflection;
#endif

namespace Microsoft.JavaScript.NodeApi;

/// <summary>
/// A base interface for a struct that represents a JavaScript value type or a built-in
/// object type. It provides functionality for converting between the struct
/// and <see cref="JSValue"/>.
/// </summary>
/// <typeparam name="TSelf">The derived struct type.</typeparam>
public interface IJSValue<TSelf> : IEquatable<JSValue> where TSelf : struct, IJSValue<TSelf>
{
/// <summary>
/// Converts the derived struct `TSelf` to a <see cref="JSValue"/> with
/// the same `napi_value` handle.
/// </summary>
/// <returns>
/// <see cref="JSValue"/> with the same `napi_value` handle as the derived struct.
/// </returns>
JSValue AsJSValue();

#if NET7_0_OR_GREATER
/// <summary>
/// Checks id the derived struct `TSelf` can be created from a <see cref="JSValue"/>.
vmoroz marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
static abstract bool CanCreateFrom(JSValue value);

/// <summary>
/// Creates a new instance of the derived struct `TSelf` from a <see cref="JSValue"/> without
/// checking the enclosed handle type.
/// </summary>
static abstract TSelf CreateUnchecked(JSValue value);
#endif
}

#if !NET7_0_OR_GREATER
/// <summary>
/// Implements IJSValue interface static functions for the previous .Net versions.
/// </summary>
/// <typeparam name="T"></typeparam>
internal static class IJSValueShim<T> where T : struct, IJSValue<T>
{
/// <summary>
/// A static field to keep a reference to the CanCreateFrom private method.
/// </summary>
private static readonly Func<JSValue, bool> s_canBeCreatedFrom =
vmoroz marked this conversation as resolved.
Show resolved Hide resolved
(Func<JSValue, bool>)Delegate.CreateDelegate(
typeof(Func<JSValue, bool>),
typeof(T).GetMethod(
"CanCreateFrom",
vmoroz marked this conversation as resolved.
Show resolved Hide resolved
BindingFlags.Static | BindingFlags.NonPublic)!);

/// <summary>
/// A static field to keep a reference to the CreateUnchecked private method.
/// </summary>
private static readonly Func<JSValue, T>s_createUnchecked =
(Func<JSValue, T>)Delegate.CreateDelegate(
typeof(Func<JSValue, T>),
typeof(T).GetMethod(
"CreateUnchecked",
vmoroz marked this conversation as resolved.
Show resolved Hide resolved
BindingFlags.Static | BindingFlags.NonPublic)!);

/// <summary>
/// Invokes `T.CanCreateFrom` static public method.
/// </summary>
public static bool CanCreateFrom(JSValue value) => s_canBeCreatedFrom(value);

/// <summary>
/// Invokes `T.CreateUnchecked` static private method.
/// </summary>
public static T CreateUnchecked(JSValue value) => s_createUnchecked(value);
}
#endif
80 changes: 76 additions & 4 deletions src/NodeApi/Interop/JSAbortSignal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,37 @@ namespace Microsoft.JavaScript.NodeApi.Interop;
/// https://nodejs.org/api/globals.html#class-abortsignal
/// https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
/// </remarks>
public readonly struct JSAbortSignal : IEquatable<JSValue>
public readonly struct JSAbortSignal : IJSValue<JSAbortSignal>
{
private readonly JSValue _value;

public static explicit operator JSAbortSignal(JSValue value) => new(value);
public static implicit operator JSValue(JSAbortSignal promise) => promise._value;
/// <summary>
/// Implicitly converts a <see cref="JSAbortSignal" /> to a <see cref="JSValue" />.
/// </summary>
/// <param name="value">The <see cref="JSAbortSignal" /> to convert.</param>
public static implicit operator JSValue(JSAbortSignal value) => value.AsJSValue();

/// <summary>
/// Explicitly converts a <see cref="JSValue" /> to a nullable <see cref="JSAbortSignal" />.
/// </summary>
/// <param name="value">The <see cref="JSValue" /> to convert.</param>
/// <returns>
/// The <see cref="JSAbortSignal" /> if it was successfully created or `null` if it was failed.
/// </returns>
public static explicit operator JSAbortSignal?(JSValue value) => value.As<JSAbortSignal>();

/// <summary>
/// Explicitly converts a <see cref="JSValue" /> to a <see cref="JSAbortSignal" />.
/// </summary>
/// <param name="value">The <see cref="JSValue" /> to convert.</param>
/// <returns><see cref="JSAbortSignal" /> struct created based on this `JSValue`.</returns>
/// <exception cref="InvalidCastException">
/// Thrown when the T struct cannot be created based on this `JSValue`.
/// </exception>
public static explicit operator JSAbortSignal(JSValue value) => value.CastTo<JSAbortSignal>();

public static explicit operator JSAbortSignal(JSObject obj) => (JSAbortSignal)(JSValue)obj;
public static implicit operator JSObject(JSAbortSignal promise) => (JSObject)promise._value;
public static implicit operator JSObject(JSAbortSignal signal) => (JSObject)signal._value;

private JSAbortSignal(JSValue value)
{
Expand All @@ -35,9 +57,59 @@ public static explicit operator CancellationToken(JSAbortSignal signal)

public static explicit operator JSAbortSignal(CancellationToken cancellation)
=> FromCancellationToken(cancellation);

public static explicit operator JSAbortSignal(CancellationToken? cancellation)
=> cancellation.HasValue ? FromCancellationToken(cancellation.Value) : default;

#region IJSValue<JSAbortSignal> implementation

/// <summary>
/// Converts the <see cref="JSAbortSignal" /> to a <see cref="JSValue" />.
/// </summary>
/// <returns>
/// The <see cref="JSValue" /> representation of the <see cref="JSAbortSignal" />.
/// </returns>
public JSValue AsJSValue() => _value;

/// <summary>
/// Determines whether a <see cref="JSAbortSignal" /> can be created from
/// the specified <see cref="JSValue" />.
/// </summary>
/// <param name="value">The <see cref="JSValue" /> to check.</param>
/// <returns>
/// <c>true</c> if a <see cref="JSAbortSignal" /> can be created from
/// the specified <see cref="JSValue" />; otherwise, <c>false</c>.
/// </returns>
#if NET7_0_OR_GREATER
static bool IJSValue<JSAbortSignal>.CanCreateFrom(JSValue value)
#else
#pragma warning disable IDE0051 // It is used by the IJSValueShim<T> class through reflection.
private static bool CanCreateFrom(JSValue value)
#pragma warning restore IDE0051
#endif
=> value.IsObject() && value.InstanceOf(JSValue.Global["AbortSignal"]);

/// <summary>
/// Creates a new instance of <see cref="JSAbortSignal" /> from
/// the specified <see cref="JSValue" />.
/// </summary>
/// <param name="value">
/// The <see cref="JSValue" /> to create a <see cref="JSAbortSignal" /> from.
/// </param>
/// <returns>
/// A new instance of <see cref="JSAbortSignal" /> created from
/// the specified <see cref="JSValue" />.
/// </returns>
#if NET7_0_OR_GREATER
static JSAbortSignal IJSValue<JSAbortSignal>.CreateUnchecked(JSValue value) => new(value);
#else
#pragma warning disable IDE0051 // It is used by the IJSValueShim<T> class through reflection.
private static JSAbortSignal CreateUnchecked(JSValue value) => new(value);
#pragma warning restore IDE0051
#endif

#endregion

private CancellationToken ToCancellationToken()
{
if (!_value.IsObject())
Expand Down
77 changes: 74 additions & 3 deletions src/NodeApi/JSArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,34 @@

namespace Microsoft.JavaScript.NodeApi;

public readonly partial struct JSArray : IList<JSValue>, IEquatable<JSValue>
public readonly partial struct JSArray : IJSValue<JSArray>, IList<JSValue>
{
private readonly JSValue _value;

public static explicit operator JSArray(JSValue value) => new(value);
public static implicit operator JSValue(JSArray arr) => arr._value;
/// <summary>
/// Implicitly converts a <see cref="JSArray" /> to a <see cref="JSValue" />.
/// </summary>
/// <param name="value">The <see cref="JSArray" /> to convert.</param>
public static implicit operator JSValue(JSArray arr) => arr.AsJSValue();

/// <summary>
/// Explicitly converts a <see cref="JSValue" /> to a nullable <see cref="JSArray" />.
/// </summary>
/// <param name="value">The <see cref="JSValue" /> to convert.</param>
/// <returns>
/// The <see cref="JSArray" /> if it was successfully created or `null` if it was failed.
/// </returns>
public static explicit operator JSArray?(JSValue value) => value.As<JSArray>();

/// <summary>
/// Explicitly converts a <see cref="JSValue" /> to a <see cref="JSArray" />.
/// </summary>
/// <param name="value">The <see cref="JSValue" /> to convert.</param>
/// <returns><see cref="JSArray" /> struct created based on this `JSValue`.</returns>
/// <exception cref="InvalidCastException">
/// Thrown when the T struct cannot be created based on this `JSValue`.
/// </exception>
public static explicit operator JSArray(JSValue value) => value.CastTo<JSArray>();

public static explicit operator JSArray(JSObject obj) => (JSArray)(JSValue)obj;
public static implicit operator JSObject(JSArray arr) => (JSObject)arr._value;
Expand Down Expand Up @@ -45,6 +67,55 @@ public JSArray(JSValue[] array)
}
}

#region IJSValue<JSArray> implementation

/// <summary>
/// Converts the <see cref="JSArray" /> to a <see cref="JSValue" />.
/// </summary>
/// <returns>
/// The <see cref="JSValue" /> representation of the <see cref="JSArray" />.
/// </returns>
public JSValue AsJSValue() => _value;

/// <summary>
/// Determines whether a <see cref="JSArray" /> can be created from
/// the specified <see cref="JSValue" />.
/// </summary>
/// <param name="value">The <see cref="JSValue" /> to check.</param>
/// <returns>
/// <c>true</c> if a <see cref="JSArray" /> can be created from
/// the specified <see cref="JSValue" />; otherwise, <c>false</c>.
/// </returns>
#if NET7_0_OR_GREATER
static bool IJSValue<JSArray>.CanCreateFrom(JSValue value)
#else
#pragma warning disable IDE0051 // It is used by the IJSValueShim<T> class through reflection.
private static bool CanCreateFrom(JSValue value)
#pragma warning restore IDE0051
#endif
=> value.IsArray();

/// <summary>
/// Creates a new instance of <see cref="JSArray" /> from
/// the specified <see cref="JSValue" />.
/// </summary>
/// <param name="value">
/// The <see cref="JSValue" /> to create a <see cref="JSArray" /> from.
/// </param>
/// <returns>
/// A new instance of <see cref="JSArray" /> created from
/// the specified <see cref="JSValue" />.
/// </returns>
#if NET7_0_OR_GREATER
static JSArray IJSValue<JSArray>.CreateUnchecked(JSValue value) => new(value);
#else
#pragma warning disable IDE0051 // It is used by the IJSValueShim<T> class through reflection.
private static JSArray CreateUnchecked(JSValue value) => new(value);
#pragma warning restore IDE0051
#endif

#endregion

/// <inheritdoc/>
public int Length => _value.GetArrayLength();

Expand Down
79 changes: 76 additions & 3 deletions src/NodeApi/JSAsyncIterable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,36 @@

namespace Microsoft.JavaScript.NodeApi;

public readonly partial struct JSAsyncIterable : IAsyncEnumerable<JSValue>, IEquatable<JSValue>
public readonly partial struct JSAsyncIterable :
IJSValue<JSAsyncIterable>, IAsyncEnumerable<JSValue>
{
private readonly JSValue _value;

public static explicit operator JSAsyncIterable(JSValue value) => new(value);
public static implicit operator JSValue(JSAsyncIterable iterable) => iterable._value;
/// <summary>
/// Implicitly converts a <see cref="JSAsyncIterable" /> to a <see cref="JSValue" />.
/// </summary>
/// <param name="value">The <see cref="JSAsyncIterable" /> to convert.</param>
public static implicit operator JSValue(JSAsyncIterable value) => value.AsJSValue();

/// <summary>
/// Explicitly converts a <see cref="JSValue" /> to a nullable <see cref="JSAsyncIterable" />.
/// </summary>
/// <param name="value">The <see cref="JSValue" /> to convert.</param>
/// <returns>
/// The <see cref="JSAsyncIterable" /> if it was successfully created or `null` if it was failed.
/// </returns>
public static explicit operator JSAsyncIterable?(JSValue value) => value.As<JSAsyncIterable>();

/// <summary>
/// Explicitly converts a <see cref="JSValue" /> to a <see cref="JSAsyncIterable" />.
/// </summary>
/// <param name="value">The <see cref="JSValue" /> to convert.</param>
/// <returns><see cref="JSAsyncIterable" /> struct created based on this `JSValue`.</returns>
/// <exception cref="InvalidCastException">
/// Thrown when the T struct cannot be created based on this `JSValue`.
/// </exception>
public static explicit operator JSAsyncIterable(JSValue value)
=> value.CastTo<JSAsyncIterable>();

public static explicit operator JSAsyncIterable(JSObject obj) => (JSAsyncIterable)(JSValue)obj;
public static implicit operator JSObject(JSAsyncIterable iterable) => (JSObject)iterable._value;
Expand All @@ -23,6 +47,55 @@ private JSAsyncIterable(JSValue value)
_value = value;
}

#region IJSValue<JSAsyncIterable> implementation

/// <summary>
/// Converts the <see cref="JSAsyncIterable" /> to a <see cref="JSValue" />.
/// </summary>
/// <returns>
/// The <see cref="JSValue" /> representation of the <see cref="JSAsyncIterable" />.
/// </returns>
public JSValue AsJSValue() => _value;

/// <summary>
/// Determines whether a <see cref="JSAsyncIterable" /> can be created from
/// the specified <see cref="JSValue" />.
/// </summary>
/// <param name="value">The <see cref="JSValue" /> to check.</param>
/// <returns>
/// <c>true</c> if a <see cref="JSAsyncIterable" /> can be created from
/// the specified <see cref="JSValue" />; otherwise, <c>false</c>.
/// </returns>
#if NET7_0_OR_GREATER
static bool IJSValue<JSAsyncIterable>.CanCreateFrom(JSValue value)
#else
#pragma warning disable IDE0051 // It is used by the IJSValueShim<T> class through reflection.
private static bool CanCreateFrom(JSValue value)
#pragma warning restore IDE0051
#endif
=> value.IsObject() && value.HasProperty(JSSymbol.AsyncIterator);

/// <summary>
/// Creates a new instance of <see cref="JSAsyncIterable" /> from
/// the specified <see cref="JSValue" />.
/// </summary>
/// <param name="value">
/// The <see cref="JSValue" /> to create a <see cref="JSAsyncIterable" /> from.
/// </param>
/// <returns>
/// A new instance of <see cref="JSAsyncIterable" /> created from
/// the specified <see cref="JSValue" />.
/// </returns>
#if NET7_0_OR_GREATER
static JSAsyncIterable IJSValue<JSAsyncIterable>.CreateUnchecked(JSValue value) => new(value);
#else
#pragma warning disable IDE0051 // It is used by the IJSValueShim<T> class through reflection.
private static JSAsyncIterable CreateUnchecked(JSValue value) => new(value);
#pragma warning restore IDE0051
#endif

#endregion

#pragma warning disable IDE0060 // Unused parameter
public Enumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
=> new(_value);
Expand Down
Loading
Loading