From 16365067f0619d98e95c7e4d95ba32631c0a4e82 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 1 Jan 2024 10:52:18 +0200 Subject: [PATCH] Make JsValue implement IConvertible (#1713) * allow disabling ExpandoObject conversion --- .../ExtensionMethods/ExtensionMethodsTest.cs | 18 +++ Jint/Native/JsValue.Convertible.cs | 105 ++++++++++++++++++ Jint/Native/JsValue.cs | 2 +- Jint/Native/Object/ObjectInstance.cs | 8 +- Jint/Options.cs | 2 +- 5 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 Jint/Native/JsValue.Convertible.cs diff --git a/Jint.Tests/Runtime/ExtensionMethods/ExtensionMethodsTest.cs b/Jint.Tests/Runtime/ExtensionMethods/ExtensionMethodsTest.cs index b80cc0538f..c47c9581de 100644 --- a/Jint.Tests/Runtime/ExtensionMethods/ExtensionMethodsTest.cs +++ b/Jint.Tests/Runtime/ExtensionMethods/ExtensionMethodsTest.cs @@ -305,5 +305,23 @@ public void GenericExtensionMethodOnGenericTypeInstantiatedInJs() Assert.Equal(false, baseObservableResult.Last); } + [Fact] + public void CanProjectAndGroupWhenExpandoObjectWrappingDisabled() + { + var engine = new Engine(options => + { + options.AllowClr().AddExtensionMethods(typeof(Enumerable)); + // prevent ExpandoObject wrapping + options.Interop.CreateClrObject = null; + }); + engine.Execute("var a = [ 2, 4 ];"); + + var selected = engine.Evaluate("JSON.stringify(a.Select(m => ({a:m,b:m})).ToArray());").AsString(); + Assert.Equal("""[{"a":2,"b":2},{"a":4,"b":4}]""", selected); + + var grouped1 = engine.Evaluate("a.GroupBy(m => ({a:m,b:m})).ToArray()").AsArray(); + var grouped = engine.Evaluate("JSON.stringify(a.GroupBy(m => ({a:m,b:m})).Select(x => x.Key).ToArray());").AsString(); + Assert.Equal("""[{"a":2,"b":2},{"a":4,"b":4}]""", grouped); + } } } diff --git a/Jint/Native/JsValue.Convertible.cs b/Jint/Native/JsValue.Convertible.cs new file mode 100644 index 0000000000..0b58675f3e --- /dev/null +++ b/Jint/Native/JsValue.Convertible.cs @@ -0,0 +1,105 @@ +using Jint.Runtime; + +namespace Jint.Native; + +public partial class JsValue : IConvertible +{ + TypeCode IConvertible.GetTypeCode() + { + var type = _type & ~InternalTypes.InternalFlags; + return type switch + { + InternalTypes.Boolean => TypeCode.Boolean, + InternalTypes.String => TypeCode.String, + InternalTypes.Number => TypeCode.Double, + InternalTypes.Integer => TypeCode.Int32, + InternalTypes.PrivateName => TypeCode.String, + InternalTypes.Undefined => TypeCode.Object, + InternalTypes.Null => TypeCode.Object, + InternalTypes.Object => TypeCode.Object, + InternalTypes.PlainObject => TypeCode.Object, + InternalTypes.Array => TypeCode.Object, + _ => TypeCode.Empty + }; + } + + bool IConvertible.ToBoolean(IFormatProvider? provider) + { + return this.AsBoolean(); + } + + byte IConvertible.ToByte(IFormatProvider? provider) + { + throw new NotImplementedException(); + } + + char IConvertible.ToChar(IFormatProvider? provider) + { + throw new NotImplementedException(); + } + + DateTime IConvertible.ToDateTime(IFormatProvider? provider) + { + throw new NotImplementedException(); + } + + decimal IConvertible.ToDecimal(IFormatProvider? provider) + { + throw new NotImplementedException(); + } + + double IConvertible.ToDouble(IFormatProvider? provider) + { + return this.AsNumber(); + } + + short IConvertible.ToInt16(IFormatProvider? provider) + { + throw new NotImplementedException(); + } + + int IConvertible.ToInt32(IFormatProvider? provider) + { + return this.AsInteger(); + } + + long IConvertible.ToInt64(IFormatProvider? provider) + { + throw new NotImplementedException(); + } + + sbyte IConvertible.ToSByte(IFormatProvider? provider) + { + throw new NotImplementedException(); + } + + float IConvertible.ToSingle(IFormatProvider? provider) + { + throw new NotImplementedException(); + } + + string IConvertible.ToString(IFormatProvider? provider) + { + return this.AsString(); + } + + object IConvertible.ToType(Type conversionType, IFormatProvider? provider) + { + throw new NotImplementedException(); + } + + ushort IConvertible.ToUInt16(IFormatProvider? provider) + { + throw new NotImplementedException(); + } + + uint IConvertible.ToUInt32(IFormatProvider? provider) + { + throw new NotImplementedException(); + } + + ulong IConvertible.ToUInt64(IFormatProvider? provider) + { + throw new NotImplementedException(); + } +} diff --git a/Jint/Native/JsValue.cs b/Jint/Native/JsValue.cs index 3d53cbea10..9f558043af 100644 --- a/Jint/Native/JsValue.cs +++ b/Jint/Native/JsValue.cs @@ -13,7 +13,7 @@ namespace Jint.Native { - public abstract class JsValue : IEquatable + public abstract partial class JsValue : IEquatable { public static readonly JsValue Undefined = new JsUndefined(); public static readonly JsValue Null = new JsNull(); diff --git a/Jint/Native/Object/ObjectInstance.cs b/Jint/Native/Object/ObjectInstance.cs index 9bf079dcd2..337c1320c3 100644 --- a/Jint/Native/Object/ObjectInstance.cs +++ b/Jint/Native/Object/ObjectInstance.cs @@ -1087,7 +1087,13 @@ private object ToObject(ObjectTraverseStack stack) break; } - var o = _engine.Options.Interop.CreateClrObject(this); + var func = _engine.Options.Interop.CreateClrObject; + if (func is null) + { + goto default; + } + + var o = func(this); foreach (var p in GetOwnProperties()) { if (!p.Value.Enumerable) diff --git a/Jint/Options.cs b/Jint/Options.cs index 7c5a119e9d..8d02dc33c0 100644 --- a/Jint/Options.cs +++ b/Jint/Options.cs @@ -326,7 +326,7 @@ public class InteropOptions /// /// Strategy to create a CLR object to hold converted . /// - public Func> CreateClrObject = _ => new ExpandoObject(); + public Func>? CreateClrObject = _ => new ExpandoObject(); /// /// Strategy to create a CLR object from TypeReference.