diff --git a/Jint.Tests.PublicInterface/InteropTests.Json.cs b/Jint.Tests.PublicInterface/InteropTests.Json.cs index 65e6c073b..fafc2f0be 100644 --- a/Jint.Tests.PublicInterface/InteropTests.Json.cs +++ b/Jint.Tests.PublicInterface/InteropTests.Json.cs @@ -1,4 +1,5 @@ using System.Dynamic; +using FluentAssertions; using Jint.Runtime.Interop; namespace Jint.Tests.PublicInterface; @@ -102,31 +103,27 @@ public void CanStringifyTimeSpanUsingCustomToJsonHook() Assert.Equal(expected, value); } - + [Fact] public void CanStringifyUsingSerializeToJson() { object testObject = new { Foo = "bar", FooBar = new { Foo = 123.45, Foobar = new DateTime(2022, 7, 16, 0, 0, 0, DateTimeKind.Utc) } }; - + // without interop - - var engineNoInterop = new Engine(); - engineNoInterop.SetValue("TimeSpan", TypeReference.CreateTypeReference(engineNoInterop)); - Assert.Throws( - () => engineNoInterop.Evaluate("JSON.stringify(TimeSpan.FromSeconds(3))")); - - engineNoInterop.SetValue("TestObject", testObject); - Assert.Equal( - "{\"Foo\":\"bar\",\"FooBar\":{\"Foo\":123.45,\"Foobar\":\"2022-07-16T00:00:00.000Z\"}}", - engineNoInterop.Evaluate("JSON.stringify(TestObject)")); - + + var e = new Engine(); + e.SetValue("TimeSpan", typeof(TimeSpan)); +#if NETFRAMEWORK + e.Evaluate("JSON.stringify(TimeSpan.FromSeconds(3))").AsString().Should().Be("""{"Ticks":30000000,"Days":0,"Hours":0,"Milliseconds":0,"Minutes":0,"Seconds":3,"TotalDays":0.00003472222222222222,"TotalHours":0.0008333333333333333,"TotalMilliseconds":3000,"TotalMinutes":0.05,"TotalSeconds":3}"""); +#else + e.Evaluate("JSON.stringify(TimeSpan.FromSeconds(3))").AsString().Should().Be("""{"Ticks":30000000,"Days":0,"Hours":0,"Milliseconds":0,"Microseconds":0,"Nanoseconds":0,"Minutes":0,"Seconds":3,"TotalDays":0.00003472222222222222,"TotalHours":0.0008333333333333334,"TotalMilliseconds":3000,"TotalMicroseconds":3000000,"TotalNanoseconds":3000000000,"TotalMinutes":0.05,"TotalSeconds":3}"""); +#endif + + e.SetValue("TestObject", testObject); + e.Evaluate("JSON.stringify(TestObject)").AsString().Should().Be("""{"Foo":"bar","FooBar":{"Foo":123.45,"Foobar":"2022-07-16T00:00:00.000Z"}}"""); + // interop using Newtonsoft serializer, for example with snake case naming - - string Serialize(object o) => - Newtonsoft.Json.JsonConvert.SerializeObject(o, - new Newtonsoft.Json.JsonSerializerSettings { - ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver { - NamingStrategy = new Newtonsoft.Json.Serialization.SnakeCaseNamingStrategy() } }); + var engine = new Engine(options => { options.Interop.SerializeToJson = Serialize; @@ -136,13 +133,26 @@ string Serialize(object o) => var expected = Serialize(TimeSpan.FromSeconds(3)); var actual = engine.Evaluate("JSON.stringify(TimeSpan.FromSeconds(3));"); - Assert.Equal(expected, actual); - + actual.AsString().Should().Be(expected); + expected = Serialize(testObject); actual = engine.Evaluate("JSON.stringify(TestObject)"); - Assert.Equal(expected, actual); + actual.AsString().Should().Be(expected); actual = engine.Evaluate("JSON.stringify({ nestedValue: TestObject })"); - Assert.Equal($@"{{""nestedValue"":{expected}}}", actual); + actual.AsString().Should().Be($$"""{"nestedValue":{{expected}}}"""); + return; + + string Serialize(object o) + { + var settings = new Newtonsoft.Json.JsonSerializerSettings + { + ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver + { + NamingStrategy = new Newtonsoft.Json.Serialization.SnakeCaseNamingStrategy() + } + }; + return Newtonsoft.Json.JsonConvert.SerializeObject(o, settings); + } } } diff --git a/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj b/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj index bf3c659d5..06e26b9cf 100644 --- a/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj +++ b/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj @@ -14,6 +14,7 @@ + diff --git a/Jint.Tests/Runtime/InteropTests.cs b/Jint.Tests/Runtime/InteropTests.cs index 3655bc3d0..8b35900c0 100644 --- a/Jint.Tests/Runtime/InteropTests.cs +++ b/Jint.Tests/Runtime/InteropTests.cs @@ -55,7 +55,7 @@ public class Bar [Fact] public void ShouldStringifyNetObjects() { - _engine.SetValue("foo", new Foo()); + _engine.SetValue("foo", typeof(Foo)); var json = _engine.Evaluate("JSON.stringify(foo.GetBar())").AsString(); Assert.Equal("{\"Test\":\"123\"}", json); } @@ -2781,17 +2781,20 @@ static IEnumerable MemberNameCreator(MemberInfo prop) options.SetTypeResolver(customTypeResolver); options.AddExtensionMethods(typeof(CustomNamedExtensions)); }); + engine.SetValue("o", new CustomNamed()); Assert.Equal("StringField", engine.Evaluate("o.jsStringField").AsString()); Assert.Equal("StringField", engine.Evaluate("o.jsStringField2").AsString()); - Assert.Equal("StaticStringField", engine.Evaluate("o.jsStaticStringField").AsString()); Assert.Equal("StringProperty", engine.Evaluate("o.jsStringProperty").AsString()); Assert.Equal("Method", engine.Evaluate("o.jsMethod()").AsString()); - Assert.Equal("StaticMethod", engine.Evaluate("o.jsStaticMethod()").AsString()); Assert.Equal("InterfaceStringProperty", engine.Evaluate("o.jsInterfaceStringProperty").AsString()); Assert.Equal("InterfaceMethod", engine.Evaluate("o.jsInterfaceMethod()").AsString()); Assert.Equal("ExtensionMethod", engine.Evaluate("o.jsExtensionMethod()").AsString()); + engine.SetValue("CustomNamed", typeof(CustomNamed)); + Assert.Equal("StaticStringField", engine.Evaluate("CustomNamed.jsStaticStringField").AsString()); + Assert.Equal("StaticMethod", engine.Evaluate("CustomNamed.jsStaticMethod()").AsString()); + engine.SetValue("XmlHttpRequest", typeof(CustomNamedEnum)); engine.Evaluate("o.jsEnumProperty = XmlHttpRequest.HEADERS_RECEIVED;"); Assert.Equal((int) CustomNamedEnum.HeadersReceived, engine.Evaluate("o.jsEnumProperty").AsNumber()); diff --git a/Jint/Options.cs b/Jint/Options.cs index 1cee53c6c..21f873570 100644 --- a/Jint/Options.cs +++ b/Jint/Options.cs @@ -371,6 +371,11 @@ public class InteropOptions /// All other values are ignored. /// public MemberTypes ObjectWrapperReportedMemberTypes { get; set; } = MemberTypes.Field | MemberTypes.Property | MemberTypes.Method; + + /// + /// Reported member binding flags when reflecting, defaults to | . + /// + public BindingFlags ObjectWrapperReportedBindingFlags { get; set; } = BindingFlags.Instance | BindingFlags.Public; } public class ConstraintOptions diff --git a/Jint/Runtime/Interop/ObjectWrapper.cs b/Jint/Runtime/Interop/ObjectWrapper.cs index adb0c50ab..1a9a56739 100644 --- a/Jint/Runtime/Interop/ObjectWrapper.cs +++ b/Jint/Runtime/Interop/ObjectWrapper.cs @@ -255,9 +255,8 @@ private IEnumerable EnumerateOwnPropertyKeys(Types types) { var interopOptions = _engine.Options.Interop; - // we take public properties, fields and methods - var bindingFlags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public; - + // we take properties, fields and methods + var bindingFlags = interopOptions.ObjectWrapperReportedBindingFlags; if ((interopOptions.ObjectWrapperReportedMemberTypes & MemberTypes.Property) == MemberTypes.Property) { foreach (var p in ClrType.GetProperties(bindingFlags)) diff --git a/Jint/Runtime/Interop/TypeResolver.cs b/Jint/Runtime/Interop/TypeResolver.cs index 7fda37c9f..c1e1c1a36 100644 --- a/Jint/Runtime/Interop/TypeResolver.cs +++ b/Jint/Runtime/Interop/TypeResolver.cs @@ -116,11 +116,11 @@ private ReflectionAccessor ResolvePropertyDescriptorFactory( // we can always check indexer if there's one, and then fall back to properties if indexer returns null IndexerAccessor.TryFindIndexer(engine, type, memberName, out var indexerAccessor, out var indexer); - const BindingFlags BindingFlags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public; + var bindingFlags = engine.Options.Interop.ObjectWrapperReportedBindingFlags; // properties and fields cannot be numbers if (!isInteger - && TryFindMemberAccessor(engine, type, memberName, BindingFlags, indexer, out var temp) + && TryFindMemberAccessor(engine, type, memberName, bindingFlags, indexer, out var temp) && (!mustBeReadable || temp.Readable) && (!mustBeWritable || temp.Writable)) {