Swagger support (#749)

* r

* fix

* sw

* x

* s

* .

* .

* .

* CreateTypeFromJObject

* .

* .

* f

* c

* .

* .

* .

* .

* .

* .

* ok

* ,

* .

* .

* .

* .

* n

* pact

* fix

* schema

* null

* fluent

* r

* -p

* .

* .

* refs

* .
This commit is contained in:
Stef Heyenrath
2022-05-13 22:01:46 +02:00
committed by GitHub
parent 0d8b3b1438
commit 5e301fd74b
45 changed files with 2371 additions and 1123 deletions

View File

@@ -1,4 +1,4 @@
namespace WireMock.Admin.Mappings
namespace WireMock.Admin.Mappings
{
/// <summary>
/// Body Model
@@ -9,11 +9,11 @@
/// <summary>
/// Gets or sets the matcher.
/// </summary>
public MatcherModel Matcher { get; set; }
public MatcherModel? Matcher { get; set; }
/// <summary>
/// Gets or sets the matchers.
/// </summary>
public MatcherModel[] Matchers { get; set; }
public MatcherModel[]? Matchers { get; set; }
}
}

View File

@@ -1,30 +1,31 @@
using System.Collections.Generic;
namespace WireMock.Admin.Mappings;
/// <summary>
/// Cookie Model
/// </summary>
[FluentBuilder.AutoGenerateBuilder]
public class CookieModel
namespace WireMock.Admin.Mappings
{
/// <summary>
/// Gets or sets the name.
/// Cookie Model
/// </summary>
public string Name { get; set; } = null!;
[FluentBuilder.AutoGenerateBuilder]
public class CookieModel
{
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// Gets or sets the matchers.
/// </summary>
public IList<MatcherModel>? Matchers { get; set; }
/// <summary>
/// Gets or sets the matchers.
/// </summary>
public IList<MatcherModel>? Matchers { get; set; }
/// <summary>
/// Gets or sets the ignore case.
/// </summary>
public bool? IgnoreCase { get; set; }
/// <summary>
/// Gets or sets the ignore case.
/// </summary>
public bool? IgnoreCase { get; set; }
/// <summary>
/// Reject on match.
/// </summary>
public bool? RejectOnMatch { get; set; }
/// <summary>
/// Reject on match.
/// </summary>
public bool? RejectOnMatch { get; set; }
}
}

View File

@@ -14,17 +14,17 @@ namespace WireMock.Admin.Mappings
/// <summary>
/// Gets or sets the pattern. Can be a string (default) or an object.
/// </summary>
public object Pattern { get; set; }
public object? Pattern { get; set; }
/// <summary>
/// Gets or sets the patterns. Can be array of strings (default) or an array of objects.
/// </summary>
public object[] Patterns { get; set; }
public object[]? Patterns { get; set; }
/// <summary>
/// Gets or sets the pattern as a file.
/// </summary>
public string PatternAsFile { get; set; }
public string? PatternAsFile { get; set; }
/// <summary>
/// Gets or sets the ignore case.
@@ -36,4 +36,4 @@ namespace WireMock.Admin.Mappings
/// </summary>
public bool? RejectOnMatch { get; set; }
}
}
}

View File

@@ -1,4 +1,4 @@
namespace WireMock.Admin.Mappings
namespace WireMock.Admin.Mappings
{
/// <summary>
/// PathModel
@@ -9,6 +9,6 @@
/// <summary>
/// Gets or sets the matchers.
/// </summary>
public MatcherModel[] Matchers { get; set; }
public MatcherModel[]? Matchers { get; set; }
}
}

View File

@@ -0,0 +1,29 @@
using System.Linq;
using WireMock.Admin.Mappings;
namespace WireMock.Extensions;
public static class RequestModelExtensions
{
public static string? GetPathAsString(this RequestModel request)
{
var path = request.Path switch
{
string pathAsString => pathAsString,
PathModel pathModel => pathModel.Matchers?.FirstOrDefault()?.Pattern as string,
_ => null
};
return FixPath(path);
}
private static string? FixPath(string? path)
{
if (string.IsNullOrEmpty(path))
{
return path;
}
return path!.StartsWith("/") ? path : $"/{path}";
}
}

View File

@@ -0,0 +1,20 @@
using WireMock.Admin.Mappings;
namespace WireMock.Extensions;
public static class ResponseModelExtensions
{
private const string DefaultStatusCode = "200";
public static string GetStatusCodeAsString(this ResponseModel response)
{
return response.StatusCode switch
{
string statusCodeAsString => statusCodeAsString,
int statusCodeAsInt => statusCodeAsInt.ToString(),
_ => response.StatusCode?.ToString() ?? DefaultStatusCode
};
}
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using WireMock.ResponseBuilders;
using WireMock.Types;
using WireMock.Util;
@@ -13,7 +13,7 @@ namespace WireMock
/// <summary>
/// The Body.
/// </summary>
IBodyData BodyData { get; }
IBodyData? BodyData { get; }
/// <summary>
/// Gets the body destination (SameAsSource, String or Bytes).

View File

@@ -1,130 +1,132 @@
#pragma warning disable CS1591
using System.Linq;
using FluentAssertions;
using FluentAssertions.Execution;
using WireMock.Server;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions
namespace WireMock.FluentAssertions;
public class WireMockAssertions
{
public class WireMockAssertions
private readonly IWireMockServer _subject;
private readonly int? _callsCount;
public WireMockAssertions(IWireMockServer subject, int? callsCount)
{
private readonly IWireMockServer _subject;
_subject = subject;
_callsCount = callsCount;
}
public WireMockAssertions(IWireMockServer subject, int? callsCount)
[CustomAssertion]
public AndConstraint<WireMockAssertions> AtAbsoluteUrl(string absoluteUrl, string because = "", params object[] becauseArgs)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => _subject.LogEntries.Select(x => x.RequestMessage).ToList())
.ForCondition(requests => requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but no calls were made.",
absoluteUrl)
.Then
.ForCondition(x => _callsCount == null && x.Any(y => y.AbsoluteUrl == absoluteUrl) || _callsCount == x.Count(y => y.AbsoluteUrl == absoluteUrl))
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but didn't find it among the calls to {1}.",
_ => absoluteUrl, requests => requests.Select(request => request.AbsoluteUrl));
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> AtUrl(string url, string because = "", params object[] becauseArgs)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => _subject.LogEntries.Select(x => x.RequestMessage).ToList())
.ForCondition(requests => requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the url {0}{reason}, but no calls were made.",
url)
.Then
.ForCondition(x => _callsCount == null && x.Any(y => y.Url == url) || _callsCount == x.Count(y => y.Url == url))
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the url {0}{reason}, but didn't find it among the calls to {1}.",
_ => url, requests => requests.Select(request => request.Url));
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithProxyUrl(string proxyUrl, string because = "", params object[] becauseArgs)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => _subject.LogEntries.Select(x => x.RequestMessage).ToList())
.ForCondition(requests => requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called with proxy url {0}{reason}, but no calls were made.",
proxyUrl)
.Then
.ForCondition(x => _callsCount == null && x.Any(y => y.ProxyUrl == proxyUrl) || _callsCount == x.Count(y => y.ProxyUrl == proxyUrl))
.FailWith(
"Expected {context:wiremockserver} to have been called with proxy url {0}{reason}, but didn't find it among the calls with {1}.",
_ => proxyUrl, requests => requests.Select(request => request.ProxyUrl));
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> FromClientIP(string clientIP, string because = "", params object[] becauseArgs)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => _subject.LogEntries.Select(x => x.RequestMessage).ToList())
.ForCondition(requests => requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called from client IP {0}{reason}, but no calls were made.",
clientIP)
.Then
.ForCondition(x => _callsCount == null && x.Any(y => y.ClientIP == clientIP) || _callsCount == x.Count(y => y.ClientIP == clientIP))
.FailWith(
"Expected {context:wiremockserver} to have been called from client IP {0}{reason}, but didn't find it among the calls from IP(s) {1}.",
_ => clientIP, requests => requests.Select(request => request.ClientIP));
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithHeader(string expectedKey, string value, string because = "", params object[] becauseArgs)
=> WithHeader(expectedKey, new[] { value }, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithHeader(string expectedKey, string[] expectedValues, string because = "", params object[] becauseArgs)
{
var headersDictionary = _subject.LogEntries.SelectMany(x => x.RequestMessage.Headers)
.ToDictionary(x => x.Key, x => x.Value);
using (new AssertionScope("headers from requests sent"))
{
_subject = subject;
headersDictionary.Should().ContainKey(expectedKey, because, becauseArgs);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> AtAbsoluteUrl(string absoluteUrl, string because = "", params object[] becauseArgs)
using (new AssertionScope($"header \"{expectedKey}\" from requests sent with value(s)"))
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => _subject.LogEntries.Select(x => x.RequestMessage).ToList())
.ForCondition(requests => requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but no calls were made.",
absoluteUrl)
.Then
.ForCondition(x => x.Any(y => y.AbsoluteUrl == absoluteUrl))
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but didn't find it among the calls to {1}.",
_ => absoluteUrl, requests => requests.Select(request => request.AbsoluteUrl));
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithHeader(string expectedKey, string value, string because = "", params object[] becauseArgs)
=> WithHeader(expectedKey, new[] { value }, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithHeader(string expectedKey, string[] expectedValues, string because = "", params object[] becauseArgs)
{
var headersDictionary = _subject.LogEntries.SelectMany(x => x.RequestMessage.Headers)
.ToDictionary(x => x.Key, x => x.Value);
using (new AssertionScope("headers from requests sent"))
if (expectedValues.Length == 1)
{
headersDictionary.Should().ContainKey(expectedKey, because, becauseArgs);
headersDictionary[expectedKey].Should().Contain(expectedValues.First());
}
using (new AssertionScope($"header \"{expectedKey}\" from requests sent with value(s)"))
else
{
if (expectedValues.Length == 1)
var trimmedHeaderValues = string.Join(",", headersDictionary[expectedKey].Select(x => x)).Split(',')
.Select(x => x.Trim())
.ToList();
foreach (var expectedValue in expectedValues)
{
headersDictionary[expectedKey].Should().Contain(expectedValues.First());
}
else
{
var trimmedHeaderValues = string.Join(",", headersDictionary[expectedKey].Select(x => x)).Split(',')
.Select(x => x.Trim())
.ToList();
foreach (var expectedValue in expectedValues)
{
trimmedHeaderValues.Should().Contain(expectedValue);
}
trimmedHeaderValues.Should().Contain(expectedValue);
}
}
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> AtUrl(string url, string because = "", params object[] becauseArgs)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => _subject.LogEntries.Select(x => x.RequestMessage).ToList())
.ForCondition(requests => requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the url {0}{reason}, but no calls were made.",
url)
.Then
.ForCondition(x => x.Any(y => y.Url == url))
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the url {0}{reason}, but didn't find it among the calls to {1}.",
_ => url, requests => requests.Select(request => request.Url));
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithProxyUrl(string proxyUrl, string because = "", params object[] becauseArgs)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => _subject.LogEntries.Select(x => x.RequestMessage).ToList())
.ForCondition(requests => requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called with proxy url {0}{reason}, but no calls were made.",
proxyUrl)
.Then
.ForCondition(x => x.Any(y => y.ProxyUrl == proxyUrl))
.FailWith(
"Expected {context:wiremockserver} to have been called with proxy url {0}{reason}, but didn't find it among the calls with {1}.",
_ => proxyUrl, requests => requests.Select(request => request.ProxyUrl));
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> FromClientIP(string clientIP, string because = "", params object[] becauseArgs)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => _subject.LogEntries.Select(x => x.RequestMessage).ToList())
.ForCondition(requests => requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called from client IP {0}{reason}, but no calls were made.",
clientIP)
.Then
.ForCondition(x => x.Any(y => y.ClientIP == clientIP))
.FailWith(
"Expected {context:wiremockserver} to have been called from client IP {0}{reason}, but didn't find it among the calls from IP(s) {1}.",
_ => clientIP, requests => requests.Select(request => request.ClientIP));
return new AndConstraint<WireMockAssertions>(this);
}
return new AndConstraint<WireMockAssertions>(this);
}
}

View File

@@ -17,6 +17,15 @@ namespace WireMock.FluentAssertions
{
}
/// <summary>
/// Asserts if <see cref="IWireMockServer"/> has received no calls.
/// </summary>
/// <returns><see cref="WireMockAssertions"/></returns>
public WireMockAssertions HaveReceivedNoCalls()
{
return new WireMockAssertions(Subject, 0);
}
/// <summary>
/// Asserts if <see cref="IWireMockServer"/> has received a call.
/// </summary>

View File

@@ -1,9 +1,10 @@
namespace WireMock.Constants
namespace WireMock.Constants;
internal static class WireMockConstants
{
internal static class WireMockConstants
{
public const int AdminPriority = int.MinValue;
public const int MinPriority = -1_000_000;
public const int ProxyPriority = -2_000_000;
}
public const int AdminPriority = int.MinValue;
public const int MinPriority = -1_000_000;
public const int ProxyPriority = -2_000_000;
public const string ContentTypeJson = "application/json";
}

View File

@@ -1,11 +1,10 @@
using System;
using System;
using System.Collections;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WireMock.Util;
using Stef.Validation;
using WireMock.Util;
namespace WireMock.Matchers
{
@@ -38,7 +37,7 @@ namespace WireMock.Matchers
/// <param name="value">The string value to check for equality.</param>
/// <param name="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
public JsonMatcher([NotNull] string value, bool ignoreCase = false, bool throwException = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, throwException)
public JsonMatcher(string value, bool ignoreCase = false, bool throwException = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, throwException)
{
}
@@ -48,7 +47,7 @@ namespace WireMock.Matchers
/// <param name="value">The object value to check for equality.</param>
/// <param name="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
public JsonMatcher([NotNull] object value, bool ignoreCase = false, bool throwException = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, throwException)
public JsonMatcher(object value, bool ignoreCase = false, bool throwException = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, throwException)
{
}
@@ -59,7 +58,7 @@ namespace WireMock.Matchers
/// <param name="value">The value to check for equality.</param>
/// <param name="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
public JsonMatcher(MatchBehaviour matchBehaviour, [NotNull] object value, bool ignoreCase = false, bool throwException = false)
public JsonMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool throwException = false)
{
Guard.NotNull(value, nameof(value));
@@ -75,12 +74,12 @@ namespace WireMock.Matchers
}
/// <inheritdoc cref="IObjectMatcher.IsMatch"/>
public double IsMatch(object input)
public double IsMatch(object? input)
{
bool match = false;
// When input is null or byte[], return Mismatch.
if (input != null && !(input is byte[]))
if (input != null && input is not byte[])
{
try
{
@@ -132,7 +131,7 @@ namespace WireMock.Matchers
}
}
private static string ToUpper(string input)
private static string? ToUpper(string? input)
{
return input?.ToUpperInvariant();
}

View File

@@ -8,108 +8,107 @@ using WireMock.Models;
using WireMock.RegularExpressions;
using Stef.Validation;
namespace WireMock.Matchers
namespace WireMock.Matchers;
/// <summary>
/// Regular Expression Matcher
/// </summary>
/// <inheritdoc cref="IStringMatcher"/>
/// <inheritdoc cref="IIgnoreCaseMatcher"/>
public class RegexMatcher : IStringMatcher, IIgnoreCaseMatcher
{
private readonly AnyOf<string, StringPattern>[] _patterns;
private readonly Regex[] _expressions;
/// <inheritdoc cref="IMatcher.MatchBehaviour"/>
public MatchBehaviour MatchBehaviour { get; }
/// <inheritdoc cref="IMatcher.ThrowException"/>
public bool ThrowException { get; }
/// <summary>
/// Regular Expression Matcher
/// Initializes a new instance of the <see cref="RegexMatcher"/> class.
/// </summary>
/// <inheritdoc cref="IStringMatcher"/>
/// <inheritdoc cref="IIgnoreCaseMatcher"/>
public class RegexMatcher : IStringMatcher, IIgnoreCaseMatcher
/// <param name="pattern">The pattern.</param>
/// <param name="ignoreCase">Ignore the case from the pattern.</param>
/// <param name="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
/// <param name="useRegexExtended">Use RegexExtended (default = true).</param>
public RegexMatcher([NotNull, RegexPattern] AnyOf<string, StringPattern> pattern, bool ignoreCase = false, bool throwException = false, bool useRegexExtended = true) :
this(MatchBehaviour.AcceptOnMatch, new[] { pattern }, ignoreCase, throwException, useRegexExtended)
{
private readonly AnyOf<string, StringPattern>[] _patterns;
private readonly Regex[] _expressions;
/// <inheritdoc cref="IMatcher.MatchBehaviour"/>
public MatchBehaviour MatchBehaviour { get; }
/// <inheritdoc cref="IMatcher.ThrowException"/>
public bool ThrowException { get; }
/// <summary>
/// Initializes a new instance of the <see cref="RegexMatcher"/> class.
/// </summary>
/// <param name="pattern">The pattern.</param>
/// <param name="ignoreCase">Ignore the case from the pattern.</param>
/// <param name="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
/// <param name="useRegexExtended">Use RegexExtended (default = true).</param>
public RegexMatcher([NotNull, RegexPattern] AnyOf<string, StringPattern> pattern, bool ignoreCase = false, bool throwException = false, bool useRegexExtended = true) :
this(MatchBehaviour.AcceptOnMatch, new[] { pattern }, ignoreCase, throwException, useRegexExtended)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RegexMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="pattern">The pattern.</param>
/// <param name="ignoreCase">Ignore the case from the pattern.</param>
/// <param name="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
/// <param name="useRegexExtended">Use RegexExtended (default = true).</param>
public RegexMatcher(MatchBehaviour matchBehaviour, [NotNull, RegexPattern] AnyOf<string, StringPattern> pattern, bool ignoreCase = false, bool throwException = false, bool useRegexExtended = true) :
this(matchBehaviour, new[] { pattern }, ignoreCase, throwException, useRegexExtended)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RegexMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="patterns">The patterns.</param>
/// <param name="ignoreCase">Ignore the case from the pattern.</param>
/// <param name="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
/// <param name="useRegexExtended">Use RegexExtended (default = true).</param>
public RegexMatcher(MatchBehaviour matchBehaviour, [NotNull, RegexPattern] AnyOf<string, StringPattern>[] patterns, bool ignoreCase = false, bool throwException = false, bool useRegexExtended = true)
{
Guard.NotNull(patterns, nameof(patterns));
_patterns = patterns;
IgnoreCase = ignoreCase;
MatchBehaviour = matchBehaviour;
ThrowException = throwException;
RegexOptions options = RegexOptions.Compiled | RegexOptions.Multiline;
if (ignoreCase)
{
options |= RegexOptions.IgnoreCase;
}
_expressions = patterns.Select(p => useRegexExtended ? new RegexExtended(p.GetPattern(), options) : new Regex(p.GetPattern(), options)).ToArray();
}
/// <inheritdoc cref="IStringMatcher.IsMatch"/>
public virtual double IsMatch(string input)
{
double match = MatchScores.Mismatch;
if (input != null)
{
try
{
match = MatchScores.ToScore(_expressions.Select(e => e.IsMatch(input)));
}
catch (Exception)
{
if (ThrowException)
{
throw;
}
}
}
return MatchBehaviourHelper.Convert(MatchBehaviour, match);
}
/// <inheritdoc cref="IStringMatcher.GetPatterns"/>
public virtual AnyOf<string, StringPattern>[] GetPatterns()
{
return _patterns;
}
/// <inheritdoc cref="IMatcher.Name"/>
public virtual string Name => "RegexMatcher";
/// <inheritdoc cref="IIgnoreCaseMatcher.IgnoreCase"/>
public bool IgnoreCase { get; }
}
/// <summary>
/// Initializes a new instance of the <see cref="RegexMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="pattern">The pattern.</param>
/// <param name="ignoreCase">Ignore the case from the pattern.</param>
/// <param name="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
/// <param name="useRegexExtended">Use RegexExtended (default = true).</param>
public RegexMatcher(MatchBehaviour matchBehaviour, [NotNull, RegexPattern] AnyOf<string, StringPattern> pattern, bool ignoreCase = false, bool throwException = false, bool useRegexExtended = true) :
this(matchBehaviour, new[] { pattern }, ignoreCase, throwException, useRegexExtended)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RegexMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="patterns">The patterns.</param>
/// <param name="ignoreCase">Ignore the case from the pattern.</param>
/// <param name="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
/// <param name="useRegexExtended">Use RegexExtended (default = true).</param>
public RegexMatcher(MatchBehaviour matchBehaviour, [NotNull, RegexPattern] AnyOf<string, StringPattern>[] patterns, bool ignoreCase = false, bool throwException = false, bool useRegexExtended = true)
{
Guard.NotNull(patterns, nameof(patterns));
_patterns = patterns;
IgnoreCase = ignoreCase;
MatchBehaviour = matchBehaviour;
ThrowException = throwException;
RegexOptions options = RegexOptions.Compiled | RegexOptions.Multiline;
if (ignoreCase)
{
options |= RegexOptions.IgnoreCase;
}
_expressions = patterns.Select(p => useRegexExtended ? new RegexExtended(p.GetPattern(), options) : new Regex(p.GetPattern(), options)).ToArray();
}
/// <inheritdoc cref="IStringMatcher.IsMatch"/>
public virtual double IsMatch(string input)
{
double match = MatchScores.Mismatch;
if (input != null)
{
try
{
match = MatchScores.ToScore(_expressions.Select(e => e.IsMatch(input)));
}
catch (Exception)
{
if (ThrowException)
{
throw;
}
}
}
return MatchBehaviourHelper.Convert(MatchBehaviour, match);
}
/// <inheritdoc />
public virtual AnyOf<string, StringPattern>[] GetPatterns()
{
return _patterns;
}
/// <inheritdoc />
public virtual string Name => nameof(RegexMatcher);
/// <inheritdoc />
public bool IgnoreCase { get; }
}

View File

@@ -5,75 +5,74 @@ using JetBrains.Annotations;
using WireMock.Extensions;
using WireMock.Models;
namespace WireMock.Matchers
namespace WireMock.Matchers;
/// <summary>
/// WildcardMatcher
/// </summary>
/// <seealso cref="RegexMatcher" />
public class WildcardMatcher : RegexMatcher
{
private readonly AnyOf<string, StringPattern>[] _patterns;
/// <summary>
/// WildcardMatcher
/// Initializes a new instance of the <see cref="WildcardMatcher"/> class.
/// </summary>
/// <seealso cref="RegexMatcher" />
public class WildcardMatcher : RegexMatcher
/// <param name="pattern">The pattern.</param>
/// <param name="ignoreCase">IgnoreCase</param>
public WildcardMatcher([NotNull] AnyOf<string, StringPattern> pattern, bool ignoreCase = false) : this(new[] { pattern }, ignoreCase)
{
private readonly AnyOf<string, StringPattern>[] _patterns;
}
/// <summary>
/// Initializes a new instance of the <see cref="WildcardMatcher"/> class.
/// </summary>
/// <param name="pattern">The pattern.</param>
/// <param name="ignoreCase">IgnoreCase</param>
public WildcardMatcher([NotNull] AnyOf<string, StringPattern> pattern, bool ignoreCase = false) : this(new[] { pattern }, ignoreCase)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WildcardMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="pattern">The pattern.</param>
/// <param name="ignoreCase">IgnoreCase</param>
public WildcardMatcher(MatchBehaviour matchBehaviour, [NotNull] AnyOf<string, StringPattern> pattern, bool ignoreCase = false) : this(matchBehaviour, new[] { pattern }, ignoreCase)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WildcardMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="pattern">The pattern.</param>
/// <param name="ignoreCase">IgnoreCase</param>
public WildcardMatcher(MatchBehaviour matchBehaviour, [NotNull] AnyOf<string, StringPattern> pattern, bool ignoreCase = false) : this(matchBehaviour, new[] { pattern }, ignoreCase)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WildcardMatcher"/> class.
/// </summary>
/// <param name="patterns">The patterns.</param>
/// <param name="ignoreCase">IgnoreCase</param>
public WildcardMatcher([NotNull] AnyOf<string, StringPattern>[] patterns, bool ignoreCase = false) : this(MatchBehaviour.AcceptOnMatch, patterns, ignoreCase)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WildcardMatcher"/> class.
/// </summary>
/// <param name="patterns">The patterns.</param>
/// <param name="ignoreCase">IgnoreCase</param>
public WildcardMatcher([NotNull] AnyOf<string, StringPattern>[] patterns, bool ignoreCase = false) : this(MatchBehaviour.AcceptOnMatch, patterns, ignoreCase)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WildcardMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="patterns">The patterns.</param>
/// <param name="ignoreCase">IgnoreCase</param>
/// <param name="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
public WildcardMatcher(MatchBehaviour matchBehaviour, [NotNull] AnyOf<string, StringPattern>[] patterns, bool ignoreCase = false, bool throwException = false) :
base(matchBehaviour, CreateArray(patterns), ignoreCase, throwException)
{
_patterns = patterns;
}
/// <summary>
/// Initializes a new instance of the <see cref="WildcardMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="patterns">The patterns.</param>
/// <param name="ignoreCase">IgnoreCase</param>
/// <param name="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
public WildcardMatcher(MatchBehaviour matchBehaviour, [NotNull] AnyOf<string, StringPattern>[] patterns, bool ignoreCase = false, bool throwException = false) :
base(matchBehaviour, CreateArray(patterns), ignoreCase, throwException)
{
_patterns = patterns;
}
/// <inheritdoc />
public override AnyOf<string, StringPattern>[] GetPatterns()
{
return _patterns;
}
/// <inheritdoc cref="IStringMatcher.GetPatterns"/>
public override AnyOf<string, StringPattern>[] GetPatterns()
{
return _patterns;
}
/// <inheritdoc />
public override string Name => nameof(WildcardMatcher);
/// <inheritdoc cref="IMatcher.Name"/>
public override string Name => "WildcardMatcher";
private static AnyOf<string, StringPattern>[] CreateArray(AnyOf<string, StringPattern>[] patterns)
{
return patterns.Select(pattern => new AnyOf<string, StringPattern>(
private static AnyOf<string, StringPattern>[] CreateArray(AnyOf<string, StringPattern>[] patterns)
{
return patterns.Select(pattern => new AnyOf<string, StringPattern>(
new StringPattern
{
Pattern = "^" + Regex.Escape(pattern.GetPattern()).Replace(@"\*", ".*").Replace(@"\?", ".") + "$",
PatternAsFile = pattern.IsSecond ? pattern.Second.PatternAsFile : null
}))
.ToArray();
}
.ToArray();
}
}

View File

@@ -17,6 +17,6 @@ namespace WireMock.Owin.Mappers
/// </summary>
/// <param name="responseMessage">The ResponseMessage</param>
/// <param name="response">The OwinResponse/HttpResponse</param>
Task MapAsync(IResponseMessage responseMessage, IResponse response);
Task MapAsync(IResponseMessage? responseMessage, IResponse response);
}
}

View File

@@ -47,20 +47,18 @@ namespace WireMock.Owin.Mappers
/// <param name="options">The IWireMockMiddlewareOptions.</param>
public OwinResponseMapper(IWireMockMiddlewareOptions options)
{
Guard.NotNull(options, nameof(options));
_options = options;
_options = Guard.NotNull(options);
}
/// <inheritdoc cref="IOwinResponseMapper.MapAsync"/>
public async Task MapAsync(IResponseMessage responseMessage, IResponse response)
public async Task MapAsync(IResponseMessage? responseMessage, IResponse response)
{
if (responseMessage == null)
{
return;
}
byte[] bytes;
byte[]? bytes;
switch (responseMessage.FaultType)
{
case FaultType.EMPTY_RESPONSE:
@@ -122,33 +120,28 @@ namespace WireMock.Owin.Mappers
return responseMessage.FaultPercentage == null || _randomizerDouble.Generate() <= responseMessage.FaultPercentage;
}
private byte[] GetNormalBody(IResponseMessage responseMessage)
private byte[]? GetNormalBody(IResponseMessage responseMessage)
{
byte[] bytes = null;
switch (responseMessage.BodyData?.DetectedBodyType)
{
case BodyType.String:
bytes = (responseMessage.BodyData.Encoding ?? _utf8NoBom).GetBytes(responseMessage.BodyData.BodyAsString);
break;
return (responseMessage.BodyData.Encoding ?? _utf8NoBom).GetBytes(responseMessage.BodyData.BodyAsString);
case BodyType.Json:
Formatting formatting = responseMessage.BodyData.BodyAsJsonIndented == true
var formatting = responseMessage.BodyData.BodyAsJsonIndented == true
? Formatting.Indented
: Formatting.None;
string jsonBody = JsonConvert.SerializeObject(responseMessage.BodyData.BodyAsJson, new JsonSerializerSettings { Formatting = formatting, NullValueHandling = NullValueHandling.Ignore });
bytes = (responseMessage.BodyData.Encoding ?? _utf8NoBom).GetBytes(jsonBody);
break;
return (responseMessage.BodyData.Encoding ?? _utf8NoBom).GetBytes(jsonBody);
case BodyType.Bytes:
bytes = responseMessage.BodyData.BodyAsBytes;
break;
return responseMessage.BodyData.BodyAsBytes;
case BodyType.File:
bytes = _options.FileSystemHandler.ReadResponseBodyAsFile(responseMessage.BodyData.BodyAsFile);
break;
return _options.FileSystemHandler.ReadResponseBodyAsFile(responseMessage.BodyData.BodyAsFile);
}
return bytes;
return null;
}
private static void SetResponseHeaders(IResponseMessage responseMessage, IResponse response)

View File

@@ -72,8 +72,8 @@ namespace WireMock.Owin
var request = await _requestMapper.MapAsync(ctx.Request, _options).ConfigureAwait(false);
var logRequest = false;
IResponseMessage response = null;
(MappingMatcherResult Match, MappingMatcherResult Partial) result = (null, null);
IResponseMessage? response = null;
(MappingMatcherResult? Match, MappingMatcherResult? Partial) result = (null, null);
try
{
foreach (var mapping in _options.Mappings.Values.Where(m => m?.Scenario != null))

View File

@@ -1,4 +1,4 @@
// This source file is based on mock4net by Alexandre Victoor which is licensed under the Apache 2.0 License.
// This source file is based on mock4net by Alexandre Victoor which is licensed under the Apache 2.0 License.
// For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root.
using System.Collections.Generic;
using System.Linq;
@@ -27,7 +27,7 @@ namespace WireMock
public string BodyDestination { get; set; }
/// <inheritdoc cref="IResponseMessage.BodyData" />
public IBodyData BodyData { get; set; }
public IBodyData? BodyData { get; set; }
/// <inheritdoc cref="IResponseMessage.FaultType" />
public FaultType FaultType { get; set; }

View File

@@ -1,50 +1,49 @@
using System;
using System;
using System.Collections.Generic;
using WireMock.Admin.Mappings;
using WireMock.Constants;
using WireMock.Http;
using WireMock.Types;
using WireMock.Util;
namespace WireMock
namespace WireMock;
internal static class ResponseMessageBuilder
{
internal static class ResponseMessageBuilder
private static readonly IDictionary<string, WireMockList<string>> ContentTypeJsonHeaders = new Dictionary<string, WireMockList<string>>
{
private static string ContentTypeJson = "application/json";
private static readonly IDictionary<string, WireMockList<string>> ContentTypeJsonHeaders = new Dictionary<string, WireMockList<string>>
{ HttpKnownHeaderNames.ContentType, new WireMockList<string> { WireMockConstants.ContentTypeJson } }
};
internal static ResponseMessage Create(string? message, int statusCode = 200, Guid? guid = null)
{
var response = new ResponseMessage
{
{ HttpKnownHeaderNames.ContentType, new WireMockList<string> { ContentTypeJson } }
StatusCode = statusCode,
Headers = ContentTypeJsonHeaders
};
internal static ResponseMessage Create(string message, int statusCode = 200, Guid? guid = null)
if (message != null)
{
var response = new ResponseMessage
response.BodyData = new BodyData
{
StatusCode = statusCode,
Headers = ContentTypeJsonHeaders
};
if (message != null)
{
response.BodyData = new BodyData
DetectedBodyType = BodyType.Json,
BodyAsJson = new StatusModel
{
DetectedBodyType = BodyType.Json,
BodyAsJson = new StatusModel
{
Guid = guid,
Status = message
}
};
}
return response;
}
internal static ResponseMessage Create(int statusCode)
{
return new ResponseMessage
{
StatusCode = statusCode
Guid = guid,
Status = message
}
};
}
return response;
}
internal static ResponseMessage Create(int statusCode)
{
return new ResponseMessage
{
StatusCode = statusCode
};
}
}

View File

@@ -1,30 +1,34 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace WireMock.Serialization
namespace WireMock.Serialization;
internal static class JsonSerializationConstants
{
internal static class JsonSerializationConstants
public static readonly JsonSerializerSettings JsonSerializerSettingsDefault = new()
{
public static readonly JsonSerializerSettings JsonSerializerSettingsDefault = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore
};
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore
};
public static readonly JsonSerializerSettings JsonSerializerSettingsIncludeNullValues = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Include
};
public static readonly JsonSerializerSettings JsonSerializerSettingsIncludeNullValues = new()
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Include
};
public static readonly JsonSerializerSettings JsonDeserializerSettingsWithDateParsingNone = new JsonSerializerSettings
{
DateParseHandling = DateParseHandling.None
};
public static readonly JsonSerializerSettings JsonDeserializerSettingsWithDateParsingNone = new()
{
DateParseHandling = DateParseHandling.None
};
public static readonly JsonSerializerSettings JsonSerializerSettingsPact = new JsonSerializerSettings
public static readonly JsonSerializerSettings JsonSerializerSettingsPact = new()
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new DefaultContractResolver
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore
};
}
NamingStrategy = new CamelCaseNamingStrategy()
}
};
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using AnyOfTypes;
using JetBrains.Annotations;
using SimMetrics.Net;
using WireMock.Admin.Mappings;
using WireMock.Extensions;
@@ -22,12 +21,12 @@ namespace WireMock.Serialization
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
}
public IMatcher[] Map([CanBeNull] IEnumerable<MatcherModel> matchers)
public IMatcher[] Map(IEnumerable<MatcherModel>? matchers)
{
return matchers?.Select(Map).Where(m => m != null).ToArray();
}
public IMatcher Map([CanBeNull] MatcherModel matcher)
public IMatcher? Map(MatcherModel? matcher)
{
if (matcher == null)
{
@@ -36,7 +35,7 @@ namespace WireMock.Serialization
string[] parts = matcher.Name.Split('.');
string matcherName = parts[0];
string matcherType = parts.Length > 1 ? parts[1] : null;
string? matcherType = parts.Length > 1 ? parts[1] : null;
var stringPatterns = ParseStringPatterns(matcher);
var matchBehaviour = matcher.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch;
bool ignoreCase = matcher.IgnoreCase == true;
@@ -69,16 +68,16 @@ namespace WireMock.Serialization
return new RegexMatcher(matchBehaviour, stringPatterns, ignoreCase, throwExceptionWhenMatcherFails, useRegexExtended);
case nameof(JsonMatcher):
object valueForJsonMatcher = matcher.Pattern ?? matcher.Patterns;
return new JsonMatcher(matchBehaviour, valueForJsonMatcher, ignoreCase, throwExceptionWhenMatcherFails);
var valueForJsonMatcher = matcher.Pattern ?? matcher.Patterns;
return new JsonMatcher(matchBehaviour, valueForJsonMatcher!, ignoreCase, throwExceptionWhenMatcherFails);
case nameof(JsonPartialMatcher):
object valueForJsonPartialMatcher = matcher.Pattern ?? matcher.Patterns;
return new JsonPartialMatcher(matchBehaviour, valueForJsonPartialMatcher, ignoreCase, throwExceptionWhenMatcherFails);
var valueForJsonPartialMatcher = matcher.Pattern ?? matcher.Patterns;
return new JsonPartialMatcher(matchBehaviour, valueForJsonPartialMatcher!, ignoreCase, throwExceptionWhenMatcherFails);
case nameof(JsonPartialWildcardMatcher):
object valueForJsonPartialWildcardMatcher = matcher.Pattern ?? matcher.Patterns;
return new JsonPartialWildcardMatcher(matchBehaviour, valueForJsonPartialWildcardMatcher, ignoreCase, throwExceptionWhenMatcherFails);
var valueForJsonPartialWildcardMatcher = matcher.Pattern ?? matcher.Patterns;
return new JsonPartialWildcardMatcher(matchBehaviour, valueForJsonPartialWildcardMatcher!, ignoreCase, throwExceptionWhenMatcherFails);
case nameof(JsonPathMatcher):
return new JsonPathMatcher(matchBehaviour, throwExceptionWhenMatcherFails, stringPatterns);
@@ -114,12 +113,12 @@ namespace WireMock.Serialization
}
}
public MatcherModel[] Map([CanBeNull] IEnumerable<IMatcher> matchers)
public MatcherModel[] Map(IEnumerable<IMatcher>? matchers)
{
return matchers?.Select(Map).Where(m => m != null).ToArray();
}
public MatcherModel Map([CanBeNull] IMatcher matcher)
public MatcherModel? Map(IMatcher? matcher)
{
if (matcher == null)
{

View File

@@ -2,28 +2,25 @@ using System;
using System.Collections.Generic;
using System.Linq;
using WireMock.Admin.Mappings;
using WireMock.Extensions;
using WireMock.Matchers;
using WireMock.Pact.Models.V2;
using WireMock.Server;
using WireMock.Util;
namespace WireMock.Server;
namespace WireMock.Serialization;
public partial class WireMockServer
internal static class PactMapper
{
private const string DefaultPath = "/";
private const string DefaultMethod = "GET";
private const int DefaultStatus = 200;
private const int DefaultStatusCode = 200;
private const string DefaultConsumer = "Default Consumer";
private const string DefaultProvider = "Default Provider";
/// <summary>
/// Save the mappings as a Pact Json file V2.
/// </summary>
/// <param name="folder">The folder to save the pact file.</param>
/// <param name="filename">The filename for the .json file [optional].</param>
public void SavePact(string folder, string? filename = null)
public static (string FileName, byte[] Bytes) ToPact(WireMockServer server, string? filename = null)
{
var consumer = Consumer ?? DefaultConsumer;
var provider = Provider ?? DefaultProvider;
var consumer = server.Consumer ?? DefaultConsumer;
var provider = server.Provider ?? DefaultProvider;
filename ??= $"{consumer} - {provider}.json";
@@ -33,41 +30,31 @@ public partial class WireMockServer
Provider = new Pacticipant { Name = provider }
};
foreach (var mapping in MappingModels)
foreach (var mapping in server.MappingModels.OrderBy(m => m.Guid))
{
var path = mapping.Request.GetPathAsString();
if (path == null)
{
// Path is null (probably a Func<>), skip this.
continue;
}
var interaction = new Interaction
{
Description = mapping.Description,
ProviderState = mapping.Title,
Request = MapRequest(mapping.Request),
Request = MapRequest(mapping.Request, path),
Response = MapResponse(mapping.Response)
};
pact.Interactions.Add(interaction);
}
var bytes = JsonUtils.SerializeAsPactFile(pact);
_settings.FileSystemHandler.WriteFile(folder, filename, bytes);
return (filename, JsonUtils.SerializeAsPactFile(pact));
}
private static Request MapRequest(RequestModel request)
private static Request MapRequest(RequestModel request, string path)
{
string path;
switch (request.Path)
{
case string pathAsString:
path = pathAsString;
break;
case PathModel pathModel:
path = GetPatternAsStringFromMatchers(pathModel.Matchers, DefaultPath);
break;
default:
path = DefaultPath;
break;
}
return new Request
{
Method = request.Methods?.FirstOrDefault() ?? DefaultMethod,
@@ -97,7 +84,7 @@ public partial class WireMockServer
{
if (statusCode is string statusCodeAsString)
{
return int.TryParse(statusCodeAsString, out var statusCodeAsInt) ? statusCodeAsInt : DefaultStatus;
return int.TryParse(statusCodeAsString, out var statusCodeAsInt) ? statusCodeAsInt : DefaultStatusCode;
}
if (statusCode != null)
@@ -106,7 +93,7 @@ public partial class WireMockServer
return Convert.ToInt32(statusCode);
}
return DefaultStatus;
return DefaultStatusCode;
}
private static string? MapQueryParameters(IList<ParamModel>? queryParameters)
@@ -118,41 +105,37 @@ public partial class WireMockServer
var values = queryParameters
.Where(qp => qp.Matchers != null && qp.Matchers.Any() && qp.Matchers[0].Pattern is string)
.Select(param => $"{Uri.EscapeDataString(param.Name)}={Uri.EscapeDataString((string)param.Matchers![0].Pattern)}");
.Select(param => $"{Uri.EscapeDataString(param.Name)}={Uri.EscapeDataString((string)param.Matchers![0].Pattern!)}");
return string.Join("&", values);
}
private static IDictionary<string, string>? MapRequestHeaders(IList<HeaderModel>? headers)
{
if (headers == null)
{
return null;
}
var validHeaders = headers.Where(h => h.Matchers != null && h.Matchers.Any() && h.Matchers[0].Pattern is string);
return validHeaders.ToDictionary(x => x.Name, y => (string)y.Matchers![0].Pattern);
var validHeaders = headers?.Where(h => h.Matchers != null && h.Matchers.Any() && h.Matchers[0].Pattern is string);
return validHeaders?.ToDictionary(x => x.Name, y => (string)y.Matchers![0].Pattern!);
}
private static IDictionary<string, string>? MapResponseHeaders(IDictionary<string, object>? headers)
{
if (headers == null)
{
return null;
}
var validHeaders = headers.Where(h => h.Value is string);
return validHeaders.ToDictionary(x => x.Key, y => (string)y.Value);
var validHeaders = headers?.Where(h => h.Value is string);
return validHeaders?.ToDictionary(x => x.Key, y => (string)y.Value);
}
private static object? MapBody(BodyModel? body)
{
if (body == null || body.Matcher.Name != "JsonMatcher")
if (body?.Matcher == null || body.Matchers == null)
{
return null;
}
return body.Matcher.Pattern;
if (body.Matcher is { Name: nameof(JsonMatcher) })
{
return body.Matcher.Pattern;
}
var jsonMatcher = body.Matchers.FirstOrDefault(m => m.Name == nameof(JsonMatcher));
return jsonMatcher?.Pattern;
}
private static string GetPatternAsStringFromMatchers(MatcherModel[]? matchers, string defaultValue)

View File

@@ -0,0 +1,325 @@
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using NJsonSchema;
using NJsonSchema.Extensions;
using NSwag;
using WireMock.Admin.Mappings;
using WireMock.Constants;
using WireMock.Extensions;
using WireMock.Matchers;
using WireMock.Server;
using WireMock.Util;
namespace WireMock.Serialization;
internal static class SwaggerMapper
{
private const string DefaultMethod = "GET";
private const string Generator = "WireMock.Net";
private static readonly JsonSchema JsonSchemaString = new() { Type = JsonObjectType.String };
public static string ToSwagger(WireMockServer server)
{
var openApiDocument = new OpenApiDocument
{
Generator = Generator,
Info = new OpenApiInfo
{
Title = $"{Generator} Mappings Swagger specification",
Version = SystemUtils.Version
},
};
foreach (var url in server.Urls)
{
openApiDocument.Servers.Add(new OpenApiServer
{
Url = url
});
}
foreach (var mapping in server.MappingModels)
{
var path = mapping.Request.GetPathAsString();
if (path == null)
{
// Path is null (probably a Func<>), skip this.
continue;
}
var operation = new OpenApiOperation();
foreach (var openApiParameter in MapRequestQueryParameters(mapping.Request.Params))
{
operation.Parameters.Add(openApiParameter);
}
foreach (var openApiParameter in MapRequestHeaders(mapping.Request.Headers))
{
operation.Parameters.Add(openApiParameter);
}
foreach (var openApiParameter in MapRequestCookies(mapping.Request.Cookies))
{
operation.Parameters.Add(openApiParameter);
}
operation.RequestBody = MapRequestBody(mapping.Request);
var response = MapResponse(mapping.Response);
if (response != null)
{
operation.Responses.Add(mapping.Response.GetStatusCodeAsString(), response);
}
var method = mapping.Request.Methods?.FirstOrDefault() ?? DefaultMethod;
if (!openApiDocument.Paths.ContainsKey(path))
{
var openApiPathItem = new OpenApiPathItem
{
{ method, operation }
};
openApiDocument.Paths.Add(path, openApiPathItem);
}
else
{
// The combination of path+method uniquely identify an operation. Duplicates are not allowed.
if (!openApiDocument.Paths[path].ContainsKey(method))
{
openApiDocument.Paths[path].Add(method, operation);
}
}
}
return openApiDocument.ToJson(SchemaType.OpenApi3, Formatting.Indented);
}
private static IEnumerable<OpenApiParameter> MapRequestQueryParameters(IList<ParamModel>? queryParameters)
{
if (queryParameters == null)
{
return new List<OpenApiParameter>();
}
return queryParameters
.Where(x => x.Matchers != null && x.Matchers.Any())
.Select(x => new
{
x.Name,
Details = GetDetailsFromMatcher(x.Matchers![0])
})
.Select(x => new OpenApiParameter
{
Name = x.Name,
Example = x.Details.Example,
Description = x.Details.Description,
Kind = OpenApiParameterKind.Query,
Schema = x.Details.JsonSchemaRegex,
IsRequired = !x.Details.Reject
})
.ToList();
}
private static IEnumerable<OpenApiParameter> MapRequestHeaders(IList<HeaderModel>? headers)
{
if (headers == null)
{
return new List<OpenApiHeader>();
}
return headers
.Where(x => x.Matchers != null && x.Matchers.Any())
.Select(x => new
{
x.Name,
Details = GetDetailsFromMatcher(x.Matchers![0])
})
.Select(x => new OpenApiHeader
{
Name = x.Name,
Example = x.Details.Example,
Description = x.Details.Description,
Kind = OpenApiParameterKind.Header,
Schema = x.Details.JsonSchemaRegex,
IsRequired = !x.Details.Reject
})
.ToList();
}
private static IEnumerable<OpenApiParameter> MapRequestCookies(IList<CookieModel>? cookies)
{
if (cookies == null)
{
return new List<OpenApiParameter>();
}
return cookies
.Where(x => x.Matchers != null && x.Matchers.Any())
.Select(x => new
{
x.Name,
Details = GetDetailsFromMatcher(x.Matchers![0])
})
.Select(x => new OpenApiParameter
{
Name = x.Name,
Example = x.Details.Example,
Description = x.Details.Description,
Kind = OpenApiParameterKind.Cookie,
Schema = x.Details.JsonSchemaRegex,
IsRequired = !x.Details.Reject
})
.ToList();
}
private static (JsonSchema JsonSchemaRegex, string? Example, string? Description, bool Reject) GetDetailsFromMatcher(MatcherModel matcher)
{
var pattern = GetPatternAsStringFromMatcher(matcher);
var reject = matcher.RejectOnMatch == true;
var description = $"{matcher.Name} with RejectOnMatch = '{reject}' and Pattern = '{pattern}'";
return matcher.Name is nameof(RegexMatcher) ?
(new JsonSchema { Type = JsonObjectType.String, Format = "regex", Pattern = pattern }, pattern, description, reject) :
(JsonSchemaString, pattern, description, reject);
}
private static OpenApiRequestBody? MapRequestBody(RequestModel request)
{
var body = MapRequestBody(request.Body);
if (body == null)
{
return null;
}
var openApiMediaType = new OpenApiMediaType
{
Schema = GetJsonSchema(body)
};
var requestBodyPost = new OpenApiRequestBody();
requestBodyPost.Content.Add(GetContentType(request), openApiMediaType);
return requestBodyPost;
}
private static OpenApiResponse? MapResponse(ResponseModel response)
{
if (response.Body != null)
{
return new OpenApiResponse
{
Schema = new JsonSchemaProperty
{
Type = JsonObjectType.String,
Example = response.Body
}
};
}
if (response.BodyAsBytes != null)
{
// https://stackoverflow.com/questions/62794949/how-to-define-byte-array-in-openapi-3-0
return new OpenApiResponse
{
Schema = new JsonSchemaProperty
{
Type = JsonObjectType.Array,
Items =
{
new JsonSchema
{
Type = JsonObjectType.String,
Format = JsonFormatStrings.Byte
}
}
}
};
}
if (response.BodyAsJson == null)
{
return null;
}
return new OpenApiResponse
{
Schema = GetJsonSchema(response.BodyAsJson)
};
}
private static JsonSchema GetJsonSchema(object instance)
{
switch (instance)
{
case string instanceAsString:
try
{
var value = JsonConvert.DeserializeObject(instanceAsString);
return GetJsonSchema(value!);
}
catch
{
return JsonSchemaString;
}
default:
return instance.ToJsonSchema();
}
}
private static object? MapRequestBody(BodyModel? body)
{
if (body == null)
{
return null;
}
var matcher = GetMatcher(body.Matcher, body.Matchers);
if (matcher is { Name: nameof(JsonMatcher) })
{
var pattern = GetPatternAsStringFromMatcher(matcher);
if (JsonUtils.TryParseAsJObject(pattern, out var jObject))
{
return jObject;
}
return pattern;
}
return null;
}
private static string GetContentType(RequestModel request)
{
var contentType = request.Headers?.FirstOrDefault(h => h.Name == "Content-Type");
return contentType == null ?
WireMockConstants.ContentTypeJson :
GetPatternAsStringFromMatchers(contentType.Matchers, WireMockConstants.ContentTypeJson);
}
private static string GetPatternAsStringFromMatchers(IList<MatcherModel>? matchers, string defaultValue)
{
if (matchers == null || !matchers.Any())
{
return defaultValue;
}
return GetPatternAsStringFromMatcher(matchers.First()) ?? defaultValue;
}
private static string? GetPatternAsStringFromMatcher(MatcherModel matcher)
{
if (matcher.Pattern is string patternAsString)
{
return patternAsString;
}
return matcher.Patterns?.FirstOrDefault() as string;
}
private static MatcherModel? GetMatcher(MatcherModel? matcher, MatcherModel[]? matchers)
{
return matcher ?? matchers?.FirstOrDefault();
}
}

View File

@@ -35,7 +35,6 @@ namespace WireMock.Server;
public partial class WireMockServer
{
private const int EnhancedFileSystemWatcherTimeoutMs = 1000;
private const string ContentTypeJson = "application/json";
private const string AdminFiles = "/__admin/files";
private const string AdminMappings = "/__admin/mappings";
private const string AdminMappingsWireMockOrg = "/__admin/mappings/wiremock.org";
@@ -45,7 +44,7 @@ public partial class WireMockServer
private const string QueryParamReloadStaticMappings = "reloadStaticMappings";
private readonly Guid _proxyMappingGuid = new("e59914fd-782e-428e-91c1-4810ffb86567");
private readonly RegexMatcher _adminRequestContentTypeJson = new ContentTypeMatcher(ContentTypeJson, true);
private readonly RegexMatcher _adminRequestContentTypeJson = new ContentTypeMatcher(WireMockConstants.ContentTypeJson, true);
private readonly RegexMatcher _adminMappingsGuidPathMatcher = new(@"^\/__admin\/mappings\/([0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12})$");
private readonly RegexMatcher _adminRequestsGuidPathMatcher = new(@"^\/__admin\/requests\/([0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12})$");
@@ -73,7 +72,10 @@ public partial class WireMockServer
Given(Request.Create().WithPath(_adminMappingsGuidPathMatcher).UsingDelete()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingDelete));
// __admin/mappings/save
Given(Request.Create().WithPath(AdminMappings + "/save").UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingsSave));
Given(Request.Create().WithPath($"{AdminMappings}/save").UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingsSave));
// __admin/mappings/swagger
Given(Request.Create().WithPath($"{AdminMappings}/swagger").UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(SwaggerGet));
// __admin/requests
Given(Request.Create().WithPath(AdminRequests).UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(RequestsGet));
@@ -148,7 +150,7 @@ public partial class WireMockServer
/// <inheritdoc cref="IWireMockServer.WatchStaticMappings" />
[PublicAPI]
public void WatchStaticMappings([CanBeNull] string folder = null)
public void WatchStaticMappings(string? folder = null)
{
if (folder == null)
{
@@ -379,6 +381,20 @@ public partial class WireMockServer
#endregion Mapping/{guid}
#region Mappings
private IResponseMessage SwaggerGet(IRequestMessage requestMessage)
{
return new ResponseMessage
{
BodyData = new BodyData
{
DetectedBodyType = BodyType.String,
BodyAsString = SwaggerMapper.ToSwagger(this)
},
StatusCode = (int)HttpStatusCode.OK,
Headers = new Dictionary<string, WireMockList<string>> { { HttpKnownHeaderNames.ContentType, new WireMockList<string>(WireMockConstants.ContentTypeJson) } }
};
}
private IResponseMessage MappingsSave(IRequestMessage requestMessage)
{
SaveStaticMappings();
@@ -667,6 +683,19 @@ public partial class WireMockServer
}
#endregion
#region Pact
/// <summary>
/// Save the mappings as a Pact Json file V2.
/// </summary>
/// <param name="folder">The folder to save the pact file.</param>
/// <param name="filename">The filename for the .json file [optional].</param>
[PublicAPI]
public void SavePact(string folder, string? filename = null)
{
var (filenameUpdated, bytes) = PactMapper.ToPact(this, filename);
_settings.FileSystemHandler.WriteFile(folder, filenameUpdated, bytes);
}
/// <summary>
/// This stores details about the consumer of the interaction.
/// </summary>
@@ -688,7 +717,7 @@ public partial class WireMockServer
Provider = provider;
return this;
}
#endregion
private IRequestBuilder? InitRequestBuilder(RequestModel requestModel, bool pathOrUrlRequired)
{
IRequestBuilder requestBuilder = Request.Create();
@@ -904,68 +933,6 @@ public partial class WireMockServer
return responseBuilder;
}
private ResponseMessage ToJson<T>(T result, bool keepNullValues = false)
{
return new ResponseMessage
{
BodyData = new BodyData
{
DetectedBodyType = BodyType.String,
BodyAsString = JsonConvert.SerializeObject(result, keepNullValues ? JsonSerializationConstants.JsonSerializerSettingsIncludeNullValues : JsonSerializationConstants.JsonSerializerSettingsDefault)
},
StatusCode = (int)HttpStatusCode.OK,
Headers = new Dictionary<string, WireMockList<string>> { { HttpKnownHeaderNames.ContentType, new WireMockList<string>(ContentTypeJson) } }
};
}
private Encoding? ToEncoding(EncodingModel? encodingModel)
{
return encodingModel != null ? Encoding.GetEncoding(encodingModel.CodePage) : null;
}
private T? DeserializeObject<T>(IRequestMessage requestMessage)
{
if (requestMessage?.BodyData?.DetectedBodyType == BodyType.String)
{
return JsonUtils.DeserializeObject<T>(requestMessage.BodyData.BodyAsString);
}
if (requestMessage?.BodyData?.DetectedBodyType == BodyType.Json)
{
return ((JObject)requestMessage.BodyData.BodyAsJson).ToObject<T>();
}
return default(T);
}
private T[] DeserializeRequestMessageToArray<T>(IRequestMessage requestMessage)
{
if (requestMessage.BodyData?.DetectedBodyType == BodyType.Json)
{
var bodyAsJson = requestMessage.BodyData.BodyAsJson;
return DeserializeObjectToArray<T>(bodyAsJson);
}
return default(T[]);
}
private T[] DeserializeObjectToArray<T>(object value)
{
if (value is JArray jArray)
{
return jArray.ToObject<T[]>();
}
var singleResult = ((JObject)value).ToObject<T>();
return new[] { singleResult };
}
private T[] DeserializeJsonToArray<T>(string value)
{
return DeserializeObjectToArray<T>(JsonUtils.DeserializeObject(value));
}
private void DisposeEnhancedFileSystemWatcher()
{
if (_enhancedFileSystemWatcher != null)
@@ -1012,4 +979,66 @@ public partial class WireMockServer
DeleteMapping(args.FullPath);
}
}
private static Encoding? ToEncoding(EncodingModel? encodingModel)
{
return encodingModel != null ? Encoding.GetEncoding(encodingModel.CodePage) : null;
}
private static ResponseMessage ToJson<T>(T result, bool keepNullValues = false)
{
return new ResponseMessage
{
BodyData = new BodyData
{
DetectedBodyType = BodyType.String,
BodyAsString = JsonConvert.SerializeObject(result, keepNullValues ? JsonSerializationConstants.JsonSerializerSettingsIncludeNullValues : JsonSerializationConstants.JsonSerializerSettingsDefault)
},
StatusCode = (int)HttpStatusCode.OK,
Headers = new Dictionary<string, WireMockList<string>> { { HttpKnownHeaderNames.ContentType, new WireMockList<string>(WireMockConstants.ContentTypeJson) } }
};
}
private static T? DeserializeObject<T>(IRequestMessage requestMessage)
{
if (requestMessage?.BodyData?.DetectedBodyType == BodyType.String)
{
return JsonUtils.DeserializeObject<T>(requestMessage.BodyData.BodyAsString);
}
if (requestMessage?.BodyData?.DetectedBodyType == BodyType.Json)
{
return ((JObject)requestMessage.BodyData.BodyAsJson).ToObject<T>();
}
return default(T);
}
private static T[] DeserializeRequestMessageToArray<T>(IRequestMessage requestMessage)
{
if (requestMessage.BodyData?.DetectedBodyType == BodyType.Json)
{
var bodyAsJson = requestMessage.BodyData.BodyAsJson;
return DeserializeObjectToArray<T>(bodyAsJson);
}
return default(T[]);
}
private static T[] DeserializeJsonToArray<T>(string value)
{
return DeserializeObjectToArray<T>(JsonUtils.DeserializeObject(value));
}
private static T[] DeserializeObjectToArray<T>(object value)
{
if (value is JArray jArray)
{
return jArray.ToObject<T[]>();
}
var singleResult = ((JObject)value).ToObject<T>();
return new[] { singleResult };
}
}

View File

@@ -5,87 +5,89 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using JetBrains.Annotations;
using Stef.Validation;
using WireMock.Logging;
using WireMock.Matchers;
using WireMock.Matchers.Request;
namespace WireMock.Server
namespace WireMock.Server;
public partial class WireMockServer
{
public partial class WireMockServer
/// <inheritdoc cref="IWireMockServer.LogEntriesChanged" />
[PublicAPI]
public event NotifyCollectionChangedEventHandler LogEntriesChanged
{
/// <inheritdoc cref="IWireMockServer.LogEntriesChanged" />
[PublicAPI]
public event NotifyCollectionChangedEventHandler LogEntriesChanged
add
{
add
_options.LogEntries.CollectionChanged += (sender, eventRecordArgs) =>
{
_options.LogEntries.CollectionChanged += (sender, eventRecordArgs) =>
try
{
try
{
value(sender, eventRecordArgs);
}
catch (Exception exception)
{
_options.Logger.Error("Error calling the LogEntriesChanged event handler: {0}", exception.Message);
}
};
}
remove => _options.LogEntries.CollectionChanged -= value;
}
/// <inheritdoc cref="IWireMockServer.LogEntries" />
[PublicAPI]
public IEnumerable<ILogEntry> LogEntries => new ReadOnlyCollection<LogEntry>(_options.LogEntries.ToList());
/// <summary>
/// The search log-entries based on matchers.
/// </summary>
/// <param name="matchers">The matchers.</param>
/// <returns>The <see cref="IEnumerable"/>.</returns>
[PublicAPI]
public IEnumerable<LogEntry> FindLogEntries([NotNull] params IRequestMatcher[] matchers)
{
var results = new Dictionary<LogEntry, RequestMatchResult>();
foreach (var log in _options.LogEntries.ToList())
{
var requestMatchResult = new RequestMatchResult();
foreach (var matcher in matchers)
{
matcher.GetMatchingScore(log.RequestMessage, requestMatchResult);
value(sender, eventRecordArgs);
}
if (requestMatchResult.AverageTotalScore > MatchScores.AlmostPerfect)
catch (Exception exception)
{
results.Add(log, requestMatchResult);
_options.Logger.Error("Error calling the LogEntriesChanged event handler: {0}", exception.Message);
}
}
return new ReadOnlyCollection<LogEntry>(results.OrderBy(x => x.Value).Select(x => x.Key).ToList());
};
}
/// <inheritdoc cref="IWireMockServer.ResetLogEntries" />
[PublicAPI]
public void ResetLogEntries()
{
_options.LogEntries.Clear();
}
remove => _options.LogEntries.CollectionChanged -= value;
}
/// <inheritdoc cref="IWireMockServer.DeleteLogEntry" />
[PublicAPI]
public bool DeleteLogEntry(Guid guid)
/// <inheritdoc cref="IWireMockServer.LogEntries" />
[PublicAPI]
public IEnumerable<ILogEntry> LogEntries => new ReadOnlyCollection<LogEntry>(_options.LogEntries.ToList());
/// <summary>
/// The search log-entries based on matchers.
/// </summary>
/// <param name="matchers">The matchers.</param>
/// <returns>The <see cref="IEnumerable"/>.</returns>
[PublicAPI]
public IEnumerable<LogEntry> FindLogEntries(params IRequestMatcher[] matchers)
{
Guard.NotNull(matchers);
var results = new Dictionary<LogEntry, RequestMatchResult>();
foreach (var log in _options.LogEntries.ToList())
{
// Check a LogEntry exists with the same GUID, if so, remove it.
var existing = _options.LogEntries.ToList().FirstOrDefault(m => m.Guid == guid);
if (existing != null)
var requestMatchResult = new RequestMatchResult();
foreach (var matcher in matchers)
{
_options.LogEntries.Remove(existing);
return true;
matcher.GetMatchingScore(log.RequestMessage, requestMatchResult);
}
return false;
if (requestMatchResult.AverageTotalScore > MatchScores.AlmostPerfect)
{
results.Add(log, requestMatchResult);
}
}
return new ReadOnlyCollection<LogEntry>(results.OrderBy(x => x.Value).Select(x => x.Key).ToList());
}
/// <inheritdoc cref="IWireMockServer.ResetLogEntries" />
[PublicAPI]
public void ResetLogEntries()
{
_options.LogEntries.Clear();
}
/// <inheritdoc cref="IWireMockServer.DeleteLogEntry" />
[PublicAPI]
public bool DeleteLogEntry(Guid guid)
{
// Check a LogEntry exists with the same GUID, if so, remove it.
var existing = _options.LogEntries.ToList().FirstOrDefault(m => m.Guid == guid);
if (existing != null)
{
_options.LogEntries.Remove(existing);
return true;
}
return false;
}
}

View File

@@ -5,18 +5,49 @@ using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WireMock.Pact.Models.V2;
using WireMock.Serialization;
namespace WireMock.Util;
internal static class JsonUtils
{
public static bool TryParseAsComplexObject(string strInput, [NotNullWhen(true)] out JToken? token)
public static Type CreateTypeFromJObject(JObject instance, string? fullName = null)
{
token = null;
static Type ConvertType(JToken value, string? propertyName = null)
{
var type = value.Type;
return type switch
{
JTokenType.Array => value.HasValues ? ConvertType(value.First!, propertyName).MakeArrayType() : typeof(object).MakeArrayType(),
JTokenType.Boolean => typeof(bool),
JTokenType.Bytes => typeof(byte[]),
JTokenType.Date => typeof(DateTime),
JTokenType.Guid => typeof(Guid),
JTokenType.Float => typeof(float),
JTokenType.Integer => typeof(long),
JTokenType.Null => typeof(object),
JTokenType.Object => CreateTypeFromJObject((JObject)value, propertyName),
JTokenType.String => typeof(string),
JTokenType.TimeSpan => typeof(TimeSpan),
JTokenType.Uri => typeof(string),
_ => typeof(object)
};
}
if (string.IsNullOrWhiteSpace(strInput))
var properties = new Dictionary<string, Type>();
foreach (var item in instance.Properties())
{
properties.Add(item.Name, ConvertType(item.Value, item.Name));
}
return TypeBuilderUtils.BuildType(properties, fullName) ?? throw new InvalidOperationException();
}
public static bool TryParseAsJObject(string? strInput, [NotNullWhen(true)] out JObject? value)
{
value = null;
if (strInput == null || string.IsNullOrWhiteSpace(strInput))
{
return false;
}
@@ -30,7 +61,7 @@ internal static class JsonUtils
try
{
// Try to convert this string into a JToken
token = JToken.Parse(strInput);
value = JObject.Parse(strInput);
return true;
}
catch
@@ -105,17 +136,19 @@ internal static class JsonUtils
private static void WalkNode(JToken node, string? path, string? propertyName, List<string> lines)
{
if (node.Type == JTokenType.Object)
switch (node.Type)
{
ProcessObject(node, propertyName, lines);
}
else if (node.Type == JTokenType.Array)
{
ProcessArray(node, propertyName, lines);
}
else
{
ProcessItem(node, path ?? "it", propertyName, lines);
case JTokenType.Object:
ProcessObject(node, propertyName, lines);
break;
case JTokenType.Array:
ProcessArray(node, propertyName, lines);
break;
default:
ProcessItem(node, path ?? "it", propertyName, lines);
break;
}
}
@@ -125,7 +158,7 @@ internal static class JsonUtils
var text = new StringBuilder("new (");
// In case of Object, loop all children. Do a ToArray() to avoid `Collection was modified` exceptions.
foreach (JProperty child in node.Children<JProperty>().ToArray())
foreach (var child in node.Children<JProperty>().ToArray())
{
WalkNode(child.Value, child.Path, child.Name, items);
}
@@ -147,8 +180,8 @@ internal static class JsonUtils
var text = new StringBuilder("(new [] { ");
// In case of Array, loop all items. Do a ToArray() to avoid `Collection was modified` exceptions.
int idx = 0;
foreach (JToken child in node.Children().ToArray())
var idx = 0;
foreach (var child in node.Children().ToArray())
{
WalkNode(child, $"{node.Path}[{idx}]", null, items);
idx++;
@@ -165,50 +198,21 @@ internal static class JsonUtils
lines.Add(text.ToString());
}
private static void ProcessItem(JToken node, string path, string propertyName, List<string> lines)
private static void ProcessItem(JToken node, string path, string? propertyName, List<string> lines)
{
string castText;
switch (node.Type)
var castText = node.Type switch
{
case JTokenType.Boolean:
castText = $"bool({path})";
break;
case JTokenType.Date:
castText = $"DateTime({path})";
break;
case JTokenType.Float:
castText = $"double({path})";
break;
case JTokenType.Guid:
castText = $"Guid({path})";
break;
case JTokenType.Integer:
castText = $"long({path})";
break;
case JTokenType.Null:
castText = "null";
break;
case JTokenType.String:
castText = $"string({path})";
break;
case JTokenType.TimeSpan:
castText = $"TimeSpan({path})";
break;
case JTokenType.Uri:
castText = $"Uri({path})";
break;
default:
throw new NotSupportedException($"JTokenType '{node.Type}' cannot be converted to a Dynamic Linq cast operator.");
}
JTokenType.Boolean => $"bool({path})",
JTokenType.Date => $"DateTime({path})",
JTokenType.Float => $"double({path})",
JTokenType.Guid => $"Guid({path})",
JTokenType.Integer => $"long({path})",
JTokenType.Null => "null",
JTokenType.String => $"string({path})",
JTokenType.TimeSpan => $"TimeSpan({path})",
JTokenType.Uri => $"Uri({path})",
_ => throw new NotSupportedException($"JTokenType '{node.Type}' cannot be converted to a Dynamic Linq cast operator.")
};
if (!string.IsNullOrEmpty(propertyName))
{

View File

@@ -0,0 +1,8 @@
using System.Reflection;
namespace WireMock.Util;
internal static class SystemUtils
{
public static readonly string Version = typeof(SystemUtils).GetTypeInfo().Assembly.GetName().Version.ToString();
}

View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
namespace WireMock.Util;
/// <summary>
/// Code based on https://stackoverflow.com/questions/40507909/convert-jobject-to-anonymous-object
/// </summary>
internal static class TypeBuilderUtils
{
private static readonly ConcurrentDictionary<IDictionary<string, Type>, Type> Types = new();
private static readonly ModuleBuilder ModuleBuilder =
AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("WireMock.Net.Reflection"), AssemblyBuilderAccess.Run)
.DefineDynamicModule("WireMock.Net.Reflection.Module");
public static Type BuildType(IDictionary<string, Type> properties, string? name = null)
{
var keyExists = Types.Keys.FirstOrDefault(k => Compare(k, properties));
if (keyExists != null)
{
return Types[keyExists];
}
var typeBuilder = GetTypeBuilder(name ?? Guid.NewGuid().ToString());
foreach (var property in properties)
{
CreateGetSetMethods(typeBuilder, property.Key, property.Value);
}
var type = typeBuilder.CreateTypeInfo().AsType();
Types.TryAdd(properties, type);
return type;
}
/// <summary>
/// https://stackoverflow.com/questions/3804367/testing-for-equality-between-dictionaries-in-c-sharp
/// </summary>
private static bool Compare<TKey, TValue>(IDictionary<TKey, TValue> dict1, IDictionary<TKey, TValue> dict2)
{
if (dict1 == dict2)
{
return true;
}
if (dict1.Count != dict2.Count)
{
return false;
}
var valueComparer = EqualityComparer<TValue>.Default;
foreach (var kvp in dict1)
{
if (!dict2.TryGetValue(kvp.Key, out var value2))
{
return false;
}
if (!valueComparer.Equals(kvp.Value, value2))
{
return false;
}
}
return true;
}
private static TypeBuilder GetTypeBuilder(string name)
{
return ModuleBuilder.DefineType(name,
TypeAttributes.Public |
TypeAttributes.Class |
TypeAttributes.AutoClass |
TypeAttributes.AnsiClass |
TypeAttributes.BeforeFieldInit |
TypeAttributes.AutoLayout,
null);
}
private static void CreateGetSetMethods(TypeBuilder typeBuilder, string propertyName, Type propertyType)
{
var fieldBuilder = typeBuilder.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);
var propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
var getPropertyMethodBuilder = typeBuilder.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
var getIl = getPropertyMethodBuilder.GetILGenerator();
getIl.Emit(OpCodes.Ldarg_0);
getIl.Emit(OpCodes.Ldfld, fieldBuilder);
getIl.Emit(OpCodes.Ret);
var setPropertyMethodBuilder = typeBuilder.DefineMethod("set_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null, new[] { propertyType });
var setIl = setPropertyMethodBuilder.GetILGenerator();
var modifyProperty = setIl.DefineLabel();
var exitSet = setIl.DefineLabel();
setIl.MarkLabel(modifyProperty);
setIl.Emit(OpCodes.Ldarg_0);
setIl.Emit(OpCodes.Ldarg_1);
setIl.Emit(OpCodes.Stfld, fieldBuilder);
setIl.Emit(OpCodes.Nop);
setIl.MarkLabel(exitSet);
setIl.Emit(OpCodes.Ret);
propertyBuilder.SetGetMethod(getPropertyMethodBuilder);
propertyBuilder.SetSetMethod(setPropertyMethodBuilder);
}
}

View File

@@ -58,11 +58,12 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NJsonSchema.Extensions" Version="0.1.0" />
<PackageReference Include="NSwag.Core" Version="13.15.10" />
<PackageReference Include="SimMetrics.Net" Version="1.0.5" />
<PackageReference Include="Stef.Validation" Version="0.1.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.18" />
<!--<PackageReference Include="RandomDataGenerator.Net" Version="1.0.15" />-->
<PackageReference Include="JmesPath.Net" Version="1.0.125" />
<PackageReference Include="AnyOf" Version="0.3.0" />
<PackageReference Include="TinyMapper" Version="3.0.3" />