diff --git a/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs b/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs index 5b4076db..b0f8bf41 100644 --- a/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs +++ b/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Net; using Newtonsoft.Json; using WireMock.Logging; @@ -398,7 +399,23 @@ namespace WireMock.Net.ConsoleApplication server .Given(Request.Create().WithPath("/random")) .RespondWith(Response.Create() - .WithBody("Text:{{Random Type=\"Text\" Min=8 Max=20}}\r\nDateTime:{{Random Type=\"DateTime\"}}\r\nGuid:{{Random Type=\"Guid\" Uppercase=true}}") + .WithHeader("Content-Type", "application/json") + .WithBodyAsJson(new + { + Text = "{{Random Type=\"Text\" Min=8 Max=20}}", + TextLipsum = "{{Random Type=\"TextLipsum\"}}", + TimeSpan1 = "{{Random Type=\"TimeSpan\" Format=\"c\" IncludeMilliseconds=false}}", + TimeSpan2 = "{{Random Type=\"TimeSpan\"}}", + DateTime1 = "{{Random Type=\"DateTime\"}}", + DateTime2 = DateTime.Now, + DateTime3 = DateTime.Now.ToString("s", CultureInfo.InvariantCulture), + Guid1 = "{{Random Type=\"Guid\" Uppercase=false}}", + Guid2 = "{{Random Type=\"Guid\"}}", + Integer1 = "{{Random Type=\"Integer\" Min=1000 Max=9999}}", + Integer2 = "{{#Random Type=\"Integer\" Min=10000000 Max=99999999}}{{this}}{{/Random}}", + Double1 = "{{Random Type=\"Double\" Min=10 Max=99}}", + Double2 = "{{Random Type=\"Double\" Min=100 Max=999}}" + }) .WithTransformer() ); diff --git a/src/WireMock.Net/Transformers/HandleBarsHelpers.cs b/src/WireMock.Net/Transformers/HandleBarsHelpers.cs index f35142ae..3726d5f9 100644 --- a/src/WireMock.Net/Transformers/HandleBarsHelpers.cs +++ b/src/WireMock.Net/Transformers/HandleBarsHelpers.cs @@ -1,16 +1,18 @@ -namespace WireMock.Transformers +using HandlebarsDotNet; + +namespace WireMock.Transformers { internal static class HandlebarsHelpers { - public static void Register() + public static void Register(IHandlebars handlebarsContext) { - HandleBarsRegex.Register(); + HandleBarsRegex.Register(handlebarsContext); - HandleBarsJsonPath.Register(); + HandleBarsJsonPath.Register(handlebarsContext); - HandleBarsLinq.Register(); + HandleBarsLinq.Register(handlebarsContext); - HandleBarsRandom.Register(); + HandleBarsRandom.Register(handlebarsContext); } } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/HandleBarsJsonPath.cs b/src/WireMock.Net/Transformers/HandleBarsJsonPath.cs index 0ba42401..d7eacadc 100644 --- a/src/WireMock.Net/Transformers/HandleBarsJsonPath.cs +++ b/src/WireMock.Net/Transformers/HandleBarsJsonPath.cs @@ -9,15 +9,15 @@ namespace WireMock.Transformers { internal static class HandleBarsJsonPath { - public static void Register() + public static void Register(IHandlebars handlebarsContext) { - Handlebars.RegisterHelper("JsonPath.SelectToken", (writer, context, arguments) => + handlebarsContext.RegisterHelper("JsonPath.SelectToken", (writer, context, arguments) => { - (JObject valueToProcess, string jsonpath) = ParseArguments(arguments); + (JObject valueToProcess, string jsonPath) = ParseArguments(arguments); try { - var result = valueToProcess.SelectToken(jsonpath); + var result = valueToProcess.SelectToken(jsonPath); writer.WriteSafeString(result); } catch (JsonException) @@ -26,13 +26,13 @@ namespace WireMock.Transformers } }); - Handlebars.RegisterHelper("JsonPath.SelectTokens", (writer, options, context, arguments) => + handlebarsContext.RegisterHelper("JsonPath.SelectTokens", (writer, options, context, arguments) => { - (JObject valueToProcess, string jsonpath) = ParseArguments(arguments); + (JObject valueToProcess, string jsonPath) = ParseArguments(arguments); try { - var values = valueToProcess.SelectTokens(jsonpath); + var values = valueToProcess.SelectTokens(jsonPath); if (values != null) { options.Template(writer, values.ToDictionary(value => value.Path, value => value)); diff --git a/src/WireMock.Net/Transformers/HandleBarsLinq.cs b/src/WireMock.Net/Transformers/HandleBarsLinq.cs index b11f56d6..2eb43a82 100644 --- a/src/WireMock.Net/Transformers/HandleBarsLinq.cs +++ b/src/WireMock.Net/Transformers/HandleBarsLinq.cs @@ -11,9 +11,9 @@ namespace WireMock.Transformers { internal static class HandleBarsLinq { - public static void Register() + public static void Register(IHandlebars handlebarsContext) { - Handlebars.RegisterHelper("Linq", (writer, context, arguments) => + handlebarsContext.RegisterHelper("Linq", (writer, context, arguments) => { (JToken valueToProcess, string linqStatement) = ParseArguments(arguments); @@ -28,7 +28,7 @@ namespace WireMock.Transformers } }); - Handlebars.RegisterHelper("Linq", (writer, options, context, arguments) => + handlebarsContext.RegisterHelper("Linq", (writer, options, context, arguments) => { (JToken valueToProcess, string linqStatement) = ParseArguments(arguments); diff --git a/src/WireMock.Net/Transformers/HandleBarsRandom.cs b/src/WireMock.Net/Transformers/HandleBarsRandom.cs index b516d54f..1f6a89f8 100644 --- a/src/WireMock.Net/Transformers/HandleBarsRandom.cs +++ b/src/WireMock.Net/Transformers/HandleBarsRandom.cs @@ -1,5 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; using HandlebarsDotNet; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using RandomDataGenerator.FieldOptions; using RandomDataGenerator.Randomizers; using WireMock.Validation; @@ -8,24 +13,40 @@ namespace WireMock.Transformers { internal static class HandleBarsRandom { - public static void Register() + public static void Register(IHandlebars handlebarsContext) { - Handlebars.RegisterHelper("Random", (writer, context, arguments) => + handlebarsContext.RegisterHelper("Random", (writer, context, arguments) => { - var fieldOptions = GetFieldOptionsFromArguments(arguments); - - dynamic randomizer = RandomizerFactory.GetRandomizerAsDynamic(fieldOptions); - if (randomizer.GetType().GetMethod("GenerateAsString") != null) - { - string valueAsString = randomizer.GenerateAsString(); - writer.WriteSafeString(valueAsString); - } - else - { - object valueAsObject = randomizer.Generate(); - writer.WriteSafeString(valueAsObject); - } + object value = GetRandomValue(arguments); + writer.Write(value); }); + + handlebarsContext.RegisterHelper("Random", (writer, options, context, arguments) => + { + object value = GetRandomValue(arguments); + options.Template(writer, value); + }); + } + + private static object GetRandomValue(object[] arguments) + { + var fieldOptions = GetFieldOptionsFromArguments(arguments); + dynamic randomizer = RandomizerFactory.GetRandomizerAsDynamic(fieldOptions); + + // Format DateTime as ISO 8601 + if (fieldOptions is IFieldOptionsDateTime) + { + DateTime? date = randomizer.Generate(); + return date?.ToString("s", CultureInfo.InvariantCulture); + } + + // If the IFieldOptionsGuid defines Uppercase, use the 'GenerateAsString' method. + if (fieldOptions is IFieldOptionsGuid fieldOptionsGuid) + { + return fieldOptionsGuid.Uppercase ? randomizer.GenerateAsString() : randomizer.Generate(); + } + + return randomizer.Generate(); } private static FieldOptionsAbstract GetFieldOptionsFromArguments(object[] arguments) @@ -34,11 +55,59 @@ namespace WireMock.Transformers Check.NotNull(arguments[0], "arguments[0]"); var properties = (Dictionary)arguments[0]; - string type = (string)properties["Type"]; + var newProperties = new Dictionary(); - properties.Remove("Type"); + foreach (KeyValuePair property in properties.Where(p => p.Key != "Type")) + { + if (property.Value.GetType().Name == "UndefinedBindingResult") + { + if (TryParseSpecialValue(property.Value, out object parsedValue)) + { + newProperties.Add(property.Key, parsedValue); + } + } + else + { + newProperties.Add(property.Key, property.Value); + } + } - return FieldOptionsFactory.GetFieldOptions(type, properties); + return FieldOptionsFactory.GetFieldOptions((string)properties["Type"], newProperties); + } + + /// + /// In case it's an UndefinedBindingResult, just try to convert the value using Json + /// This logic adds functionality like parsing an array + /// + /// The property value + /// The parsed value + /// true in case parsing is ok, else false + private static bool TryParseSpecialValue(object value, out object parsedValue) + { + parsedValue = null; + string propertyValueAsString = value.ToString(); + + try + { + JToken jToken = JToken.Parse(propertyValueAsString); + switch (jToken) + { + case JArray jTokenArray: + parsedValue = jTokenArray.ToObject().ToList(); // Just convert to a String List to enable Random StringList + break; + + default: + return jToken.ToObject(); + } + + return true; + } + catch (JsonException) + { + // Ignore and don't add this value + } + + return false; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/HandleBarsRegex.cs b/src/WireMock.Net/Transformers/HandleBarsRegex.cs index 7a0ca278..a22d5554 100644 --- a/src/WireMock.Net/Transformers/HandleBarsRegex.cs +++ b/src/WireMock.Net/Transformers/HandleBarsRegex.cs @@ -9,9 +9,9 @@ namespace WireMock.Transformers { internal static class HandleBarsRegex { - public static void Register() + public static void Register(IHandlebars handlebarsContext) { - Handlebars.RegisterHelper("Regex.Match", (writer, context, arguments) => + handlebarsContext.RegisterHelper("Regex.Match", (writer, context, arguments) => { (string stringToProcess, string regexPattern, object defaultValue) = ParseArguments(arguments); @@ -27,7 +27,7 @@ namespace WireMock.Transformers } }); - Handlebars.RegisterHelper("Regex.Match", (writer, options, context, arguments) => + handlebarsContext.RegisterHelper("Regex.Match", (writer, options, context, arguments) => { (string stringToProcess, string regexPattern, object defaultValue) = ParseArguments(arguments); diff --git a/src/WireMock.Net/Transformers/ResponseMessageTransformer.cs b/src/WireMock.Net/Transformers/ResponseMessageTransformer.cs index 2a8842a5..00cd809d 100644 --- a/src/WireMock.Net/Transformers/ResponseMessageTransformer.cs +++ b/src/WireMock.Net/Transformers/ResponseMessageTransformer.cs @@ -10,9 +10,16 @@ namespace WireMock.Transformers { internal static class ResponseMessageTransformer { + private static readonly HandlebarsConfiguration HandlebarsConfiguration = new HandlebarsConfiguration + { + UnresolvedBindingFormatter = "{0}" + }; + + private static readonly IHandlebars HandlebarsContext = Handlebars.Create(HandlebarsConfiguration); + static ResponseMessageTransformer() { - HandlebarsHelpers.Register(); + HandlebarsHelpers.Register(HandlebarsContext); } public static ResponseMessage Transform(RequestMessage requestMessage, ResponseMessage original) @@ -40,9 +47,9 @@ namespace WireMock.Transformers var newHeaders = new Dictionary>(); foreach (var header in original.Headers) { - var templateHeaderKey = Handlebars.Compile(header.Key); + var templateHeaderKey = HandlebarsContext.Compile(header.Key); var templateHeaderValues = header.Value - .Select(Handlebars.Compile) + .Select(HandlebarsContext.Compile) .Select(func => func(template)) .ToArray(); @@ -109,7 +116,7 @@ namespace WireMock.Transformers return; } - var templateForStringValue = Handlebars.Compile(stringValue); + var templateForStringValue = HandlebarsContext.Compile(stringValue); string transformedString = templateForStringValue(template); if (!string.Equals(stringValue, transformedString)) { @@ -132,7 +139,7 @@ namespace WireMock.Transformers private static void TransformBodyAsString(object template, ResponseMessage original, ResponseMessage responseMessage) { - var templateBody = Handlebars.Compile(original.BodyData.BodyAsString); + var templateBody = HandlebarsContext.Compile(original.BodyData.BodyAsString); responseMessage.BodyData = new BodyData { diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index cd586170..34480a0e 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -53,7 +53,7 @@ - + diff --git a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsRandomTests.cs b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsRandomTests.cs index bfe54ebd..3aca51fc 100644 --- a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsRandomTests.cs +++ b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsRandomTests.cs @@ -1,4 +1,7 @@ -using System.Threading.Tasks; +using System; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; using NFluent; using WireMock.Models; using WireMock.ResponseBuilders; @@ -11,7 +14,7 @@ namespace WireMock.Net.Tests.ResponseBuilders private const string ClientIp = "::1"; [Fact] - public async Task Response_ProvideResponseAsync_Handlebars_Random() + public async Task Response_ProvideResponseAsync_Handlebars_Random1() { // Assign var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "GET", ClientIp); @@ -21,15 +24,87 @@ namespace WireMock.Net.Tests.ResponseBuilders { Text = "{{Random Type=\"Text\" Min=8 Max=20}}", DateTime = "{{Random Type=\"DateTime\"}}", - Guid = "{{Random Type=\"Guid\" Uppercase=true}}" + Integer = "{{Random Type=\"Integer\" Min=1000 Max=1000}}", + Long = "{{Random Type=\"Long\" Min=77777777 Max=99999999}}" }) .WithTransformer(); // Act var responseMessage = await response.ProvideResponseAsync(request); - // assert - Check.That(responseMessage.BodyData).IsNotNull(); + // Assert + JObject j = JObject.FromObject(responseMessage.BodyData.BodyAsJson); + Check.That(j["Text"]).IsNotNull(); + Check.That(j["Integer"].Value()).IsEqualTo(1000); + Check.That(j["Long"].Value()).IsStrictlyGreaterThan(77777777).And.IsStrictlyLessThan(99999999); + } + + [Fact] + public async Task Response_ProvideResponseAsync_Handlebars_Random1_Guid() + { + // Assign + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "GET", ClientIp); + + var response = Response.Create() + .WithBodyAsJson(new + { + Guid1 = "{{Random Type=\"Guid\" Uppercase=false}}", + Guid2 = "{{Random Type=\"Guid\"}}" + }) + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + JObject j = JObject.FromObject(responseMessage.BodyData.BodyAsJson); + string guid1 = j["Guid1"].Value(); + Check.That(guid1.ToUpper()).IsNotEqualTo(guid1); + string guid2 = j["Guid2"].Value(); + Check.That(guid2.ToUpper()).IsEqualTo(guid2); + } + + [Fact] + public async Task Response_ProvideResponseAsync_Handlebars_Random1_StringList() + { + // Assign + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "GET", ClientIp); + + var response = Response.Create() + .WithBodyAsJson(new + { + StringValue = "{{Random Type=\"StringList\" Values=[\"a\", \"b\", \"c\"]}}" + }) + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + JObject j = JObject.FromObject(responseMessage.BodyData.BodyAsJson); + string value = j["StringValue"].Value(); + Check.That(new[] { "a", "b", "c" }.Contains(value)).IsTrue(); + } + + [Fact] + public async Task Response_ProvideResponseAsync_Handlebars_Random2() + { + // Assign + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "GET", ClientIp); + + var response = Response.Create() + .WithBodyAsJson(new + { + Integer = "{{#Random Type=\"Integer\" Min=10000000 Max=99999999}}{{this}}{{/Random}}", + }) + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + JObject j = JObject.FromObject(responseMessage.BodyData.BodyAsJson); + Check.That(j["Integer"].Value()).IsStrictlyGreaterThan(10000000).And.IsStrictlyLessThan(99999999); } } } \ No newline at end of file