diff --git a/src/WireMock.Net/Transformers/HandleBarsHelpers.cs b/src/WireMock.Net/Transformers/HandleBarsHelpers.cs index 17078d1c..880aee8c 100644 --- a/src/WireMock.Net/Transformers/HandleBarsHelpers.cs +++ b/src/WireMock.Net/Transformers/HandleBarsHelpers.cs @@ -1,8 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Text.RegularExpressions; using HandlebarsDotNet; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using WireMock.Utils; using WireMock.Validation; namespace WireMock.Transformers @@ -11,9 +15,41 @@ namespace WireMock.Transformers { public static void Register() { + Handlebars.RegisterHelper("Regex.Match", (writer, context, arguments) => + { + (string stringToProcess, string regexPattern, object defaultValue) = ParseRegexArguments(arguments); + + Match match = Regex.Match(stringToProcess, regexPattern); + + if (match.Success) + { + writer.WriteSafeString(match.Value); + } + else if (defaultValue != null) + { + writer.WriteSafeString(defaultValue); + } + }); + + Handlebars.RegisterHelper("Regex.Match", (writer, options, context, arguments) => + { + (string stringToProcess, string regexPattern, object defaultValue) = ParseRegexArguments(arguments); + + var regex = new Regex(regexPattern); + var namedGroups = RegexUtils.GetNamedGroups(regex, stringToProcess); + if (namedGroups.Any()) + { + options.Template(writer, namedGroups); + } + else if (defaultValue != null) + { + writer.WriteSafeString(defaultValue); + } + }); + Handlebars.RegisterHelper("JsonPath.SelectToken", (writer, context, arguments) => { - (JObject valueToProcess, string jsonpath) = Parse(arguments); + (JObject valueToProcess, string jsonpath) = ParseJsonPathArguments(arguments); JToken result = null; try @@ -34,7 +70,7 @@ namespace WireMock.Transformers Handlebars.RegisterHelper("JsonPath.SelectTokens", (writer, options, context, arguments) => { - (JObject valueToProcess, string jsonpath) = Parse(arguments); + (JObject valueToProcess, string jsonpath) = ParseJsonPathArguments(arguments); IEnumerable values = null; try @@ -61,7 +97,7 @@ namespace WireMock.Transformers }); } - private static (JObject valueToProcess, string jsonpath) Parse(object[] arguments) + private static (JObject valueToProcess, string jsonpath) ParseJsonPathArguments(object[] arguments) { Check.Condition(arguments, args => args.Length == 2, nameof(arguments)); Check.NotNull(arguments[0], "arguments[0]"); @@ -80,10 +116,29 @@ namespace WireMock.Transformers break; default: - throw new NotSupportedException($"The value '{arguments[0]}' with type {arguments[0].GetType()} cannot be used in Handlebars JsonPath."); + throw new NotSupportedException($"The value '{arguments[0]}' with type '{arguments[0]?.GetType()}' cannot be used in Handlebars JsonPath."); } return (valueToProcess, arguments[1] as string); } + + private static (string stringToProcess, string regexPattern, object defaultValue) ParseRegexArguments(object[] arguments) + { + Check.Condition(arguments, args => args.Length >= 2, nameof(arguments)); + + string ParseAsString(object arg) + { + if (arg is string) + { + return arg as string; + } + else + { + throw new NotSupportedException($"The value '{arg}' with type '{arg?.GetType()}' cannot be used in Handlebars Regex."); + } + } + + return (ParseAsString(arguments[0]), ParseAsString(arguments[1]), arguments.Length == 3 ? arguments[2] : null); + } } } \ No newline at end of file diff --git a/src/WireMock.Net/Util/IndexableDictionary.cs b/src/WireMock.Net/Util/IndexableDictionary.cs new file mode 100644 index 00000000..af6b8fb0 --- /dev/null +++ b/src/WireMock.Net/Util/IndexableDictionary.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; + +namespace WireMock.Util +{ + public class IndexableDictionary : Dictionary + { + /// + /// Gets the value associated with the specified index. + /// + /// The index of the value to get. + /// The value associated with the specified index. + public TValue this[int index] + { + get + { + // get the item for that index. + if (index < 0 || index > Count) + { + throw new KeyNotFoundException(); + } + return Values.Cast().ToArray()[index]; + } + } + } +} diff --git a/src/WireMock.Net/Util/RegexUtils.cs b/src/WireMock.Net/Util/RegexUtils.cs new file mode 100644 index 00000000..1d567238 --- /dev/null +++ b/src/WireMock.Net/Util/RegexUtils.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace WireMock.Utils +{ + internal static class RegexUtils + { + public static Dictionary GetNamedGroups(Regex regex, string input) + { + var namedGroupsDictionary = new Dictionary(); + + GroupCollection groups = regex.Match(input).Groups; + foreach (string groupName in regex.GetGroupNames()) + { + if (groups[groupName].Captures.Count > 0) + { + namedGroupsDictionary.Add(groupName, groups[groupName].Value); + } + } + + return namedGroupsDictionary; + } + } +} diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index dddadb98..f8401b3d 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -37,6 +37,7 @@ + diff --git a/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsTests.cs b/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsTests.cs index 98497e62..09db6ff1 100644 --- a/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsTests.cs +++ b/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsTests.cs @@ -494,6 +494,136 @@ namespace WireMock.Net.Tests.ResponseBuilderTests Check.ThatAsyncCode(() => response.ProvideResponseAsync(request)).Throws(); } + [Fact] + public async void Response_ProvideResponse_Handlebars_RegexMatch1() + { + // Assign + var body = new BodyData { BodyAsString = "abc" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithBody("{{Regex.Match request.body \"^(?\\w+)$\"}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // assert + Check.That(responseMessage.Body).Equals("abc"); + } + + [Fact] + public async void Response_ProvideResponse_Handlebars_RegexMatch1_NoMatch() + { + // Assign + var body = new BodyData { BodyAsString = "abc" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithBody("{{Regex.Match request.body \"^?0$\"}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // assert + Check.That(responseMessage.Body).Equals(""); + } + + [Fact] + public async void Response_ProvideResponse_Handlebars_RegexMatch1_NoMatch_WithDefaultValue() + { + // Assign + var body = new BodyData { BodyAsString = "abc" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithBody("{{Regex.Match request.body \"^?0$\" \"d\"}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // assert + Check.That(responseMessage.Body).Equals("d"); + } + + [Fact] + public async void Response_ProvideResponse_Handlebars_RegexMatch2() + { + // Assign + var body = new BodyData { BodyAsString = "https://localhost:5000/" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithBody("{{#Regex.Match request.body \"^(?\\w+)://[^/]+?(?\\d+)/?\"}}{{this.port}}-{{this.proto}}{{/Regex.Match}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // assert + Check.That(responseMessage.Body).Equals("5000-https"); + } + + [Fact] + public async void Response_ProvideResponse_Handlebars_RegexMatch2_NoMatch() + { + // Assign + var body = new BodyData { BodyAsString = "{{\\test" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithBody("{{#Regex.Match request.body \"^(?\\w+)://[^/]+?(?\\d+)/?\"}}{{this}}{{/Regex.Match}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // assert + Check.That(responseMessage.Body).Equals(""); + } + + [Fact] + public async void Response_ProvideResponse_Handlebars_RegexMatch2_NoMatch_WithDefaultValue() + { + // Assign + var body = new BodyData { BodyAsString = "{{\\test" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithBody("{{#Regex.Match request.body \"^(?\\w+)://[^/]+?(?\\d+)/?\" \"x\"}}{{this}}{{/Regex.Match}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // assert + Check.That(responseMessage.Body).Equals("x"); + } + + [Fact] + public void Response_ProvideResponse_Handlebars_RegexMatch_Throws() + { + // Assign + var body = new BodyData { BodyAsString = "{{\\test" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithBody("{{#Regex.Match request.bodyAsJson \"^(?\\w+)://[^/]+?(?\\d+)/?\"}}{{/Regex.Match}}") + .WithTransformer(); + + // Act and Assert + Check.ThatAsyncCode(() => response.ProvideResponseAsync(request)).Throws(); + } + [Fact] public async Task Response_ProvideResponse_Handlebars_WithBodyAsJson_ResultAsArray() {