diff --git a/CHANGELOG.md b/CHANGELOG.md index 74e0aa00..e9275b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 1.5.25 (13 May 2023) +- [#934](https://github.com/WireMock-Net/WireMock.Net/pull/934) - Code generator improvements [feature] contributed by [cezarypiatek](https://github.com/cezarypiatek) + # 1.5.24 (07 May 2023) - [#926](https://github.com/WireMock-Net/WireMock.Net/pull/926) - Fix C# mapping code generator for header names [bug] contributed by [cezarypiatek](https://github.com/cezarypiatek) - [#927](https://github.com/WireMock-Net/WireMock.Net/pull/927) - Enrich generated code with status code [feature] contributed by [cezarypiatek](https://github.com/cezarypiatek) diff --git a/Directory.Build.props b/Directory.Build.props index 091613e7..643c355d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ - 1.5.24 + 1.5.25 WireMock.Net-Logo.png https://github.com/WireMock-Net/WireMock.Net Apache-2.0 diff --git a/Generate-ReleaseNotes.cmd b/Generate-ReleaseNotes.cmd index 7d6d613b..61c3fb5d 100644 --- a/Generate-ReleaseNotes.cmd +++ b/Generate-ReleaseNotes.cmd @@ -1,6 +1,6 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=1.5.24 +SET version=1.5.25 GitHubReleaseNotes --output CHANGELOG.md --skip-empty-releases --exclude-labels question invalid doc duplicate --version %version% --token %GH_TOKEN% diff --git a/PackageReleaseNotes.txt b/PackageReleaseNotes.txt index 5e0d7589..7bcd1a63 100644 --- a/PackageReleaseNotes.txt +++ b/PackageReleaseNotes.txt @@ -1,8 +1,4 @@ -# 1.5.24 (07 May 2023) -- #926 Fix C# mapping code generator for header names [bug] -- #927 Enrich generated code with status code [feature] -- #930 Update C# mapping code generator for WithStatusCode [feature] -- #931 Add property 'IsStartedWithAdminInterface' to 'IWireMockServer' [feature] -- #933 C# code generator improvements [feature] +# 1.5.25 (13 May 2023) +- #934 Code generator improvements [feature] The full release notes can be found here: https://github.com/WireMock-Net/WireMock.Net/blob/master/CHANGELOG.md \ No newline at end of file diff --git a/src/WireMock.Net/Serialization/MappingConverter.cs b/src/WireMock.Net/Serialization/MappingConverter.cs index 21858eb9..aa45caf5 100644 --- a/src/WireMock.Net/Serialization/MappingConverter.cs +++ b/src/WireMock.Net/Serialization/MappingConverter.cs @@ -5,7 +5,6 @@ using System.Net; using System.Text; using System.Threading; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Stef.Validation; using WireMock.Admin.Mappings; using WireMock.Constants; @@ -19,6 +18,7 @@ using WireMock.Settings; using WireMock.Types; using WireMock.Util; +using static WireMock.Util.CSharpFormatter; namespace WireMock.Serialization; internal class MappingConverter @@ -150,18 +150,17 @@ internal class MappingConverter { case BodyType.String: case BodyType.FormUrlEncoded: - sb.AppendLine($" .WithBody(\"{EscapeCSharpString(bodyData.BodyAsString)}\")"); + sb.AppendLine($" .WithBody({ToCSharpStringLiteral(bodyData.BodyAsString)})"); break; case BodyType.Json: if (bodyData.BodyAsJson is string bodyStringValue) { - sb.AppendLine($" .WithBody(\"{EscapeCSharpString(bodyStringValue)}\")"); + sb.AppendLine($" .WithBody({ToCSharpStringLiteral(bodyStringValue)})"); } - else + else if(bodyData.BodyAsJson is {} jsonBody) { - var serializedBody = JsonConvert.SerializeObject(bodyData.BodyAsJson); - var deserializedBody = JToken.Parse(serializedBody); - sb.AppendLine($" .WithBodyAsJson({ConvertJsonToAnonymousObjectDefinition(deserializedBody, 2)})"); + var anonymousObjectDefinition = ConvertToAnonymousObjectDefinition(jsonBody); + sb.AppendLine($" .WithBodyAsJson({anonymousObjectDefinition})"); } break; @@ -406,7 +405,7 @@ internal class MappingConverter private static string GetString(IStringMatcher stringMatcher) { - return stringMatcher.GetPatterns().Select(p => $"\"{p.GetPattern()}\"").First(); + return stringMatcher.GetPatterns().Select(p => ToCSharpStringLiteral(p.GetPattern())).First(); } private static string[] GetStringArray(IReadOnlyList stringMatchers) @@ -459,11 +458,9 @@ internal class MappingConverter private static string ToValueArguments(string[]? values, string defaultValue = "") { - return values is { } ? string.Join(", ", values.Select(v => $"\"{EscapeCSharpString(v)}\"")) : $"\"{EscapeCSharpString(defaultValue)}\""; + return values is { } ? string.Join(", ", values.Select(ToCSharpStringLiteral)) : ToCSharpStringLiteral(defaultValue); } - private static string? EscapeCSharpString(string? value) => value?.Replace("\"", "\\\""); - private static WebProxyModel? MapWebProxy(WebProxySettings? settings) { return settings != null ? new WebProxyModel @@ -493,48 +490,5 @@ internal class MappingConverter return newDictionary; } - private static string ConvertJsonToAnonymousObjectDefinition(JToken token, int ind = 0) - { - return token switch - { - JArray jArray => FormatArray(jArray, ind), - JObject jObject => FormatObject(jObject, ind), - JProperty jProperty => $"{jProperty.Name} = {ConvertJsonToAnonymousObjectDefinition(jProperty.Value, ind)}", - JValue jValue => jValue.Type switch - { - JTokenType.None => "null", - JTokenType.Integer => jValue.Value?.ToString() ?? "null", - JTokenType.Float => jValue.Value?.ToString() ?? "null", - JTokenType.String => $"\"{EscapeCSharpString(jValue.Value?.ToString())}\"", - JTokenType.Boolean => jValue.Value?.ToString()?.ToLower() ?? "null", - JTokenType.Null => "null", - JTokenType.Undefined => "null", - _ => $"UNHANDLED_CASE: {jValue.Type}" - }, - _ => $"UNHANDLED_CASE: {token}" - }; - } - private static string FormatObject(JObject jObject, int ind) - { - var indStr = new string(' ', 4 * ind); - var indStrSub = new string(' ', 4 * (ind + 1)); - var items = jObject.Properties().Select(x => ConvertJsonToAnonymousObjectDefinition(x, ind + 1)); - - return $"new\r\n{indStr}{{\r\n{indStrSub}{string.Join($",\r\n{indStrSub}", items)}\r\n{indStr}}}"; - } - - private static string FormatArray(JArray jArray, int ind) - { - var hasComplexItems = jArray.FirstOrDefault() is JObject or JArray; - var items = jArray.Select(x => ConvertJsonToAnonymousObjectDefinition(x, hasComplexItems ? ind + 1 : ind)); - if (hasComplexItems) - { - var indStr = new string(' ', 4 * ind); - var indStrSub = new string(' ', 4 * (ind + 1)); - return $"new []\r\n{indStr}{{\r\n{indStrSub}{string.Join($",\r\n{indStrSub}", items)}\r\n{indStr}}}"; - } - - return $"new [] {{ {string.Join(", ", items)} }}"; - } } \ No newline at end of file diff --git a/src/WireMock.Net/Util/CSharpFormatter.cs b/src/WireMock.Net/Util/CSharpFormatter.cs new file mode 100644 index 00000000..66cf248f --- /dev/null +++ b/src/WireMock.Net/Util/CSharpFormatter.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace WireMock.Util; + +internal static class CSharpFormatter +{ + #region Reserved Keywords + private static readonly HashSet CSharpReservedKeywords = new(new[] + { + "abstract", + "as", + "base", + "bool", + "break", + "byte", + "case", + "catch", + "char", + "checked", + "class", + "const", + "continue", + "decimal", + "default", + "delegate", + "do", + "double", + "else", + "enum", + "event", + "explicit", + "extern", + "false", + "finally", + "fixed", + "float", + "for", + "foreach", + "goto", + "if", + "implicit", + "in", + "int", + "interface", + "internal", + "is", + "lock", + "long", + "namespace", + "new", + "null", + "object", + "operator", + "out", + "override", + "params", + "private", + "protected", + "public", + "readonly", + "ref", + "return", + "sbyte", + "sealed", + "short", + "sizeof", + "stackalloc", + "static", + "string", + "struct", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "uint", + "ulong", + "unchecked", + "unsafe", + "ushort", + "using", + "virtual", + "void", + "volatile", + "while" + }); + #endregion + private const string Null = "null"; + + public static object ConvertToAnonymousObjectDefinition(object jsonBody) + { + var serializedBody = JsonConvert.SerializeObject(jsonBody); + using var jsonReader = new JsonTextReader(new StringReader(serializedBody)); + jsonReader.DateParseHandling = DateParseHandling.None; + var deserializedBody = JObject.Load(jsonReader); + + return ConvertJsonToAnonymousObjectDefinition(deserializedBody, 2); + } + + private static string ConvertJsonToAnonymousObjectDefinition(JToken token, int ind = 0) + { + return token switch + { + JArray jArray => FormatArray(jArray, ind), + JObject jObject => FormatObject(jObject, ind), + JProperty jProperty => $"{FormatPropertyName(jProperty.Name)} = {ConvertJsonToAnonymousObjectDefinition(jProperty.Value, ind)}", + JValue jValue => jValue.Type switch + { + JTokenType.None => Null, + JTokenType.Integer => jValue.Value != null ? string.Format(CultureInfo.InvariantCulture, "{0}", jValue.Value) : Null, + JTokenType.Float => jValue.Value != null ? string.Format(CultureInfo.InvariantCulture, "{0}", jValue.Value) : Null, + JTokenType.String => ToCSharpStringLiteral(jValue.Value?.ToString()), + JTokenType.Boolean => jValue.Value != null ? string.Format(CultureInfo.InvariantCulture, "{0}", jValue.Value).ToLower() : Null, + JTokenType.Null => Null, + JTokenType.Undefined => Null, + JTokenType.Date when jValue.Value is DateTime dateValue => + $"DateTime.Parse({ToCSharpStringLiteral(dateValue.ToString("s"))})", + _ => $"UNHANDLED_CASE: {jValue.Type}" + }, + _ => $"UNHANDLED_CASE: {token}" + }; + } + + public static string ToCSharpStringLiteral(string? value) + { + var escapedValue = value?.Replace("\"", "\\\"") ?? string.Empty; + if (escapedValue.Contains("\n")) + { + return $"@\"{escapedValue}\""; + } + + return $"\"{escapedValue}\""; + } + + private static string FormatPropertyName(string propertyName) + { + return CSharpReservedKeywords.Contains(propertyName) ? "@" + propertyName : propertyName; + } + + private static string FormatObject(JObject jObject, int ind) + { + var indStr = new string(' ', 4 * ind); + var indStrSub = new string(' ', 4 * (ind + 1)); + var items = jObject.Properties().Select(x => ConvertJsonToAnonymousObjectDefinition(x, ind + 1)); + + return $"new\r\n{indStr}{{\r\n{indStrSub}{string.Join($",\r\n{indStrSub}", items)}\r\n{indStr}}}"; + } + + private static string FormatArray(JArray jArray, int ind) + { + var hasComplexItems = jArray.FirstOrDefault() is JObject or JArray; + var items = jArray.Select(x => ConvertJsonToAnonymousObjectDefinition(x, hasComplexItems ? ind + 1 : ind)); + if (hasComplexItems) + { + var indStr = new string(' ', 4 * ind); + var indStrSub = new string(' ', 4 * (ind + 1)); + return $"new []\r\n{indStr}{{\r\n{indStrSub}{string.Join($",\r\n{indStrSub}", items)}\r\n{indStr}}}"; + } + + return $"new [] {{ {string.Join(", ", items)} }}"; + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingsCode.verified.txt b/test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingsCode.verified.txt index 50764a68..777f41cd 100644 --- a/test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingsCode.verified.txt +++ b/test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingsCode.verified.txt @@ -36,7 +36,7 @@ server .WithStatusCode(208) .WithBodyAsJson(new { - a = 1, + @as = 1, b = 1.2, d = true, e = false, @@ -59,7 +59,14 @@ server b = 3 } } - } + }, + date_field = "2023-05-08T11:20:19", + string_field_with_date = "2021-03-13T21:04:00Z", + multiline_text = @"This +is +multiline +text +" }) ); diff --git a/test/WireMock.Net.Tests/WireMockAdminApiTests.cs b/test/WireMock.Net.Tests/WireMockAdminApiTests.cs index d8be86e4..8aeeb7cc 100644 --- a/test/WireMock.Net.Tests/WireMockAdminApiTests.cs +++ b/test/WireMock.Net.Tests/WireMockAdminApiTests.cs @@ -762,7 +762,11 @@ public class WireMockAdminApiTests .RespondWith( Response.Create() .WithStatusCode(HttpStatusCode.AlreadyReported) - .WithBodyAsJson(new { a = 1, b=1.2, d=true, e=false, f=new[]{1,2,3,4}, g= new{z1=1, z2=2, z3=new []{"a","b","c"}, z4=new[]{new {a=1, b=2},new {a=2, b=3}}} }) + .WithBodyAsJson(new { @as = 1, b=1.2, d=true, e=false, f=new[]{1,2,3,4}, g= new{z1=1, z2=2, z3=new []{"a","b","c"}, z4=new[]{new {a=1, b=2},new {a=2, b=3}}}, date_field = new DateTime(2023,05,08,11,20,19), string_field_with_date="2021-03-13T21:04:00Z", multiline_text= @"This +is +multiline +text +" }) ); // Act