diff --git a/Lambda2Js.Tests/CustomClassMethodTests.cs b/Lambda2Js.Tests/CustomClassMethodTests.cs index d9dea09..81f1932 100644 --- a/Lambda2Js.Tests/CustomClassMethodTests.cs +++ b/Lambda2Js.Tests/CustomClassMethodTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq.Expressions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; @@ -36,9 +37,20 @@ public static int GetValue(int x) [JsonProperty(PropertyName = "otherName2")] public string Custom2 { get; set; } - + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string Custom3 { get; set; } + + public Dictionary Dictionary { get; set; } + + public List List { get; set; } + + public NestedThing Nested { get; set; } + } + + public class NestedThing + { + public string Name { get; set; } } public class MyCustomClassMethods : JavascriptConversionExtension @@ -150,6 +162,77 @@ public void NewCustomClassAsNewOfTypeWithMemberInit() Assert.IsInstanceOfType(exception, typeof(NotSupportedException), "Exception not thrown."); } + [TestMethod] + public void NewCustomClassWithNestedThingInit() + { + Expression> expr = () => new MyCustomClass { Name = "Miguel", Nested = { Name = "Nested" } }; + + var js = expr.Body.CompileToJavascript( + new JavascriptCompilationOptions( + new MemberInitAsJson(typeof(MyCustomClass)))); + + Assert.AreEqual("{Name:\"Miguel\",Nested:{Name:\"Nested\"}}", js); + } + + [TestMethod] + public void NewCustomClassWithNestedThingConstructorFailsWithDisallowedTypeInit() + { + Expression> expr = () => new MyCustomClass { Name = "Miguel", Nested = new NestedThing { Name = "Nested" } }; + + Exception exception = null; + try + { + expr.Body.CompileToJavascript( + new JavascriptCompilationOptions( + new MemberInitAsJson(typeof(MyCustomClass)))); + } + catch (Exception ex) + { + exception = ex; + } + + Assert.IsInstanceOfType(exception, typeof(NotSupportedException), "Exception not thrown."); + } + + [TestMethod] + public void NewCustomClassWithNestedThingConstructorInit() + { + Expression> expr = () => new MyCustomClass { Name = "Miguel", Nested = new NestedThing { Name = "Nested" } }; + + var js = expr.Body.CompileToJavascript( + new JavascriptCompilationOptions( + new MemberInitAsJson(typeof(MyCustomClass), typeof(NestedThing)))); + + Assert.AreEqual("{Name:\"Miguel\",Nested:{Name:\"Nested\"}}", js); + } + + [TestMethod] + public void NewCustomClassWithListInit() + { + Expression> expr = () => new MyCustomClass { Name = "Miguel", List = { "One", "Two" } }; + + var js = expr.Body.CompileToJavascript( + new JavascriptCompilationOptions( + new MemberInitAsJson(typeof(MyCustomClass)))); + + Assert.AreEqual("{Name:\"Miguel\",List:[\"One\",\"Two\"]}", js); + } + + //This Dictionary constructor is only present for netstandard 2.0 or later (netcoreapp2.0 tests, not netcoreapp1.0) +#if !NETCOREAPP1_1 + [TestMethod] + public void NewCustomClassWithDictionaryFromListConstructorInit() + { + Expression> expr = () => new MyCustomClass + { Name = "Miguel", Dictionary = new Dictionary(new[] { new KeyValuePair("One", 1), new KeyValuePair("Two", 2) }) }; + + var js = expr.Body.CompileToJavascript( + new JavascriptCompilationOptions(MemberInitAsJson.ForAllTypes)); + + Assert.AreEqual("{Name:\"Miguel\",Dictionary:{\"One\":1,\"Two\":2}}", js); + } +#endif + [TestMethod] public void CustomMetadata1() { diff --git a/Lambda2Js/Plugins/MemberInitAsJson.cs b/Lambda2Js/Plugins/MemberInitAsJson.cs index 8ff38c8..22702dd 100644 --- a/Lambda2Js/Plugins/MemberInitAsJson.cs +++ b/Lambda2Js/Plugins/MemberInitAsJson.cs @@ -1,7 +1,9 @@ using System; +using System.Collections; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Text.RegularExpressions; using JetBrains.Annotations; @@ -51,19 +53,22 @@ public MemberInitAsJson([NotNull] Predicate typePredicate) this.TypePredicate = typePredicate; } + private bool IsAcceptableType(Type type) + { + var typeOk1 = NewObjectTypes?.Contains(type) ?? false; + var typeOk2 = TypePredicate?.Invoke(type) ?? false; + var typeOk3 = NewObjectTypes == null && TypePredicate == null; + + return typeOk1 || typeOk2 || typeOk3; + } + public override void ConvertToJavascript(JavascriptConversionContext context) { var initExpr = context.Node as MemberInitExpression; if (initExpr == null) return; - var typeOk1 = this.NewObjectTypes?.Contains(initExpr.Type) ?? false; - var typeOk2 = this.TypePredicate?.Invoke(initExpr.Type) ?? false; - var typeOk3 = this.NewObjectTypes == null && this.TypePredicate == null; - if (!typeOk1 && !typeOk2 && !typeOk3) - return; - if (initExpr.NewExpression.Arguments.Count > 0) - return; - if (initExpr.Bindings.Any(mb => mb.BindingType != MemberBindingType.Assignment)) + + if (!IsAcceptableType(initExpr.Type)) return; context.PreventDefault(); @@ -72,26 +77,123 @@ public override void ConvertToJavascript(JavascriptConversionContext context) { writer.Write('{'); - var posStart = writer.Length; - foreach (var assignExpr in initExpr.Bindings.Cast()) + foreach (var binding in initExpr.Bindings) { - if (writer.Length > posStart) + if(binding != initExpr.Bindings[0]) writer.Write(','); - var metadataProvider = context.Options.GetMetadataProvider(); - var meta = metadataProvider.GetMemberMetadata(assignExpr.Member); - var memberName = meta?.MemberName; - Debug.Assert(!string.IsNullOrEmpty(memberName), "!string.IsNullOrEmpty(memberName)"); - if (Regex.IsMatch(memberName, @"^\w[\d\w]*$")) - writer.Write(memberName); + WriteBinding(context, binding, writer); + } + + writer.Write('}'); + } + } + + /// + /// Recursively callable WriteBinding() for MemberMemberBinding case + /// + /// + /// + /// + /// + /// + /// + private void WriteBinding(JavascriptConversionContext context, MemberBinding binding, JavascriptWriter writer) + { + var metadataProvider = context.Options.GetMetadataProvider(); + var meta = metadataProvider.GetMemberMetadata(binding.Member); + var memberName = meta?.MemberName; + Debug.Assert(!string.IsNullOrEmpty(memberName), "!string.IsNullOrEmpty(memberName)"); + if (Regex.IsMatch(memberName, @"^\w[\d\w]*$")) + writer.Write(memberName); + else + writer.WriteLiteral(memberName); + + writer.Write(':'); + + if (binding is MemberAssignment ma) + { + if (ma.Expression is NewExpression ne) + { + if(!IsAcceptableType(ne.Type)) + throw new InvalidOperationException($"Unable to initialize type: {ne.Type.FullName}"); + + if (typeof(IDictionary).GetTypeInfo().IsAssignableFrom(ne.Type.GetTypeInfo())) + { + writer.Write('{'); + + //Expressions don't support dictionary initializer syntax, so... + //Handle Dictionary(IEnumerable) constructor - new Dictionary(new[] { new KeyValuePair("One", new Thing { Name = "Fred" }) }) + if (ne.Arguments.Count == 1 && ne.Arguments[0] is NewArrayExpression nae) + { + foreach (var nie in nae.Expressions) + { + if (nie is NewExpression newItem) + { + //Get KVP constructor args for Key and Value + var key = newItem.Arguments[0]; + var value = newItem.Arguments[1]; + if(nie != nae.Expressions[0]) + writer.Write(','); + context.Visitor.Visit(key); + writer.Write(':'); + context.Visitor.Visit(value); + } + else + { + throw new NotSupportedException($"Not supported for Dictionary item expression: {nie}"); + } + } + } + else + { + throw new NotSupportedException($"Not supported for Dictionary constructor with {ne.Arguments.Count} arguments: {ne}"); + } + + writer.Write('}'); + } else - writer.WriteLiteral(memberName); + { + throw new NotSupportedException($"Not supported for non-dictionary constructor: {ne}"); + } + + } + else + { + context.Visitor.Visit(ma.Expression); + } + + } + else if (binding is MemberMemberBinding mmb) + { + //Nested object initializers: new Thing{ Nested = new NestedThing { Name="Fred" } } + writer.Write('{'); + foreach (var mb in mmb.Bindings) + { + if (mb != mmb.Bindings[0]) + writer.Write(','); + WriteBinding(context, mb, writer); + } + writer.Write('}'); + } + else if (binding is MemberListBinding mlb) + { + //List binding + + writer.Write('['); - writer.Write(':'); - context.Visitor.Visit(assignExpr.Expression); + foreach (var initializer in mlb.Initializers) + { + if (initializer != mlb.Initializers[0]) + writer.Write(","); + context.Visitor.Visit(initializer.Arguments[0]); } - writer.Write('}'); + writer.Write(']'); + } + else + { + throw new NotSupportedException($"Unsupported: {binding.Member.Name} - {binding.BindingType} ({binding.GetType().FullName})"); } } }