Skip to content

Commit

Permalink
Implement strongly typed cast from/to JSValue (#196)
Browse files Browse the repository at this point in the history
  • Loading branch information
vmoroz authored Aug 8, 2024
1 parent f7beb3c commit d52ad7c
Show file tree
Hide file tree
Showing 19 changed files with 2,413 additions and 36 deletions.
110 changes: 110 additions & 0 deletions src/NodeApi/IJSValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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>
/// Checks if the T struct can be created from this instance`.
/// </summary>
/// <typeparam name="T">A struct that implements IJSValue interface.</typeparam>
/// <returns>
/// `true` if the T struct can be created from this instance. Otherwise it returns `false`.
/// </returns>
bool Is<T>() where T : struct, IJSValue<T>;

/// <summary>
/// Tries to create a T struct from this instance.
/// It returns `null` if the T struct cannot be created.
/// </summary>
/// <typeparam name="T">A struct that implements IJSValue interface.</typeparam>
/// <returns>
/// Nullable value that contains T struct if it was successfully created
/// or `null` if it was failed.
/// </returns>
T? As<T>() where T : struct, IJSValue<T>;

/// <summary>
/// Creates a T struct from this instance without checking the enclosed handle type.
/// It must be used only when the handle type is known to be correct.
/// </summary>
/// <typeparam name="T">A struct that implements IJSValue interface.</typeparam>
/// <returns>T struct created based on this instance.</returns>
T AsUnchecked<T>() where T : struct, IJSValue<T>;

/// <summary>
/// Creates a T struct from this instance.
/// It throws `InvalidCastException` in case of failure.
/// </summary>
/// <typeparam name="T">A struct that implements IJSValue interface.</typeparam>
/// <returns>T struct created based on this instance.</returns>
/// <exception cref="InvalidCastException">
/// Thrown when the T struct cannot be crated based on this instance.
/// </exception>
T CastTo<T>() where T : struct, IJSValue<T>;

#if NET7_0_OR_GREATER
/// <summary>
/// Checks if the derived struct `TSelf` can be created from a <see cref="JSValue"/>.
/// </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_canCreateFrom =
(Func<JSValue, bool>)Delegate.CreateDelegate(
typeof(Func<JSValue, bool>),
typeof(T).GetMethod(
nameof(CanCreateFrom),
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(
nameof(CreateUnchecked),
BindingFlags.Static | BindingFlags.NonPublic)!);

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

/// <summary>
/// Invokes `T.CreateUnchecked` static private method.
/// </summary>
public static T CreateUnchecked(JSValue value) => s_createUnchecked(value);
}
#endif
111 changes: 107 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 signal) => signal._value;

/// <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,90 @@ 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>
/// Checks if the T struct can be created from this instance`.
/// </summary>
/// <typeparam name="T">A struct that implements IJSValue interface.</typeparam>
/// <returns>
/// `true` if the T struct can be created from this instance. Otherwise it returns `false`.
/// </returns>
public bool Is<T>() where T : struct, IJSValue<T> => _value.Is<T>();

/// <summary>
/// Tries to create a T struct from this instance.
/// It returns `null` if the T struct cannot be created.
/// </summary>
/// <typeparam name="T">A struct that implements IJSValue interface.</typeparam>
/// <returns>
/// Nullable value that contains T struct if it was successfully created
/// or `null` if it was failed.
/// </returns>
public T? As<T>() where T : struct, IJSValue<T> => _value.As<T>();

/// <summary>
/// Creates a T struct from this instance without checking the enclosed handle type.
/// It must be used only when the handle type is known to be correct.
/// </summary>
/// <typeparam name="T">A struct that implements IJSValue interface.</typeparam>
/// <returns>T struct created based on this instance.</returns>
public T AsUnchecked<T>() where T : struct, IJSValue<T> => _value.AsUnchecked<T>();

/// <summary>
/// Creates a T struct from this instance.
/// It throws `InvalidCastException` in case of failure.
/// </summary>
/// <typeparam name="T">A struct that implements IJSValue interface.</typeparam>
/// <returns>T struct created based on this instance.</returns>
/// <exception cref="InvalidCastException">
/// Thrown when the T struct cannot be crated based on this instance.
/// </exception>
public T CastTo<T>() where T : struct, IJSValue<T> => _value.CastTo<T>();

/// <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
106 changes: 104 additions & 2 deletions src/NodeApi/JSArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,35 @@

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);
/// <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._value;

/// <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,86 @@ public JSArray(JSValue[] array)
}
}

#region IJSValue<JSArray> implementation

/// <summary>
/// Checks if the T struct can be created from this instance`.
/// </summary>
/// <typeparam name="T">A struct that implements IJSValue interface.</typeparam>
/// <returns>
/// `true` if the T struct can be created from this instance. Otherwise it returns `false`.
/// </returns>
public bool Is<T>() where T : struct, IJSValue<T> => _value.Is<T>();

/// <summary>
/// Tries to create a T struct from this instance.
/// It returns `null` if the T struct cannot be created.
/// </summary>
/// <typeparam name="T">A struct that implements IJSValue interface.</typeparam>
/// <returns>
/// Nullable value that contains T struct if it was successfully created
/// or `null` if it was failed.
/// </returns>
public T? As<T>() where T : struct, IJSValue<T> => _value.As<T>();

/// <summary>
/// Creates a T struct from this instance without checking the enclosed handle type.
/// It must be used only when the handle type is known to be correct.
/// </summary>
/// <typeparam name="T">A struct that implements IJSValue interface.</typeparam>
/// <returns>T struct created based on this instance.</returns>
public T AsUnchecked<T>() where T : struct, IJSValue<T> => _value.AsUnchecked<T>();

/// <summary>
/// Creates a T struct from this instance.
/// It throws `InvalidCastException` in case of failure.
/// </summary>
/// <typeparam name="T">A struct that implements IJSValue interface.</typeparam>
/// <returns>T struct created based on this instance.</returns>
/// <exception cref="InvalidCastException">
/// Thrown when the T struct cannot be crated based on this instance.
/// </exception>
public T CastTo<T>() where T : struct, IJSValue<T> => _value.CastTo<T>();

/// <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
Loading

0 comments on commit d52ad7c

Please sign in to comment.