diff --git a/src/WireMock.Net/Serialization/MatcherMapper.cs b/src/WireMock.Net/Serialization/MatcherMapper.cs
index ebf57430..8c6b703b 100644
--- a/src/WireMock.Net/Serialization/MatcherMapper.cs
+++ b/src/WireMock.Net/Serialization/MatcherMapper.cs
@@ -105,6 +105,11 @@ namespace WireMock.Serialization
return new SimMetricsMatcher(matchBehaviour, stringPatterns, type, throwExceptionWhenMatcherFails);
default:
+ if (_settings.CustomMatcherMappings != null && _settings.CustomMatcherMappings.ContainsKey(matcherName))
+ {
+ return _settings.CustomMatcherMappings[matcherName](matcher);
+ }
+
throw new NotSupportedException($"Matcher '{matcherName}' is not supported.");
}
}
diff --git a/src/WireMock.Net/Settings/IWireMockServerSettings.cs b/src/WireMock.Net/Settings/IWireMockServerSettings.cs
index 610496e3..43b66d51 100644
--- a/src/WireMock.Net/Settings/IWireMockServerSettings.cs
+++ b/src/WireMock.Net/Settings/IWireMockServerSettings.cs
@@ -1,7 +1,10 @@
using System;
+using System.Collections.Generic;
using System.Text.RegularExpressions;
using HandlebarsDotNet;
using JetBrains.Annotations;
+using Newtonsoft.Json;
+using WireMock.Admin.Mappings;
using WireMock.Handlers;
using WireMock.Logging;
using WireMock.Matchers;
@@ -229,5 +232,11 @@ namespace WireMock.Settings
///
[PublicAPI]
bool? SaveUnmatchedRequests { get; set; }
+
+ ///
+ /// Custom matcher mappings for static mappings
+ ///
+ [PublicAPI, JsonIgnore]
+ IDictionary> CustomMatcherMappings { get; set; }
}
}
\ No newline at end of file
diff --git a/src/WireMock.Net/Settings/WireMockServerSettings.cs b/src/WireMock.Net/Settings/WireMockServerSettings.cs
index ab55eb30..6bb1fa28 100644
--- a/src/WireMock.Net/Settings/WireMockServerSettings.cs
+++ b/src/WireMock.Net/Settings/WireMockServerSettings.cs
@@ -1,9 +1,12 @@
using System;
+using System.Collections.Generic;
using HandlebarsDotNet;
using JetBrains.Annotations;
using Newtonsoft.Json;
+using WireMock.Admin.Mappings;
using WireMock.Handlers;
using WireMock.Logging;
+using WireMock.Matchers;
#if USE_ASPNETCORE
using Microsoft.Extensions.DependencyInjection;
#endif
@@ -158,5 +161,9 @@ namespace WireMock.Settings
///
[PublicAPI]
public bool? SaveUnmatchedRequests { get; set; }
+
+ ///
+ [PublicAPI, JsonIgnore]
+ public IDictionary> CustomMatcherMappings { get; set; }
}
}
\ No newline at end of file
diff --git a/test/WireMock.Net.Tests/Serialization/CustomPathParamMatcher.cs b/test/WireMock.Net.Tests/Serialization/CustomPathParamMatcher.cs
new file mode 100644
index 00000000..27c3b7ae
--- /dev/null
+++ b/test/WireMock.Net.Tests/Serialization/CustomPathParamMatcher.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using AnyOfTypes;
+using Newtonsoft.Json;
+using WireMock.Matchers;
+using WireMock.Models;
+
+namespace WireMock.Net.Tests.Serialization
+{
+ ///
+ /// This matcher is only for unit test purposes
+ ///
+ public class CustomPathParamMatcher : IStringMatcher
+ {
+ public string Name => nameof(CustomPathParamMatcher);
+ public MatchBehaviour MatchBehaviour { get; }
+ public bool ThrowException { get; }
+
+ private readonly string _path;
+ private readonly string[] _pathParts;
+ private readonly Dictionary _pathParams;
+
+ public CustomPathParamMatcher(string path, Dictionary pathParams) : this(MatchBehaviour.AcceptOnMatch, path, pathParams)
+ {
+ }
+
+ public CustomPathParamMatcher(MatchBehaviour matchBehaviour, string path, Dictionary pathParams, bool throwException = false)
+ {
+ MatchBehaviour = matchBehaviour;
+ ThrowException = throwException;
+ _path = path;
+ _pathParts = GetPathParts(path);
+ _pathParams = pathParams.ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase);
+ }
+
+ public double IsMatch(string input)
+ {
+ var inputParts = GetPathParts(input);
+ if (inputParts.Length != _pathParts.Length)
+ {
+ return MatchScores.Mismatch;
+ }
+
+ try
+ {
+ for (int i = 0; i < inputParts.Length; i++)
+ {
+ var inputPart = inputParts[i];
+ var pathPart = _pathParts[i];
+ if (pathPart.StartsWith("{") && pathPart.EndsWith("}"))
+ {
+ var pathParamName = pathPart.Trim('{').Trim('}');
+ if (!_pathParams.ContainsKey(pathParamName))
+ {
+ return MatchScores.Mismatch;
+ }
+
+ if (!Regex.IsMatch(inputPart, _pathParams[pathParamName], RegexOptions.IgnoreCase))
+ {
+ return MatchScores.Mismatch;
+ }
+ }
+ else
+ {
+ if (!inputPart.Equals(pathPart, StringComparison.InvariantCultureIgnoreCase))
+ {
+ return MatchScores.Mismatch;
+ }
+ }
+ }
+ }
+ catch
+ {
+ if (ThrowException)
+ {
+ throw;
+ }
+
+ return MatchScores.Mismatch;
+ }
+
+ return MatchScores.Perfect;
+ }
+
+ public AnyOf[] GetPatterns()
+ {
+ return new[] { new AnyOf(JsonConvert.SerializeObject(new CustomPathParamMatcherModel(_path, _pathParams))) };
+ }
+
+ private string[] GetPathParts(string path)
+ {
+ var hashMarkIndex = path.IndexOf('#');
+ if (hashMarkIndex != -1)
+ {
+ path = path.Substring(0, hashMarkIndex);
+ }
+
+ var queryParamsIndex = path.IndexOf('?');
+ if (queryParamsIndex != -1)
+ {
+ path = path.Substring(0, queryParamsIndex);
+ }
+
+ return path.Trim().Trim('/').ToLower().Split('/');
+ }
+ }
+}
diff --git a/test/WireMock.Net.Tests/Serialization/CustomPathParamMatcherModel.cs b/test/WireMock.Net.Tests/Serialization/CustomPathParamMatcherModel.cs
new file mode 100644
index 00000000..253d4dcf
--- /dev/null
+++ b/test/WireMock.Net.Tests/Serialization/CustomPathParamMatcherModel.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+namespace WireMock.Net.Tests.Serialization
+{
+ public class CustomPathParamMatcherModel
+ {
+ public string Path { get; set; }
+ public Dictionary PathParams { get; set; }
+
+ public CustomPathParamMatcherModel()
+ {
+ }
+
+ public CustomPathParamMatcherModel(string path, Dictionary pathParams)
+ {
+ Path = path;
+ PathParams = pathParams;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WireMock.Net.Tests/Serialization/MatcherModelMapperTests.cs b/test/WireMock.Net.Tests/Serialization/MatcherModelMapperTests.cs
index cef96704..3746d782 100644
--- a/test/WireMock.Net.Tests/Serialization/MatcherModelMapperTests.cs
+++ b/test/WireMock.Net.Tests/Serialization/MatcherModelMapperTests.cs
@@ -1,7 +1,10 @@
using System;
+using System.Collections.Generic;
using AnyOfTypes;
using FluentAssertions;
+using FluentAssertions.Execution;
using Moq;
+using Newtonsoft.Json;
using NFluent;
using WireMock.Admin.Mappings;
using WireMock.Handlers;
@@ -310,5 +313,71 @@ namespace WireMock.Net.Tests.Serialization
// Act
Check.ThatCode(() => _sut.Map(model)).Throws();
}
+
+ [Fact]
+ public void MatcherModelMapper_Map_MatcherModelToCustomMatcher()
+ {
+ // Arrange
+ var patternModel = new CustomPathParamMatcherModel("/customer/{customerId}/document/{documentId}",
+ new Dictionary(2)
+ {
+ { "customerId", @"^[0-9]+$" },
+ { "documentId", @"^[0-9a-zA-Z\-\_]+\.[a-zA-Z]+$" }
+ });
+ var model = new MatcherModel
+ {
+ Name = nameof(CustomPathParamMatcher),
+ Pattern = JsonConvert.SerializeObject(patternModel)
+ };
+
+ var settings = new WireMockServerSettings();
+ settings.CustomMatcherMappings = settings.CustomMatcherMappings ?? new Dictionary>();
+ settings.CustomMatcherMappings[nameof(CustomPathParamMatcher)] = matcherModel =>
+ {
+ var matcherParams = JsonConvert.DeserializeObject((string)matcherModel.Pattern);
+ return new CustomPathParamMatcher(
+ matcherModel.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch,
+ matcherParams.Path, matcherParams.PathParams,
+ settings.ThrowExceptionWhenMatcherFails == true
+ );
+ };
+ var sut = new MatcherMapper(settings);
+
+ // Act
+ var matcher = sut.Map(model) as CustomPathParamMatcher;
+
+ // Assert
+ matcher.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void MatcherModelMapper_Map_CustomMatcherToMatcherModel()
+ {
+ // Arrange
+ var matcher = new CustomPathParamMatcher("/customer/{customerId}/document/{documentId}",
+ new Dictionary(2)
+ {
+ { "customerId", @"^[0-9]+$" },
+ { "documentId", @"^[0-9a-zA-Z\-\_]+\.[a-zA-Z]+$" }
+ });
+
+ // Act
+ var model = _sut.Map(matcher);
+
+ // Assert
+ using (new AssertionScope())
+ {
+ model.Should().NotBeNull();
+ model.Name.Should().Be(nameof(CustomPathParamMatcher));
+
+ var matcherParams = JsonConvert.DeserializeObject((string)model.Pattern);
+ matcherParams.Path.Should().Be("/customer/{customerId}/document/{documentId}");
+ matcherParams.PathParams.Should().BeEquivalentTo(new Dictionary(2)
+ {
+ { "customerId", @"^[0-9]+$" },
+ { "documentId", @"^[0-9a-zA-Z\-\_]+\.[a-zA-Z]+$" }
+ });
+ }
+ }
}
}
\ No newline at end of file
diff --git a/test/WireMock.Net.Tests/WireMockServerTests.cs b/test/WireMock.Net.Tests/WireMockServerTests.cs
index 7fc83479..380dd6c8 100644
--- a/test/WireMock.Net.Tests/WireMockServerTests.cs
+++ b/test/WireMock.Net.Tests/WireMockServerTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
@@ -7,10 +8,15 @@ using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
+using Newtonsoft.Json;
using NFluent;
+using WireMock.Admin.Mappings;
+using WireMock.Matchers;
+using WireMock.Net.Tests.Serialization;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
+using WireMock.Settings;
using WireMock.Util;
using Xunit;
@@ -383,6 +389,102 @@ namespace WireMock.Net.Tests
server.Stop();
}
+
+ [Fact]
+ public async Task WireMockServer_Using_JsonMapping_And_CustomMatcher_WithCorrectParams_ShouldMatch()
+ {
+ // Arrange
+ var settings = new WireMockServerSettings();
+ settings.WatchStaticMappings = true;
+ settings.WatchStaticMappingsInSubdirectories = true;
+ settings.CustomMatcherMappings = new Dictionary>();
+ settings.CustomMatcherMappings[nameof(CustomPathParamMatcher)] = matcherModel =>
+ {
+ var matcherParams = JsonConvert.DeserializeObject((string)matcherModel.Pattern);
+ return new CustomPathParamMatcher(
+ matcherModel.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch,
+ matcherParams.Path, matcherParams.PathParams,
+ settings.ThrowExceptionWhenMatcherFails == true
+ );
+ };
+
+ var server = WireMockServer.Start(settings);
+ server.WithMapping(@"{
+ ""Request"": {
+ ""Path"": {
+ ""Matchers"": [
+ {
+ ""Name"": ""CustomPathParamMatcher"",
+ ""Pattern"": ""{\""path\"":\""/customer/{customerId}/document/{documentId}\"",\""pathParams\"":{\""customerId\"":\""^[0-9]+$\"",\""documentId\"":\""^[0-9a-zA-Z\\\\-_]+\\\\.[a-zA-Z]+$\""}}""
+ }
+ ]
+ }
+ },
+ ""Response"": {
+ ""StatusCode"": 200,
+ ""Headers"": {
+ ""Content-Type"": ""application/json""
+ },
+ ""Body"": ""OK""
+ }
+}");
+
+ // Act
+ var response = await new HttpClient().PostAsync("http://localhost:" + server.Ports[0] + "/customer/132/document/pic.jpg", new StringContent("{ Hi = \"Hello World\" }")).ConfigureAwait(false);
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ server.Stop();
+ }
+
+ [Fact]
+ public async Task WireMockServer_Using_JsonMapping_And_CustomMatcher_WithIncorrectParams_ShouldNotMatch()
+ {
+ // Arrange
+ var settings = new WireMockServerSettings();
+ settings.WatchStaticMappings = true;
+ settings.WatchStaticMappingsInSubdirectories = true;
+ settings.CustomMatcherMappings = new Dictionary>();
+ settings.CustomMatcherMappings[nameof(CustomPathParamMatcher)] = matcherModel =>
+ {
+ var matcherParams = JsonConvert.DeserializeObject((string)matcherModel.Pattern);
+ return new CustomPathParamMatcher(
+ matcherModel.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch,
+ matcherParams.Path, matcherParams.PathParams,
+ settings.ThrowExceptionWhenMatcherFails == true
+ );
+ };
+
+ var server = WireMockServer.Start(settings);
+ server.WithMapping(@"{
+ ""Request"": {
+ ""Path"": {
+ ""Matchers"": [
+ {
+ ""Name"": ""CustomPathParamMatcher"",
+ ""Pattern"": ""{\""path\"":\""/customer/{customerId}/document/{documentId}\"",\""pathParams\"":{\""customerId\"":\""^[0-9]+$\"",\""documentId\"":\""^[0-9a-zA-Z\\\\-_]+\\\\.[a-zA-Z]+$\""}}""
+ }
+ ]
+ }
+ },
+ ""Response"": {
+ ""StatusCode"": 200,
+ ""Headers"": {
+ ""Content-Type"": ""application/json""
+ },
+ ""Body"": ""OK""
+ }
+}");
+
+ // Act
+ var response = await new HttpClient().PostAsync("http://localhost:" + server.Ports[0] + "/customer/132/document/pic", new StringContent("{ Hi = \"Hello World\" }")).ConfigureAwait(false);
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
+
+ server.Stop();
+ }
#endif
}
}
\ No newline at end of file